大家都知道一個常識:“千萬不要返回局部對象或變量的引用和指針”。 既然所有C++權威的書上都要求“一定不要返回局部對象或變量的引用和指針”,那為什么C++編譯器不從語法上直接禁掉這種用法,讓你編譯通不過(在技術上應該不難實現的)。如果只是建議的話,那么“返回局部對象或變量的引用和指針”是否有用武之地呢?(從理論上來講,我認為這種做法似乎總是錯誤的,原因大家都知道。) EX(1) #include using namespace std; class CComplex { public: CComplex():real(0),image(0){} CComplex(double real,double image):real(real),image(image){} CComplex& operator+(const CComplex& second) { CComplex temp(real+second.real,image+second.image); return temp; } void Print() { cout《"("《real《"+"《image《"i)"《endl; } private: double real; double image; }; int main() { CComplex a(2,4); CComplex b(1.5,3.5); CComplex c=a+b; c.Print(); return 0; } operator+返回的是臨時對象的引用,為什么能正確地工作??? EX(2) #include #include using namespace std; string& f() { string s("hello"); return s; } int main() { cout《f()《endl; return 0; } 同樣是對象,為什么string對象就不行,就因為string比較特殊??? EX(3) #include #include using namespace std; double& f() { double d(5.55); return d; } int main() { cout《f()《endl; return 0; } 為什么內置類型(int,float等均可)返回局部變量的引用總可以正確地工作??? 這個問題似乎以前已經有人討論過,但一直沒有定論。 不要跟我說運行正確是因為我運氣好,運氣不好地話就輸出任意值; 我運行了N次,未見任何異常,也不要說運行上千萬次才有可能出問題; 我在GCC和VS2010上都驗證過了,我覺得是不是編譯器做了相應的優化啊(特別是針對內置基本類型)。 有想法的兄弟望賜教,感激不盡!! int main() { CComplex a(2,4); CComplex b(1.5,3.5); CComplex c=a+b; c.Print(); return 0; } operator+返回的是臨時對象的引用,為什么能正確地工作??? 答:main函數在執行之后,a,b入棧,接著a+b調用了operator+,temp也入棧,operator+執行完后,temp出棧并調用析構函數,由于出棧僅僅是移動了PC指針,而你又未寫析構函數將CComplex清零,因此temp所占的那塊棧空間的內存依然保持原樣,只是PC指針已經不再指向它,而operator+返回的引用其實指向的是temp所占內存,然后在調用CComplex的默認拷貝構造的函數的時候,由于拷貝構造函數的輸入參數也是引用,因此也指向temp那塊內存,對此快內存也會按照CComplex類型來進行訪問,最后c就得到了temp的內容。這里即使是寫成CComplex& c=a+b;結果也是能輸出temp的內容的。此時你若在此句話后面再加幾個函數調用,這些函數必須要有參數或內部定義有變量,然后再c.Print(),你會發現結果完全變了。 EX(2) #include #include using namespace std; string& f() { string s("hello"); return s; } int main() { cout《f()《endl; return 0; } 同樣是對象,為什么string對象就不行,就因為string比較特殊??? 答:因為s在出棧的時候其析構函數會將內存都清掉,在外面還想訪問自然訪問不成功了。 EX(3) #include #include using namespace std; double& f() { double d(5.55); return d; } int main() { cout《f()《endl; return 0; } 答:理解了上面兩個答案,這個我就不用多說了吧。 每個人必有其背后的深刻原因,只是受限于種種因素,人們不可能都去搞明白。更多時候,并不是原因不充分,只是人們以其自己的知識背景還不足以理解。 一、為什么不禁用的問題 為什么不禁引用返回局部變量,技術上真的是不難嗎?且,有足夠的必要嗎?請見以下例子: int *f1(int &ri) { return &ri; } int *f2() { int i=4; int *j; j=f1(i); return j; } int main() { int *p=f2(); *p=6; return 0; } p在初始化后,*p生命期是否已經結束了呢?我相信,如果這件事也得由編譯器去判斷,那么顯然,程序員全部可以下崗了,編譯器實在是太智能了,人還有必要存在嗎?但現有技術真的能嗎?如果能的話,要花多大開銷,這個開銷有必要嗎?“千萬不要返回局部對象或變量的引用和指針”應該是個原則性的東西,它是個典型代表,其實大原則是“不要在自動變量(不管是表達式中間結果的臨時變量(如果它不能保證總優化到寄存器中)還是源程序中有明確名字的auto變量)生命期結束后還試圖解引用它”。 程序設計語言課一般會說語言的可寫性與可讀性是對矛盾,C語言的可寫性特別強,既會給比較強的人非常靈活的選擇,又會讓入門者走不少彎路或者半途而廢。利器不是誰都能用得好,這與水平不水平沒什么關系,說人的水平不足夠使用C++,當然也可以站在沒有學會用C++的人的立場,說C++太過于復雜,以至大多數人是學不會用不好的,但它的每個設計的確都有它的現實考慮,編程語言是很實在的東西,往往外貌冷冰冰但其為什么是這樣有充足原因。 二、你的好運氣 你要是明白函數調用時局部變量是如何入棧出棧的,看看反匯編的代碼,并跟蹤一下堆棧的變化情況,你會設計出一個讓值產生變化的例子。如果這類錯誤后,導致被改變的值,并不是指針的值,則在這么小的程序中,系統不一定都崩潰,它不過是讓部分你沒照顧到的地方變了變值,卻沒有影響輸出。 建議樓主閱讀一下TCPL有關臨時變量一節,看看各種條件下生成的臨時變量的作用域,與給出名字的局部變量間,有何差同。 三、其他一些為什么的例子 關于C++的為什么特別多,如果你不是經驗豐富且善于思考,是很難理解為什么有這么多為什么的。當然,為什么的多少,是個程度問題,有差異存在的地方就有程度問題,不同的人善用不同的東西,C++是“小眾”的,但還不至于只是幾個人的,畢竟TIOBE還排第3。 1.operator重載的解析順序為什么如現在標準那樣設計?是權衡了使用者的方便,和編譯器的效率之間的一種平衡,它過度自由帶來的是呈指數級上升的編譯時開銷,且該開銷并不一定值得。 2.內置數組,為什么不設置下標檢測?如果檢測下標,定然就會在每次訪問下標時,做是否越界的檢驗,這就帶來了運行時開銷。如果你的算法非常好,定然不需要檢測下標,則語言假定一定要在每次訪問下標時都判斷,就會影響效率并失去選擇的機會。如果設置N個選項,可以用來關閉或打開是否檢測下標,那不應該是一種語言應該干的,各有各的側重點。 3.C語言傳數組參數為什么默認是轉換成指針類型?以C語言產生那個年代的硬件條件,復制數組很奢侈,尤其函數被調用往往很頻繁,算法要盡量往不復制的情況下設計,如果實在必要,非要復制,你也可以手動memcpy嘛!總之它不是默認項。C++給了用戶另一種選項,即通過加上引用,而使得能夠真正傳整個數組,不過這都是很多年以后的事了。 4.for語句為什么有的靈活有的嚴格?像在Ada中的語法,便是禁止循環變量被改變,且不能設置步長值,要想達到這兩個目的,便只能用其他變量再過渡,這樣做是為了高度的安全。反之,C語言的for則非常靈活,也沒有Ada那么多的限制,但這種靈活并不能保證用戶用其寫出錯誤邏輯的代碼;VB的自由度則介于二者之間。不能因為這些語言的設計不同,而指責其中某一種語言為何不對某一語法特性做必要的限制,它真的必要嗎?個案好說,但綜合全局,很難評估。 四、設計者們不傻 且任何有影響力的技術,其規范,都是經過全球大量從業者多年實踐后,總結整理并論證出來的,并不是一個或幾個人拍拍腦袋就草率決定的,因此C++的新標準化過程要歷時8年之久。Bjarne Stroustrup不傻,Herb Sutter, Stanley Lippman, Scott Meyer, Alexander Stepanov, Andrew Koenig等人也不傻,標準委員會都不是白給的,大多數細節問題早就被提出過。具體實現,要難得多,這點語法層面上的皮毛問題,都不值一提。 |