天天看點

Effective C++: 02構造、析構、指派運算

05:了解C++默默編寫并調用哪些函數

         1:一個空類,如果你自己沒聲明,編譯器就會為它聲明(編譯器版本的)一個copy構造函數、一個copy assignment操作符和一個析構函數。此外如果你沒有聲明任何構造函數,編譯器也會為你聲明一個default構造函數。所有這些函數都是public且inline的。

         2:隻有當這些函數被調用時,它們才會被編譯器建立出來。

         3:編譯器生成的default構造函數和析構函數主要是給編譯器一個地方用來放置“藏身幕後”的代碼,比如用base classes和non-static成員變量的構造函數和析構函數。至于copy構造函數和copy assignment操作符,編譯器建立的版本隻是單純的将來源對象的每一個non-static成員變量拷貝到目标對象。

         4:編譯器建立的析構函數是個non-virtual,除非這個class的base class自身聲明有virtual析構函數。

         5:以下情況下,編譯器會拒絕為class生出operator=:

a、類中具有引用成員或者const 常量成員:

template<class T>
class NamedObject {
public:
  NamedObject(std::string& name, const T& value);
  ...                               
private:
  std::string& nameValue;           // this is a reference
  const T objectValue;              // this is const
};      

 引用和常量必須在定義時進行初始化,不支援指派操作。是以編譯器拒絕為這樣的類建立operator=函數;

如果你打算在一個包含reference成員或const成員的class内支援指派操作,你必須自己定義copy assignment操作符。

b、如果某個base class将copy assignment操作符聲明為private,編譯器也拒絕為其derived class生成一個copy assignment操作符。

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

         1:如果不希望class支援複制初始化或者指派操作,因為不定義copy構造函數和copy assignment操作符,編譯器會自動建立一個,是以不定義這倆函數達不到這個目的。

         2:因為編譯器建立的函數都是public的,為了阻止這些函數被建立出來,可以将copy構造函數或copy assignment操作符聲明為private。這樣便阻止了編譯器建立這些函數,而且類的使用者也無法調用它們。

         3:上面的做法還是有漏洞,因為類的成員函數和友元函數還是可以調用你的private函數。這種情況下,可以僅僅聲明而不去定義它們。這種情況下,如果有成員函數或友元函數調用它們的話,将會産生一個連接配接錯誤。

         是以,将複制構造函數和指派操作符聲明為private且不去定義它們,當類的使用者企圖拷貝時,編譯器會阻止他;如果在成員函數或友元函數中這麼做,連接配接器會發出抱怨。

         4:将連接配接期錯誤移至編譯期是可能的(而且那時好事,畢竟越早偵測出錯誤越好),隻要定義一個Uncopyable類,并将自己的類繼承該類就好:

class Uncopyable {
protected:                                   // allow construction
  Uncopyable() {}                            // and destruction of
  ~Uncopyable() {}                           // derived objects...

private:
  Uncopyable(const Uncopyable&);             // ...but prevent copying
  Uncopyable& operator=(const Uncopyable&);
};      

          任何人(包括成員函數或友元函數)嘗試拷貝Uncopyable類的派生類對象時,編譯期便試着生成一個copy構造函數和一個copy assignment操作符。這些函數的編譯器生成版會嘗試調用其base class的對應函數,那些調用會被編譯器拒絕,因為其base class的拷貝函數是private。

07:為多态基類聲明virtual析構函數

         1:當derived class對象經由一個base class指針删除,而該base class帶着一個non-virtual析構函數,則其結果是未定義的。實際執行時通常發生的是對象的derived成分沒被銷毀,而其base class成分通常會被銷毀,于是造成一個詭異的“局部銷毀”對象。

         2:任何class隻要帶有virtual函數都幾乎确定應該也有一個virtual析構函數。

         3:如果class不含virtual函數,通常表示它并不願意被用做一個base class。當class不企圖被當作base class時,令其析構函數為virtual往往是個饅主意。

欲實作出virtual函數,對象必須攜帶某些資訊,主要用來在在運作期決定哪一個virtual函數該被調用。這份資訊通常是由一個所謂vptr ( virtual table pointer)指針指出。vptr指向一個由函數指針構成的數組,稱為vtbl ( virtual table );每一個帶有virtual函數的class都有一個相應的vtbl。當對象調用某一virtual函數,實際被調用的函數取決于該對象的vptr所指的那個vtb----編譯器在其中尋找适當的函數指針。

是以,無端的将某個class的析構函數聲明為virtual,會增加對象的體積(vptr)。是以許多人的心得是:隻有當class内含至少一個virtual函數,才為它聲明virtual析構函數。

        4:标準string,以及所有STL容器如vector,list,set,map等等,都不virtual析構函數,是以,不應該将它們當做base class。

        5:如果抽象基類聲明了virtual析構函數,則必須為它提供一份定義:

class AWOV { 
public:
  virtual ~AWOV() = 0;   // declare pure virtual destructor
};

AWOV::~AWOV() {}         // definition of pure virtual    dtor      

         這是因為,派生類繼承該抽象基類後,派生類對象銷毀時,會首先調用派生類的析構函數,然後是基類的析構函數。是以編譯器會在AWOV的derived classes的析構函數中建立一個對~AWOV的調用,是以必須提供一份定義,否則會連接配接錯誤。

