Effective C++筆記: 設計與聲明(二)

 

Item 21: 當你必須返回一個對象時,不要試圖返回其引用

一個函數創建一個新對象僅有兩種方法:在棧上或者在堆上。

 

在棧上分配:

棧上的生成物通過定義一個局部變量而生成。使用這個策略,你可以用這種方法試寫 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 是一個局部對象,而局部對象在函數退出時被銷燬。返回的引用指向一個不存在的object

 

在堆上分配:

那麼,讓我們考慮一下在堆上構造一個對象並返回引向它的引用的可能性。基於堆的對象通過使用 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* 返回的引用後面的指針。這是一個早已註定的資源泄漏。

 

採用Static

那麼,讓我們考慮一下在堆上構造一個對象並返回引向它的引用的可能性。基於堆的對象通過使用 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* 返回的引用後面的指針。這是一個早已註定的資源泄漏。

 

使用Static

注意到無論是在棧上的還是在堆上的方法,爲了從 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;
}

猜猜會怎麼樣?不管 abcd 的值是什麼,表達式 ((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 對象的值。如果它們不是永遠相等,那才真的會令人大驚失色了。

 

正確的做法:

寫一個必須返回一個新對象的函數的正確方法就是讓那個函數返回一個新對象。對於 Rational operator*,這就意味着下面這些代碼或在本質上與其相當的某些東西:

inline const Rational operator*(const Rational& lhs, const Rational& rhs)
{
  return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

當然,你可能付出了構造和析構 operator* 的返回值的成本,但是從長遠看,這只是爲正確行爲付出的很小的代價。

 

總結:

絕不要返回pointerreference指向一個local stack對象;

絕不要返回reference指向一個heap allocated對象;

絕不要返回pointerreference指向一個local static對象,而該對象又可能同時需要多個。

Item 4 中提供了一份如何在“單線程環境中合理返回reference指向一個local static對象”的實例)

 

Item 22: 將數據成員聲明爲 private

protected 數據成員不比 public 數據成員更具有封裝性,假設我們有一個protected成員變量,而我們最終取消了它,則所有使用它的derived classed都會被破壞!

從封裝的觀點來看,實際只有兩個訪問層次:private(提供了封裝)與其他(不提供封裝)。

 

總結:

聲明數據成員爲 private。它爲客戶提供了訪問數據的一致性,可細微劃分的訪問控制,更多的約束條件,而且爲類的作者提供了實現上的彈性。

protected 並不比 public 的封裝性強。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章