程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 讀書筆記 effective c++ Item 21 當你必須返回一個對象的時候,不要嘗試返回引用

讀書筆記 effective c++ Item 21 當你必須返回一個對象的時候,不要嘗試返回引用

編輯:關於C++

讀書筆記 effective c++ Item 21 當你必須返回一個對象的時候,不要嘗試返回引用。本站提示廣大學習愛好者:(讀書筆記 effective c++ Item 21 當你必須返回一個對象的時候,不要嘗試返回引用)文章只能為提供參考,不一定能成為您想要的結果。以下是讀書筆記 effective c++ Item 21 當你必須返回一個對象的時候,不要嘗試返回引用正文


1. 問題的提出:要求函數返回對象時,可以返回引用麼?

一旦程序員理解了按值傳遞有可能存在效率問題之後(Item 20),許多人都成了十字軍戰士,決心清除所有隱藏的按值傳遞所引起的開銷。對純淨的按引用傳遞(不需要額外的構造或者析構)的追求絲毫沒有懈怠,但他們的始終如一會產生致命的錯誤:它們開始傳遞指向並不存在的對象的引用。這可不是好事情。

考慮表示有理數的一個類,它包含將兩個有理數相乘的函數(Item 3):

 1 class Rational {
 2 
 3 public:
 4 
 5 Rational(int numerator = 0, // see Item 24 for why this
 6 
 7 int denominator = 1); // ctor isn’t declared explicit
 8 
 9 ...
10 
11 private:
12 
13 int n, d; // numerator and denominator
14 
15 friend
16 
17 const Rational // see Item 3 for why the
18 
19 operator*(const Rational& lhs, // return type is const
20 
21 const Rational& rhs);
22 
23 };

Operator* 的這個版本為按值返回結果,如果你沒有為調用這個對象的構造函數和析構函數造成的開銷而擔心,你就是在逃避你的專業職責。如果這個對象不是必須的,你就不想為這樣一個對象的開銷去買單。所以問題是:這個對象的生成是必須的麼?

 

2. 問題的分析(一):如返回引用,必須為返回的引用創建一個新的對象

如果你能夠返回一個引用那麼就不是必須為其買單。但是記住引用只是一個別名,一個已存對象的別名。每當你聲明一個引用時,你應該馬上問問自己它用來做誰的別名,因為它必須是某些東西的別名。對於operator*來說,如果這個函數返回一個引用,它必須返回一個指向已存在Rational對象的引用,這個對象包含了兩個對象的乘積結果。

沒有任何理由假設在調用operator*之前這樣一個對象已經存在了。也就是說,如果你進行下面的操作:

1 Rational a(1, 2); // a = 1/2
2 
3 Rational b(3, 5); // b = 3/5
4 
5 Rational c = a * b; // c should be 3/10

期望已經存在一個值為3/10的有理數看上去是不合理的。如果operator*即將返回一個指向值為3/10的有理數的引用,它必須自己創建出來。

3. 問題的分析(二):創建新對象的三種錯誤方法 3.1 在棧上創建reference指向的對象

一個函數只可以通過兩種方法來創建一個新的對象:在棧上或者在堆上。通過定義一個本地變量來完成棧上的對象創建。使用這個策略,你可以嘗試使用下面的方法來實現:operator*:

 1 const Rational& operator*(const Rational& lhs, // warning! bad code!
 2 
 3 const Rational& rhs)
 4 
 5 {
 6 
 7 Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
 8 
 9 return result;
10 
11 }

你會立即否決這種做法,因為你的目標是避免調用構造函數,但是這裡的result必須被構造出來。更加嚴重的問題是:這個函數返回指向result的引用,但result是一個本地對象,當函數退出的時候這個對象就會被銷毀。所以這個版本的operator*並沒有返回指向Rational的引用,它返回的引用指向從前的Rational對象,現在變成了一個空的,令人討厭的,已經腐爛的Rational對象的屍體,它已經被銷毀了。任何使用這個函數的返回值的調用者都將會馬上進入未定義行為的范圍。事實是,任何返回指向本地對象的引用的函數都是被破壞掉的函數。(返回指向本地對象的指針的函數也是如此)。

3.2 在堆上創建reference指向的對象

讓我們再考慮一下下面這種用法的可能性:在堆上創建一個對象並且返回指向它的引用。堆上的對象通過使用new來創建,所以你可以像下面這樣實現一個基於堆的operator*:

 1 const Rational& operator*(const Rational& lhs, // warning! more bad
 2 
 3 const Rational& rhs) // code!
 4 
 5 {
 6 
 7 Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
 8 
 9 return *result;
10 
11 }

這裡你仍然需要為構造函數的調用買單,對new分配的內存進行初始化是通過調用一個合適的構造函數來實現的,但是現在有另外一個問題:誰在這個對象上應用new召喚出來的delete?

即使是一個認真負責的,心懷善意的調用者,對於下面這種合理的使用場景,他們也沒有什麼方法來避免內存洩漏:

1 Rational w, x, y, z;
2 
3 w = x * y * z; // same as operator*(operator*(x, y), z)

 

