本條款聽起來很複雜,其實不然。它只是一個很簡單的道理,真的,相信我。
先看第一種情況:返回一個局部對象的引用。它的問題在於,局部對象 ----- 顧名思義 ----
僅僅是局部的。也就是說,局部對象是在被定義時建立,在離開生命空間時被銷毀的。所謂生命空間,是指它們所在的函數體。當函數返回時,程式的控制離開了這個空間,所以函數內部所有的局部對象被自動銷毀。因此,如果返回局部對象的引用,那個局部對象其實已經在函數調用者使用它之前被銷毀了。
當想提高程式的效率而使函數的結果通過引用而不是值返回時,這個問題就會出現。下面的例子和條款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)對象的引用也會工作不正常。看了之後,也許會協助你避免頭痛醫腳所帶來的麻煩。