程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++箴言:必須返回對象時別返回引用

C++箴言:必須返回對象時別返回引用

編輯:C++入門知識

  一旦程序員抓住對象傳值的效率隱憂,很多人就會成為狂熱的聖戰分子,誓要根除傳值的罪惡,無論它隱藏多深。他們不屈不撓地追求傳引用的純度,但他們全都犯了一個致命的錯誤:他們開始傳遞並不存在的對象的引用。這可不是什麼好事。
  
  
  
  考慮一個代表有理數的類,包含一個將兩個有理數相乘的函數:
  
   class Rational {
   public:
  Rational(int numerator = 0, // see Item 24 for why this
  int denominator = 1); // ctor isn’t declared eXPlicit
  
  ...
  
   private:
  int n, d; // numerator and denominator
  
   friend:
  const Rational // see Item 3 for why the
  operator*(const Rational& lhs, // return type is const
  const Rational& rhs);
  };
  operator* 的這個版本以傳值方式返回它的結果,而且假如你沒有擔心那個對象的構造和析構的代價,你就是在推卸你的專業職責。假如你不是迫不得已,你不應該為這樣的一個對象付出成本。所以問題就在這裡:你是迫不得已嗎?
  
  哦,假如你能用返回一個引用來作為代替,你就不是迫不得已。但是,請記住一個引用僅僅是一個名字,一個實際存在的對象的名字。無論何時只要你看到一個引用的聲明,你應該馬上問自己它是什麼東西的另一個名字,因為它必定是某物的另一個名字。在這個 operator* 的情況下,假如函數返回一個引用,它必須返回某個已存在的而且其中包含兩個對象相乘的產物的 Rational 對象的引用。
  
  當然沒有什麼理由期望這樣一個對象在調用 operator* 之前就存在。也就是說,假如你有
  
   Rational a(1, 2); // a = 1/2
  Rational b(3, 5); // b = 3/5
  
  Rational c = a * b; // c should be 3/10
  似乎沒有理由期望那裡碰巧已經存在一個值為十分之三的有理數。不是這樣的,假如 operator* 返回這樣一個數的引用,它必須自己創建那個數字對象。
  
  一個函數創建一個新對象僅有兩種方法:在棧上或者在堆上。棧上的生成物通過定義一個局部變量而生成。使用這個策略,你可以用這種方法試寫 operator*:
  
   const Rational& operator*(const Rational& lhs, // warning! bad code!
  const Rational& rhs)
  {
   Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
   return result;
  }
  你可以立即否決這種方法,因為你的目標是避免調用構造函數,而 result 正像任何其它對象一樣必須被構造。一個更嚴重的問題是這個函數返回一個引向 result 的引用,但是 result 是一個局部對象,而局部對象在函數退出時被銷毀。那麼,這個 operator* 的版本不會返回引向一個 Rational 的引用——它返回引向一個前 Rational;一個曾經的 Rational;一個空洞的、惡臭的、腐敗的,從前是一個 Rational 但永不再是的屍體的引用,因為它已經被銷毀了。任何調用者甚至於沒有來得及匆匆看一眼這個函數的返回值就馬上進入了未定義行為的領地。這是事實,任何返回一個引向局部變量的引用的函數都是錯誤的。(對於任何返回一個指向局部變量的指針的函數同樣成立。)
  
  那麼,讓我們考慮一下在堆上構造一個對象並返回引向它的引用的可能性。基於堆的對象通過使用 new 而開始存在,所以你可以像這樣寫一個基於堆的 operator*:
  
   const Rational& operator*(const Rational& lhs, // warning! more bad
  const Rational& rhs) // code!
  {
   Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
   return *result;
  }
  哦,你還是必須要付出一個構造函數調用的成本,因為通過 new 分配的內存要通過調用一個適當的構造函數進行初始化,但是現在你有另一個問題:誰是刪除你用 new 做出來的對象的合適人選?
  
  即使調用者盡職盡責且一心向善,它們也不太可能是用這樣的方案來合理地預防洩漏:
  
   Rational w, x, y, z;
  
  w = x * y * z; // same as operator*(operator*(x, y), z)
  這裡,在同一個語句中有兩個 operator* 的調用,因此 new 被使用了兩次,這兩次都需要使用 delete 來銷毀。但是 operator* 的客戶沒有合理的辦法進行那些調用,因為他們沒有合理的辦法取得隱藏在通過調用 operator* 返回的引用後面的指針。這是一個早已注定的資源洩漏。
  
  但是也許你注重到無論是在棧上的還是在堆上的方法,為了從 operator* 返回的每一個 result,我們都不得不容忍一次構造函數的調用。也許你想起我們最初的目標是避免這樣的構造函數調用。也許你認為你知道一種方法能避免除一次以外幾乎全部的構造函數調用。也許下面這個實現是你做過的,一個基於 operator* 返回一個引向 static Rational 對象的引用的實現,而這個 static Rational 對象定義在函數內部:
  
   const Rational& operator*(const Rational& lhs, // warning! yet more
  const Rational& rhs) // bad code!
  {
   static Rational result; // static object to which a
   // reference will be returned
  
   result = ... ; // multiply lhs by rhs and put the
   // prodUCt inside result
   return result;
  }
  就像所有使用了 static 對象的設計一樣,這個也會立即引起我們的線程安全(thread-safety)的混亂,但那是它的比較明顯的缺點。為了看到它的更深層的缺陷,考慮這個完全合理的客戶代碼:
  
  
   bool operator==(const Rational& lhs, // an operator==
  const Rational& rhs); // for Rationals
  
  Rational a, b, c, d;
  
  ...
  if ((a * b) == (c * d)) {
   do whatever’s appropriate when the products are equal;
  } else {
   do whatever’s appropriate when they’re not;
  }
  猜猜會怎麼樣?不管 a,b,c,d 的值是什麼,表達式 ((a*b) == (c*d)) 總是等於 true!
  
  假如代碼重寫為功能完全等價的另一種形式,這一啟示就很輕易被理解了:
  
   if (operator==(operator*(a, b), operator*(c, d)))
  注重,當 operator== 被調用時,將同時存在兩個起作用的對 operator* 的調用,每一個都將返回引向 operator* 內部的 static Rational 對象的引用。因此,operator== 將被要求比較 operator* 內部的 static Rational 對象的值和 operator* 內部的 static Rational 對象的值。假如它們不是永遠相等,那才真的會令人大驚失色了。
  
  這些應該足夠讓你信服試圖從類似 operator* 這樣的函數中返回一個引用純粹是浪費時間,但是你們中的某些人可能會這樣想“好吧,就算一個 static 不夠用,也許一個 static 的數組是一個竅門……”
  
  我無法拿出示例代碼來肯定這個設計,但我可以概要說明為什麼這個想法應該讓你羞愧得無地自容。首先,你必須選擇一個 n 作為數組的大小。假如 n 太小,你可能會用完存儲函數返回值的空間,與剛剛名譽掃地的 single-static 設計相比,在任何一個方面你都不會得到更多的東西。但是假如 n 太大,就會降低你的程序的性能,因為在函數第一次被調用的時候數組中的每一個對象都會被構造。即使這個我們正在討論的函數僅被調用了一次,也將讓你付出 n 個構造函數和 n 個析構函數的成本。假如“優化”是提高軟件效率的過程,對於這種東西也只能是“悲觀主義”的。最後,考慮你怎樣將你所需要的值放入數組的對象中,以及你做這些需要付出什麼。在兩個對象間移動值的最直接方法就是通過賦值,但是一次賦值將要付出什麼?對於很多類型,這就大約相當於調用一次析構函數(銷毀原來的值)加上調用一次構造函數(把新值拷貝過去)。但是你的目標是避免付出構造和析構成本!面對的結果就是:這個方法絕對不會成功。(不,用一個 vector 代替數組也不會讓事情有多少改進。)
  
  寫一個必須返回一個新對象的函數的正確方法就是讓那個函數返回一個新對象。對於 Rational 的 operator*,這就意味著下面這些代碼或在本質上與其相當的某些東西:
  
   inline const Rational operator*(const Rational& lhs, const Rational& rhs)
  {
   return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
  }
  當然,你可能付出了構造和析構 operator* 的返回值的成本,但是從長遠看,這只是為正確行為付出的很小的代價。除此之外,這種令你感到恐怖的賬單也許永遠都不會到達。就像所有的程序設計語言,C++ 答應編譯器的實現者在不改變生成代碼的可觀察行為的條件下使用優化來提升它的性能,在某些條件下會產生如下結果:operator* 的返回值的構造和析構能被安全地消除。假如編譯器利用了這一點(編譯器經常這樣做),你的程序還是在它假定的方法上繼續運行,只是比你期待的要快。 全部的焦點在這裡:假如需要在返回一個引用和返回一個對象之間做出決定,你的工作就是讓那個選擇能提供正確的行為。讓你的編譯器廠商去絞盡腦汁使那個選擇盡可能地廉價。
  
  Things to Remember
  
  ·絕不要返回一個局部棧對象的指針或引用,絕不要返回一個被分配的堆對象的引用,假如存在需要一個以上這樣的對象的可能性時,絕不要返回一個局部 static 對象的指針或引用。
 
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved