條款31: 千萬不要返回局部對象的引用,也不要返回函數內部用new初始化的指針的引用
本條款聽起來很復雜,其實不然。它只是一個很簡單的道理,真的,相信我。
先看第一種情況:返回一個局部對象的引用。它的問題在于,局部對象 ----- 顧名思義 ---- 僅僅是局部的。也就是說,局部對象是在被定義時創建,在離開生命空間時被銷毀的。所謂生命空間,是指它們所在的函數體。當函數返回時,程序的控制離開了這個空間,所以函數內部所有的局部對象被自動銷毀。因此,如果返回局部對象的引用,那個局部對象其實已經在函數調用者使用它之前被銷毀了。
當想提高程序的效率而使函數的結果通過引用而不是值返回時,這個問題就會出現。下面的例子和條款23中的一樣,其目的在于詳細說明什么時候該返回引用,什么時候不該:
class rational { // 一個有理數類
public:
rational(int numerator = 0, int denominator = 1);
~rational();
...
private:
int n, d; // 分子和分母
// 注意operator* (不正確地)返回了一個引用
friend const rational& operator*(const rational& lhs,
const rational& rhs);
};
// operator*不正確的實現
inline const rational& operator*(const rational& lhs,
const rational& rhs)
{
rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}
這里,局部對象result在剛進入operator*函數體時就被創建。但是,所有的局部對象在離開它們所在的空間時都要被自動銷毀。具體到這個例子來說,result是在執行return語句后離開它所在的空間的。所以,如果這樣寫:
rational two = 2;
rational four = two * two; // 同operator*(two, two)
函數調用時將發生如下事件:
1. 局部對象result被創建。
2. 初始化一個引用,使之成為result的另一個名字;這個引用先放在另一邊,留做operator*的返回值。
3. 局部對象result被銷毀,它在堆棧所占的空間可被本程序其它部分或其他程序使用。
4. 用步驟2中的引用初始化對象four。
一切都很正常,直到第4步才產生了錯誤,借用高科技界的話來說,產生了"一個巨大的錯誤"。因為,第2步被初始化的引用在第3步結束時指向的不再是一個有效的對象,所以對象four的初始化結果完全是不可確定的。
教訓很明顯:別返回一個局部對象的引用。
"那好,"你可能會說,"問題不就在于要使用的對象離開它所在的空間太早嗎?我能解決。不要使用局部對象,可以用new來解決這個問題。"象下面這樣:
// operator*的另一個不正確的實現
inline const rational& operator*(const rational& lhs,
const rational& rhs)
{
// create a new object on the heap
rational *result =
new rational(lhs.n * rhs.n, lhs.d * rhs.d);
// return it
return *result;
}
這個方法的確避免了上面例子中的問題,但卻引發了新的難題。大家都知道,為了在程序中避免內存泄漏,就必須確保對每個用new產生的指針調用delete,但是,這里的問題是,對于這個函數中使用的new,誰來進行對應的delete調用呢?
顯然,operator*的調用者應該負責調用delete。真的顯然嗎?遺憾的是,即使你白紙黑字將它寫成規定,也無法解決問題。之所以做出這么悲觀的判斷,是基于兩條理由:
第一,大家都知道,程序員這類人是很馬虎的。這不是指你馬虎或我馬虎,而是指,沒有哪個程序員不和某個有這類習性的人打交道。想讓這樣的程序員記住無論何時調用operator*后必須得到結果的指針然后調用delete,這樣的幾率有多大呢?也是說,他們必須這樣使用operator*:
const rational& four = two * two; // 得到廢棄的指針;
// 將它存在一個引用中
...
delete &four; // 得到指針并刪除
這樣的幾率將會小得不能再小。記住,只要有哪怕一個operator*的調用者忘了這條規則,就會造成內存泄漏。
返回廢棄的指針還有另外一個更嚴重的問題,即使是最盡責的程序員也難以避免。因為常常有這種情況,operator*的結果只是臨時用于中間值,它的存在只是為了計算一個更大的表達式。例如:
rational one(1), two(2), three(3), four(4);
rational product;
product = one * two * three * four;
product的計算表達式需要三個單獨的operator*調用,以相應的函數形式重寫這個表達式會看得更清楚:
product = operator*(operator*(operator*(one, two), three), four);
是的,每個operator*調用所返回的對象都要被刪除,但在這里無法調用delete,因為沒有哪個返回對象被保存下來。
解決這一難題的唯一方案是叫用戶這樣寫代碼:
const rational& temp1 = one * two;
const rational& temp2 = temp1 * three;
const rational& temp3 = temp2 * four;
delete &temp1;
delete &temp2;
delete &temp3;
果真如此的話,你所能期待的最好結果是人們將不再理睬你。更現實一點,你將會在指責聲中度日,或者可能會被判處10年苦力去寫威化餅干機或烤面包機的微代碼。
所以要記住你的教訓:寫一個返回廢棄指針的函數無異于坐等內存泄漏的來臨。
另外,假如你認為自己想出了什么辦法可以避免"返回局部對象的引用"所帶來的不確定行為,以及"返回堆(heap)上分配的對象的引用"所帶來的內存泄漏,那么,請轉到條款23,看看為什么返回局部靜態(static)對象的引用也會工作不正常。看了之后,也許會幫助你避免頭痛醫腳所帶來的麻煩。
回復 更多評論