天天看點

Effective C++ 第二章 構造/析構/指派運算

第二章 構造/析構/指派運算 (Constructors,Destructors,and Assignment Operators)

條款5:了解 C++ 默默編寫并調用哪些函數

請記住:

  • 編譯器可以暗自為 class 建立 default 構造函數、copy 構造函數、copy 指派運算符和析構函數

C++11中有6個:

1、構造

2、析構

3-4、拷貝構造和指派操作符

5-6、移動構造和指派操作符

條款6:若不想使用編譯器自動生成的函數,就該明确拒絕

  • 為駁回編譯器自動(暗自)提供的機能,可将相應的成員函數聲明為private并且不予實作。使用像Uncopyable這樣的base class也是一種做法
C++11中可以直接使用

= delete

來聲明拷貝構造函數,顯示禁止編譯器生成該函數。

條款7:為多态基類聲明

virtual

析構函數

帶有多态性質的基類必須将析構函數聲明為虛函數,防止指向子類的基類指針在被釋放時隻局部銷毀了基類對象。

  • polymorphic(帶多态性質的)base classes應該聲明一個virtual析構函數。如果class帶有任何virtual函數,它就應該擁有一個virtual析構函數
  • Classes 的設計目的如果不是作為base classes使用,或不是為了具備多态性(polymorphically),就不該聲明virtual析構函數
C++11可以用

final

定義不想作為基類的類。

條款8:别讓異常逃離析構函數

析構函數中抛出異常,會讓析構動作停止,這可能會導緻資源洩漏。

盡量在析構函數中解決所有異常。

  • 析構函數絕對不要吐出異常。如果一個被析構函數調用的函數可能抛出異常,析構函數應該捕捉任何異常,然後吞下它們(不傳播)或結束程式
  • 如果客戶需要對某個操作函數運作期間抛出的異常做出反應,那麼 class 應該提供一個普通函數(而非在析構函數中)執行該操作

條款9:絕不在構造和析構過程中調用

virtual

函數

有繼承的構造順序:先構造基類,再構造子類(和打好地基再建房子一個道理)

有繼承的析構順序:先析構子類,再析構基類(一點點拆)

在基類構造期間,虛函數絕對不會下降到子類層(即:在基類構造期間,虛函數不是虛函數)。

  • 在構造和析構期間不要調用 virtual 函數,因為這類調用從不下降至 derived class (比起目前執行構造函數和析構函數的那一層)

條款10:令

operator=

傳回一個

reference to *this

因為指派支援連鎖操作(采用右結合率),比如:

a = b = c;

,是以重載指派運算符必須傳回一個指向操作數左側對象的指針,也就是

*this

這不僅适用于标準指派運算符,也适用于任何指派相關的運算符重載,如

+=

-=

  • 令指派(assignment)傳回一個

    reference to *this

條款11:在

operator=

中處理 “自我指派”

緣起于一個憨批操作:将自己賦給自己,即自我指派。

一般不會有人這麼寫,但有些時候會比較隐蔽,比如:

a[i] = a[j]; // i == j
*px = *py;   // px == py
           

若有如下類(持有堆上記憶體的一個類):

class B {...};
class Widget {
    ...
private:
    B* pb;
};

Widget& Widget::operator=(const Widget& rhs)
{
    delete pb;  // pb 是 Wdiget 類中的一個指針對象
    pb = new B(*rhs.pb);
    return *this;
}
           

這個代碼中在自我指派便是錯的。

證同測試

即判斷是不是自我指派,如果是,不做任何事。

Widget& Widget::operator=(const Widget& rhs) {
  if (this == &rhs) return *this;
  
  delete pb;	
  pb = new B(*rhs.pb);				//如果此處抛出異常,pb将指向一塊已經被删除的記憶體。
  return *this;
}
           

使用臨時對象

複制pb所指東西前不要删除pb:

Widget& Widget::operator=(const Widget& rhs)
{
    B* temp = pb;
    pb = new B(*rhs.pb); // 此處可能抛出異常
    delete temp;
    return *this;
}
           

Copy and Swap

使用交換技術代替指派(異常安全性更好)。

Copy and Swap技術:為你打算修改的對象做出一份副本,然後在副本上做一切必要的修改。若有任何修改動作抛出異常,原對象任保持未改變狀态;待所有改變都成功後,再将修改過的那個副本與原對象在一個不抛出異常的操作中交換(swap)。

class Widget {
...
void swap(Widget& rhs);        // 用于交換 rhs 和 *this 的資料
...
};

//此方法更好
Widget& Widget::operator=(const Widget& rhs)
{
  	// 顯式調用拷貝構造函數
    Widget temp(rhs);         // 仍然需要臨時變量
    swap(temp);
    return *this;
}

// 此方法過于隐晦
// 隐含了一個拷貝動作
Widget& Widget::operator=(Widget rhs) // 另一種變體,傳值方式會複制一份副本
{
    swap(rhs);               // ths 本身是副本,直接交換
    return *this;
}
           
  • 確定當對象自我指派時

    operator=

    有良好行為。其中技術包括比較“來源對象”和“目标對象”的位址、精心周到的語句順序、以及 copy-and-swap
  • 确定任何函數如果操作一個以上的對象,而其中多個對象是同一個對象時,其行為仍然正确

條款12:複制對象時勿忘其每一個成分

自定義了構造函數和 copy 函數,随後又新增了成員變量,那麼需要自己留意在所有這些構造函數和 copy 函數中添加這個新的成員變量的操作,編譯器不會提醒你。一旦忘記了,初始化或指派操作就是不完整的。

如果是一個派生類中的 copy 函數,除了處理好派生類内的成員對象的 copy 操作,還要負責基類中對象的 copy 操作,也就是在初始化清單中完成對基類的 copy 操作。如下代碼:

class Derived : public Base {
public:
  ...
  Derived(const Derived& rhs);
  Derived& operator=(const Derived& rhs);
private:
  int p;
};

Derived::Derived(const Derived& rhs)
  : Base(rhs),  // 注意這裡,手動調用基類的 copy 構造函數
    p(rhs.p)
{
  ...
}

Derived& Derived::operator=(const Derived& rhs)
{
  ...
  Base::operator=(rhs);  // 注意這裡,手動調用基類的 copy 指派運算符函數
  p = rhs.p;
  return *this;
}   
           

相同代碼可以抽象為一個成員函數init();

  • Copying 函數應該確定複制 “對象内的所有成員變量” 及 “所有 base class 成分”
  • 不要嘗試以某個 copying 函數實作另一個 copying 函數。應該将共同機能放進第三個函數中,并由兩個 copying 函數共同調用

reference

[1] Effective C++ · Scott Meyers

[2] 重述Effective C++

繼續閱讀