天天看點

讀書筆記 effective c++ Item 5 了解c++預設生成并調用的函數

1 編譯器會預設生成哪些函數 

什麼時候空類不再是一個空類?答案是用c++處理的空類。如果你自己不聲明,編譯器會為你聲明它們自己版本的拷貝構造函數,拷貝指派運算符和析構函數,如果你一個構造函數都沒有聲明,編譯器同樣會為你聲明一個預設拷貝構造函數。這些所有的函數會是public和inline的(Item30)。是以,如果你寫了下面的類:

1 class Empty{};      

本質上來說和下面的類是一樣的:

1 class Empty {
 2 
 3 public:
 4 
 5 Empty() { ... } // default constructor
 6 
 7 Empty(const Empty& rhs) { ... } // copy constructor
 8 
 9 ~Empty() { ... } // destructor — see below
10 
11 // for whether it’s virtual
12 
13 Empty& operator=(const Empty& rhs) { ... } // copy assignment operator
14 
15 };      

 這些函數隻有在被需要的時候才會生成。程式中需要這些函數是很平常的事。下面的代碼會導緻相應的函數被生成:

1 Empty e1; // default constructor; destructor
2 
3 Empty e2(e1); // copy constructor
4 
5 e2 = e1; // copy assignment operator      

2 預設生成的函數會做些什麼?

考慮到編譯器會為你生成函數,這些函數會做些什麼?預設拷貝構造函數和析構函數會給編譯器騰出一個地方用來放“藏在幕後的代碼”,像基類和非靜态資料成員的構造函數和析構函數的調用。注意生成的預設析構函數不是虛函數(

Item7

),除非生成預設析構函數的類繼承自一個聲明了虛析構函數的基類(這樣預設析構函數的虛或者非虛繼承自基類)。

2.1 預設拷貝構造函數

對于拷貝構造函數和拷貝指派運算符來說,編譯器生成的版本隻是将源對象的非靜态資料成員簡單的拷貝到目标對象上。例如,考慮一個NamedObject 模闆類允許你同類型T的對象進行關聯。

1 template<typename T>
 2 
 3 class NamedObject {
 4 
 5 public:
 6 
 7 NamedObject(const char *name, const T& value);
 8 
 9 NamedObject(const std::string& name, const T& value);
10 
11 ...
12 
13 private:
14 
15 std::string nameValue;
16 
17 T objectValue;
18 
19 };      

因為在NamedObject中聲明了一個構造函數,編譯器不再為其生成預設構造函數。這很重要,如果你仔細的設計了一個需要帶參構造函數的類,你不必擔心編譯器會添加一個不帶參數的構造函數來覆寫你的版本。

NamedObject既沒有聲明拷貝構造函數也沒有聲明拷貝指派運算符,是以編譯器會産生這些函數(如果它們被需要)。看下面的例子,拷貝構造函數這麼使用:

1 NamedObject<int> no1("Smallest Prime Number", 2);
2 
3 NamedObject<int> no2(no1); // calls copy constructor      

編譯器生成的拷貝構造函數必須分别用no1.nameValue和no1.objectValue初始化no2.nameValue和no2.objectValue。nameValue的類型是string,标準的string類型有個拷貝構造函數,于是no2.nameValue會通過調用string的拷貝構造函數來進行初始化,構造函數用no1.nameValue作為參數。另外,NamedObject<int>::objectValue的類型是int,于是no2.objectValue會通過拷貝no1.objectValue的bits來進行初始化。

2.2 預設拷貝指派運算符

編譯器為NamedObject<int>生成的拷貝指派運算符的行為同拷貝構造函數基本上是一樣的。但是一般來說,編譯器生成的拷貝指派運算符隻有在生成的代碼合法,并且有合理的機會證明其有意義,行為同拷貝構造函數才是一樣的。如果不滿上述任何一個條件,編譯器都會拒絕為你的類生成operator=。

舉個例子:考慮NamedObject像下面這樣定義,nameValue是指向string的引用,objectValue是const T.

1 template<typename T>
 2 
 3 class NamedObject {
 4 
 5 public:
 6 
 7 // this ctor no longer takes a const name, because nameValue
 8 
 9 // is now a reference-to-non-const string. The char* constructor
10 
11 // is gone, because we must have a string to refer to.
12 
13 NamedObject(std::string& name, const T& value);
14 
15 ... // as above, assume no
16 
17 // operator= is declared
18 
19 private:
20 
21 std::string& nameValue; // this is now a reference
22 
23 const T objectValue; // this is now const
24 
25 };      

 考慮下面會發生什麼:

1 std::string newDog("Persephone");
 2 
 3 std::string oldDog("Satch");
 4 
 5 NamedObject<int> p(newDog, 2); // when I originally wrote this, our
 6 
 7                                  // dog Persephone was about to
 8 
 9                                  // have her second birthday
10 
11 NamedObject<int> s(oldDog, 36); // the family dog Satch (from my
12 
13                                 // childhood) would be 36 if she
14 
15                                 // were still alive
16 
17 p = s; // what should happen to the data members in p?      

在指派之前,p.nameValue和s.nameValue都會指向string對象,雖然不是同一個。指派如何影響p.nameValue?在指派之後,p.nameValue應該指向s.nameValue所指向的那個string麼?也就是,引用本身會改變麼?如果是這樣,就開辟了一塊新天地,因為c++沒有提供使引用指向另一個對象的方法。或者,p.nameValue指向的string對象應該被修改?這樣就會影響其他對象,這些對象持有指向這個被修改的string的指針或者引用。這是編譯器生成的拷貝指派運算符應該做得?

面對這個難題,c++拒絕編譯代碼。如果你想在一個類中支援含有引用成員的拷貝指派運算符,你必須自己定義一個拷貝指派運算符。對于包含const成員的類,編譯器的行為也是類似的。修改const成員是不合法的,是以當一個預設拷貝指派運算符生成時,編譯器對如何處理它們是不确定的。最後,如果在基類中将拷貝指派運算符聲明成private,編譯器拒絕在派生類中生成拷貝指派運算符。畢竟編譯器為派生類生成的拷貝指派運算符需要能夠處理基類的部分,在這種情況下,它們當然不能夠觸發派生類沒有權限調用的函數。

作者:

HarlanC

部落格位址:

http://www.cnblogs.com/harlanc/

個人部落格:

http://www.harlancn.me/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出,

原文連結

如果覺的部落客寫的可以,收到您的贊會是很大的動力,如果您覺的不好,您可以投反對票,但麻煩您留言寫下問題在哪裡,這樣才能共同進步。謝謝!

繼續閱讀