08:别讓異常逃離析構函數

         C++并不禁止析構函數吐出異常,但它不鼓勵這麼做。如果某個類的析構函數有可能抛出異常,則要麼:抛出異常時直接調用abort退出程式;要麼抛出異常時吞下異常,僅記錄日志。

09:絕不在構造和析構過程中調用virtual函數

         1:不要再構造函數和析構函數中,調用virtual函數。

2:在base class構造期間,如果構造函數中調用了virtual函數,即使目前正在構造derived class(構造派生類對象時,需要首先構造其基類部分),virtual函數也是base class中的版本。也就是說;在base class構造期間,virtual函數不是virtual函數。

在derived class對象的base class構造期間,對象的類型是base class而不是derived class。不隻virtual函數會被編譯器解析至(resolve to)base class,若使用運作期類型資訊(runtime type information,例如dynamic_cast和typeid),也會把對象視為base class類型。

3:相同道理也适用于析構函數。一旦derived class析構函數開始執行,對象内的derived class成員變量便呈現未定義值,是以C++視它們仿佛不再存在。進入base class析構函數後對象就成為一個base class對象。

4:确定你的構造函數和析構函數都沒有調用virtual函數,而它們調用的所有函數也都要服從這一限制。

10:令operator=傳回一個reference to *this

         1:指派時,可以将其寫成連鎖形式:x = y = z = 15;指派采用右結合律,是以這個表達式等價于:x = ( y = ( z = 15 ) );

         2:為了實作“連鎖指派”,指派操作符必須傳回一個reference指向操作符的左側實參。這是你為classes實作指派操作符時應該遵循的協定:

Widget& operator=(const Widget& rhs)   // return type is a reference to
{                                      // the current class
  ...
  return *this;                        // return the left-hand object
}      

          3:這個協定不僅适用于标準的指派形式,也适用于所有指派相關運算,比如+=。

11:在operator=中處理“自我指派”

         1:“自我指派”發生在對象被指派給自己時,不要認定客戶絕不會那麼做,而且自我指派并不總是那麼可被一眼辨識出來,例如:a[i] = a[j];這條語句中,如果i和j相同,這便是自我指派;再比如:*px = *py;如果px和py恰巧指向相同,這也是自我指派。

         2:“自我指派”時,可能會掉進“在停止使用資源之前意外釋放了它”的陷阱。比如:

Widget& Widget::operator=(const Widget& rhs)
{
  delete pb;    
  pb = new Bitmap(*rhs.pb); 
  return *this;  
}      

 這裡的自我指派問題是,operator=函數内的*this和rhs有可能是同一個對象。果真如此delete就不隻是銷毀目前對象的bitmap,它也銷毀rhs的bitmap。

欲阻止這種錯誤,傳統做法是藉由operator=最前面的一個“證同測試(identity test )”達到“自我指派”的檢驗目的:

Widget& Widget::operator=(const Widget& rhs)
{
  if (this == &rhs) return *this;   // identity test: if a self-assignment,
                                    // do nothing
  delete pb;
  pb = new Bitmap(*rhs.pb);
  return *this;
}      

3:這個新版本仍然存在異常方面的麻煩。更明确地說,如果”new Bitmap”導緻異常(不論是因為配置設定時記憶體不足或因為Bitmap的copy構造函數抛出異常),Widget最終會持有一個指針指向一塊被删除的Bitmap。

令人高興的是,讓operator=具備“異常安全性”往往自動獲得“自我指派安全”的回報。是以愈來愈多人對“自我指派”的處理态度是傾向不去管它,把焦點放在實作“異常安全性”上。例如,我們隻需注意在複制pb所指東西之前别删除pb:

Widget& Widget::operator=(const Widget& rhs)
{
  Bitmap *pOrig = pb;               // remember original pb
  pb = new Bitmap(*rhs.pb);       // make pb point to a copy of *pb
  delete pOrig;                     // delete the original pb
  return *this;
}      

 現在,如果”new Bitmap”抛出異常,pb保持原狀。即使沒有證同測試,這段代碼還是能夠處理自我指派,因為我們對原bitmap做了一份複件、删除原bitmap、然後指向新制造的那個複件。它或許不是處理“自我指派”的最高效辦法,但它行得通。

         4:在operator=函數内確定代碼不但“異常安全”而且“自我指派安全”的一個替代方案是,使用所謂的copy and swap技術。

它是一個常見而夠好的operator=撰寫辦法:

Widget& Widget::operator=(const Widget& rhs)
{
  Widget temp(rhs);             // make a copy of rhs's data

  swap(temp);                   // swap *this's data with the copy's
  return *this;
}      

 或者,可能更常見的是下面這種寫法:

Widget& Widget::operator=(Widget rhs)
{
  swap(rhs);
  return *this;
}      

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

1:如果自己寫複制構造函數或指派操作符而不使用編譯器的版本,則需要注意的是:如果你為class添加一個成員變量,你必須同時修改複制構造函數和指派操作符函數(你也需要修改class的所有構造函數,以及任何非标準形式的operator=(比如+=))。如果你忘記,編譯器不太可能提醒你。

2:任何時候隻要你承擔起“為derived class撰寫copying函數”的重責大任,必須很小心地也複制其base class成分。那些成分往往是private,是以你應該讓derived class的copying函數調用相應的base class函數。

轉載于:https://www.cnblogs.com/gqtcgq/p/7573115.html