天天看點

php 函數傳回局部對象,千萬不要傳回局部對象的引用,也不要傳回函數内部用new初始化的指針的引用...

它隻是一個很簡單的道理,真的,相信我。

先看第一種情況:傳回一個局部對象的引用。它的問題在于,局部對象 —– 顧名思義 —- 僅僅是局部的。也就是說,局部對象是在被定義時建立,在離開生命空間時被銷毀的。所謂生命空間,是指它們所在的函數體。當函數傳回時,程式的控制離開了這個空間,是以函數内部所有的局部對象被自動銷毀。是以,如果傳回局部對象的引用,那個局部對象其實已經在函數調用者使用它之前被銷毀了。

當想提高程式的效率而使函數的結果通過引用而不是值傳回時,這個問題就會出現。下面的例子和必須傳回一個對象時不要試圖傳回一個引用中的一樣,其目的在于詳細說明什麼時候該傳回引用,什麼時候不該:

C++

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

classrational{         // 一個有理數類

public:

rational(intnumerator=0,intdenominator=1);

~rational();

...

private:

intn,d;              // 分子和分母

// 注意operator* (不正确地)傳回了一個引用

friendconstrational&operator*(constrational&lhs,

constrational&rhs);

};

// operator*不正确的實作

inlineconstrational&operator*(constrational&lhs,

constrational&rhs)

{

rationalresult(lhs.n*rhs.n,lhs.d*rhs.d);

returnresult;

}

這裡,局部對象result在剛進入operator*函數體時就被建立。但是,所有的局部對象在離開它們所在的空間時都要被自動銷毀。具體到這個例子來說,result是在執行return語句後離開它所在的空間的。是以,如果這樣寫:

C++

1

2

3

rationaltwo=2;

rationalfour=two*two;        // 同operator*(two, two)

函數調用時将發生如下事件:

1. 局部對象result被建立。

2. 初始化一個引用,使之成為result的另一個名字;這個引用先放在另一邊,留做operator*的傳回值。

3. 局部對象result被銷毀,它在堆棧所占的空間可被本程式其它部分或其他程式使用。

4. 用步驟2中的引用初始化對象four。

一切都很正常,直到第4步才産生了錯誤,借用高科技界的話來說,産生了”一個巨大的錯誤”。因為,第2步被初始化的引用在第3步結束時指向的不再是一個有效的對象,是以對象four的初始化結果完全是不可确定的。

教訓很明顯:别傳回一個局部對象的引用。

“那好,”你可能會說,”問題不就在于要使用的對象離開它所在的空間太早嗎?我能解決。不要使用局部對象,可以用new來解決這個問題。”象下面這樣:

C++

1

2

3

4

5

6

7

8

9

10

11

// operator*的另一個不正确的實作

inlineconstrational&operator*(constrational&lhs,

constrational&rhs)

{

// create a new object on the heap

rational*result=

newrational(lhs.n*rhs.n,lhs.d*rhs.d);

// return it

return*result;

}

這個方法的确避免了上面例子中的問題,但卻引發了新的難題。大家都知道,為了在程式中避免記憶體洩漏,就必須確定對每個用new産生的指針調用delete,但是,這裡的問題是,對于這個函數中使用的new,誰來進行對應的delete調用呢?

顯然,operator*的調用者應該負責調用delete。真的顯然嗎?遺憾的是,即使你白紙黑字将它寫成規定,也無法解決問題。之是以做出這麼悲觀的判斷,是基于兩條理由:

第一,大家都知道,程式員這類人是很馬虎的。這不是指你馬虎或我馬虎,而是指,沒有哪個程式員不和某個有這類習性的人打交道。想讓這樣的程式員記住無論何時調用operator*後必須得到結果的指針然後調用delete,這樣的幾率有多大呢?也是說,他們必須這樣使用operator*:

C++

1

2

3

4

5

constrational&four=two*two;     // 得到廢棄的指針;

// 将它存在一個引用中

...

delete&four;                         // 得到指針并删除

這樣的幾率将會小得不能再小。記住,隻要有哪怕一個operator*的調用者忘了這條規則,就會造成記憶體洩漏。

傳回廢棄的指針還有另外一個更嚴重的問題,即使是最盡責的程式員也難以避免。因為常常有這種情況,operator*的結果隻是臨時用于中間值,它的存在隻是為了計算一個更大的表達式。例如:

C++

1

2

3

4

rationalone(1),two(2),three(3),four(4);

rationalproduct;

product=one*two*three*four;

product的計算表達式需要三個單獨的operator*調用,以相應的函數形式重寫這個表達式會看得更清楚:

C++

1

product=operator*(operator*(operator*(one,two),three),four);

是的,每個operator*調用所傳回的對象都要被删除,但在這裡無法調用delete,因為沒有哪個傳回對象被儲存下來。

解決這一難題的唯一方案是叫使用者這樣寫代碼:

C++

1

2

3

4

5

6

7

constrational&temp1=one*two;

constrational&temp2=temp1*three;

constrational&temp3=temp2*four;

delete&temp1;

delete&temp2;

delete&temp3;

果真如此的話,你所能期待的最好結果是人們将不再理睬你。更現實一點,你将會在指責聲中度日,或者可能會被判處10年苦力去寫威化餅幹機或烤面包機的微代碼。

是以要記住你的教訓:寫一個傳回廢棄指針的函數無異于坐等記憶體洩漏的來臨。

另外,假如你認為自己想出了什麼辦法可以避免”傳回局部對象的引用”所帶來的不确定行為,以及”傳回堆(heap)上配置設定的對象的引用”所帶來的記憶體洩漏,那麼,請轉到必須傳回一個對象時不要試圖傳回一個引用,看看為什麼傳回局部靜态(static)對象的引用也會工作不正常。看了之後,也許會幫助你避免頭痛醫腳所帶來的麻煩。

相關文章:必須傳回一個對象時不要試圖傳回一個引用據說愛因斯坦曾提出過這樣的建議:盡可能地讓事情簡單,但不要過于簡單。在c++語言中相似的說法應該是:盡可能地使程式高效,但不要過于高效。...

“new”和“malloc()”的不同點“malloc()”是個函數,接受(位元組)數目作為參數;它傳回一個指向未初始化空間的 void * 指針。“new”是個運算符,接受一個類型以及一套該類型的初始值(可選)作為參數;它傳回一個指向已被初始化(可選)的該類型的對象的指針。當你想為帶有非平凡初始化語義(non-trivial initialization semantics)的使用者自定義類型配置設定空間時,這兩者的差別是很明顯的。...

為什麼C++ 有指針也有引用C++ 的指針繼承于 C,若要移除指針,勢必造成嚴重的相容性問題。引用有幾方面的用處,但我在 C++ 中引入它的主要目的是為了支援運算符重載。例如:...

應該使用按值傳遞還是按引用傳遞這取決于你到底想達到什麼目的: 如果你想改變被傳遞的對象,那就按引用傳遞或者使用指針;例如 void f(X&); 或者 void f(X*); 如果你并不想改變被傳遞的對象,但該對象很大,那就按常量引用傳遞;例如 void f(const X&);...

寫operator new和operator delete時要遵循正常但事情也不是那麼簡單。因為operator new實際上會不隻一次地嘗試着去配置設定記憶體,它要在每次失敗後調用出錯處理函數,還期望出錯處理函數能想辦法釋放别處的記憶體。隻有在指向出錯處理函數的指針為空的情況下,operator new才抛出異常。...

避免隐藏标準形式的new因為内部範圍聲明的名稱會隐藏掉外部範圍的相同的名稱,是以對于分别在類的内部 和全局聲明的兩個相同名字的函數f來說,類的成員函數會隐藏掉全局函數:...

如果寫了operator new就要同時寫operator delete讓我們回過頭去看看這樣一個基本問題:為什麼有必要寫自己的operator new和operator delete? 答案通常是:為了效率。預設的operator new和operator delete具有非常好的通用性,它的這種靈活性也使得在某些特定的場合下,可以進一步改善它的性能。尤其在那些需要動态配置設定大量的但很小的對象的應用程式裡,情況更是如此。...

讓operator=傳回*this的引用c++的設計者bjarne stroustrup下了很大的功夫想使使用者自定義類型盡可能地和固定類型的工作方式相似。這就是為什麼你可以重載運算符,寫類型轉換函數,控制指派和拷貝構造函數,等等。他做了這麼多努力,那你最少也該繼續做下去。...

盡量用“傳引用”而不用“傳值”函數參數的傳遞在設計時,盡可能的選擇”傳引用“,不要用“傳值”的方式,有兩個好處:1.速度快2.可以将修改後的值帶回...

避免:其傳回值是指向成員的非const指針或引用,但成員的通路級比這個函數要低使一個成員為private或protected的原因是想限制對它的通路,對嗎?勞累的編譯器要費九牛二虎之力來確定你設定的通路限制不被破壞,對不對?是以,寫個函數來讓使用者随意地通路受限的成員沒多大意義,對不對?如果你确實認為有意義,那麼請反複閱讀本段,直到你不這樣認為為止。...