這裡,在同一個語句中調用了兩次operator*,因此使用了兩次new,這也需要使用兩次delete來對new出來的對象進行銷毀。沒有什麼合理的方法來讓operator*的客戶來進行這些調用,因為對於他們來說沒有合理的方法來獲取隱藏在從operator*返回回來的引用後面的指針。這麼做保證會產生資源洩漏。

3.3 為reference創建 static對象 3.3.1單一static 對象

你可能注意到了,不管是在堆上還是棧上創建從operator*返回的結果,你都必須要調用一個構造函數。可能你能回憶起來我們的初衷是避免這樣的構造函數調用。可能你認為你知道一種只需要調用一次構造函數,其余的構造函數被避免調用的方法。下面的這種實現突然出現了,這種方法基於另外一種operator*的實現:令其返回指向static Rational對象的引用,函數實現如下:

 1 const Rational& operator*(const Rational& lhs, // warning! yet more
 2 
 3 const Rational& rhs) // bad code!
 4 
 5 {
 6 
 7 static Rational result; // static object to which a
 8 
 9 // reference will be returned
10 
11 result = ... ; // multiply lhs by rhs and put the
12 
13 // product inside result
14 
15 return result;
16 
17 }

 

像所有使用靜態對象的設計一樣,這種方法增加了對於線程安全的梳理工作,但這個缺點是比較明顯的。為了看一下更深層次的缺陷,考慮一份完全合理的客戶代碼:

 1 bool operator==(const Rational& lhs, // an operator==
 2 
 3 const Rational& rhs); // for Rationals
 4 
 5 Rational a, b, c, d;
 6 
 7 ...
 8 
 9 if ((a * b) == (c * d)) {
10 
11 do whatever’s appropriate when the products are equal;
12 
13 } else {
14 
15 do whatever’s appropriate when they’re not;
16 
17 }

 

你猜怎麼著?表達式((a*b) == (c*d))的求值結果總為true,而不管a,b,c,d的值是什麼!

將表達式用等價的函數形式進行重寫,上面的不可思議的事情就能很容易明白:

1 if (operator==(operator*(a, b), operator*(c, d)))

注意當operator==被調用的時候,已經調用了兩次operato*,每次調用都會返回指向operator*中的static Raitional對象的引用。因此,operator==會對operator*中的static Rational對象和operator* 中的static Rational對象進行比較。如果不相等就奇怪了。

3.3.2 Static數組

這應該足夠使你相信從像operator*一樣的函數中返回一個引用是在浪費時間,但是一些人現在開始想了:好,如果一個static不夠,可能一個static數組能夠達到目的。。。

我不能提供示例代碼來讓這個設計顯得如此高貴,但是我能描述一下為什麼這個想法會讓你感到羞愧臉紅。首先,你必須選擇一個合適的n,也就是數組的大小。如果n太小,你可能會耗盡存儲函數返回值的空間,這樣對於上面的單一靜態對象設計來說,我們沒有獲得任何好處。如果n太大,你的程序的性能會降低,因為即使這個函數僅被使用一次,在第一次被調用之前,數組中的每一個對象都會被構造出來。這會讓你付出調用n個構造函數和n個析構函數的代價。如果最優化(optimization)是改善軟件性能的一個過程,那麼這種事情應該被叫做“最差化”(pessimization)。最後,想象一下你該如何把你所需要的值放入數組的對象中,並且這樣做會付出什麼代價。最直接的方法是通過賦值來對對象之間的值進行移動,但是賦值的代價是什麼呢?對於許多類型來說,賦值等同於調用一個析構函數(釋放舊值)和一個構造函數(拷貝新值)。但是你的目標是要避免析構和構造的開銷!直面它把,這個方法沒有奏效。(使用vector來代替數組也不會對問題有所改善。)

4. 問題結論:從函數中返回新對象的正確方法是——返回對象

實現一個必須返回一個新對象的函數的正確方法是讓函數返回新的對象(value不是reference)。對於Rational的opertaor*函數來說,其實現如下面的代碼(或者與其等價的代碼):

1 inline const Rational operator*(const Rational& lhs, const Rational& rhs)
2 
3 {
4 
5 return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
6 
7 }

當然,你會從operator*的返回值中引入構造和析構的開銷,但從長遠來看,這是為正確的行為付出了一個小的代價。此外,讓你毛骨悚然的賬單再也不會到來。像許多編程語言一樣,C++允許編譯器實現者在不改變可視化代碼行為的前提下,對代碼進行優化,以達到改善生成碼性能的目的。在一些情況中,我們發現,operator*返回值的構造和析構可以被安全的消除。當編譯器利用了這個事實(編譯器經常這麼做),你的程序就會以你所期望的方式進行下去,只是比你想要的要快。

將本條款歸結如下:在返回一個引用還是返回一個對象之間做決定時,你的工作是選擇能夠提供正確行為的那個。對於“如何使這個選擇有盡可能小的開銷”這個問題的解決,讓編譯器供應商去斗爭把。

5. 總結

絕不要返回指向本地棧對象的指針或者引用,指向堆對象的引用,或者在有可能需要多個對象的時候返回指向本地靜態對象的指針或者引用。(Item 4)給出了一種設計的一個例子,說明了返回指向本地靜態對象的引用是合理的,至少在單線程環境中。)

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved