幾乎每一個你自己寫的類都會有一個或多個構(gòu)造函數(shù),一個析構(gòu)函數(shù)和一個拷貝賦值運(yùn)算符。不要驚奇,那是些就像你的面包黃油一樣的函數(shù),他們控制著基本的操作,如創(chuàng)建一個新的對象并確保已被初始化,消除一個函數(shù)并確保它被完全清除,以及為對象賦予一個新值。這些函數(shù)中出現(xiàn)錯誤,將引起你的類出現(xiàn)影響深遠(yuǎn)的,而且令人不快的反彈,所以保證他們正確是生死攸關(guān)的事情。本章中,我將對如何組裝這些函數(shù)以成為一個好的類的中樞骨干提供一些指導(dǎo)。
什么時候一個空的類將變得不空?答案是當(dāng) C++ 得到了它。如果你自己不聲明一個拷貝構(gòu)造函數(shù),一個拷貝賦值運(yùn)算符和一個析構(gòu)函數(shù),編譯器就會為這些東西聲明一個它自己的版本。而且,如果你自己連一個構(gòu)造函數(shù)都沒有聲明,編譯器就會為你聲明一個缺省構(gòu)造函數(shù)。所有這些函數(shù)都被聲明為 public 和 inline(參見 Item 30)。作為結(jié)果,如果你寫
class Empty{};
在本質(zhì)上和你如下寫是一樣的:
class Empty {
public:
Empty() { ... } // default constructor
Empty(const Empty& rhs) { ... } // copy constructor
~Empty() { ... } // destructor - see below
// for whether it’s virtual
Empty& operator=(const Empty& rhs) { ... } // copy assignment operator
};
這些函數(shù)只有在它們被需要的時候才會生成,但是并不需要做太多的事情,就會用到它們。下面的代碼會促使每一個函數(shù)生成:
Empty e1; // default constructor;
// destructor
Empty e2(e1); // copy constructor
e2 = e1; // copy assignment operator
假設(shè)編譯器為你寫成了這些函數(shù),那么它們做些什么呢?缺省構(gòu)造函數(shù)和析構(gòu)函數(shù)主要是給編譯器一個地方放置“幕后的”諸如調(diào)用基類和 non-static 數(shù)據(jù)成員的構(gòu)造函數(shù)和析構(gòu)函數(shù)的代碼。注意,生成的析構(gòu)函數(shù)是非虛擬(non-virtual)的,除非它從一個基類繼承而來,而基類聲明了一個虛析構(gòu)函數(shù)(這種情況下,函數(shù)的虛擬性來自基類)。
編譯器版本的拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符,只是簡單地從原對象拷貝每一個 non-static 數(shù)據(jù)成員到目標(biāo)對象。例如,考慮一個將名字和類型為 T 的對象聯(lián)系起來的 NamedObject 模板:
template
class NamedObject {
public:
NamedObject(const char *name, const T& value);
NamedObject(const std::string& name, const T& value);
..
private:
std::string nameValue;
T objectValue;
};
因?yàn)?NamedObject 中聲明了構(gòu)造函數(shù),編譯器就不會再生成缺省構(gòu)造函數(shù)。這一點(diǎn)非常重要,它意味著如果你足夠謹(jǐn)慎地設(shè)計(jì)你的類,使它需要構(gòu)造函數(shù)參數(shù),你就不必顧慮編譯器會不顧你的決定,輕率地增加一個不需要參數(shù)的構(gòu)造函數(shù)。
NamedObject 既沒有聲明拷貝構(gòu)造函數(shù)也沒有聲明拷貝賦值運(yùn)算符,所以編譯器將生成這些函數(shù)(當(dāng)然是在需要的時候)???,這就是拷貝構(gòu)造函數(shù)的用法:
NamedObject no1("Smallest Prime Number", 2);
NamedObject no2(no1); // calls copy constructor
編譯器生成的拷貝構(gòu)造函數(shù)一定會用 no1.nameValue 和 no1.objectValue 分別初始化 no2.nameValue 和 no2.objectValue。nameValue 的類型是 string,標(biāo)準(zhǔn) string 類型有一個拷貝構(gòu)造函數(shù),所以將以 no1.nameValue 作為參數(shù)調(diào)用 string 的拷貝構(gòu)造函數(shù)初始化 no2.nameValue。而另一方面,NamedObject::objectValue 的類型是 int(因?yàn)樵谶@個模板的實(shí)例化中 T 是 int),而 int 是內(nèi)建類型,所以 no2.objectValue 將通過拷貝no1.objectValue 的每一個位來初始化。
編譯器為 NamedObject 生成的拷貝賦值運(yùn)算符本質(zhì)上也會有同樣的行為,但是,通常情況下,只有在結(jié)果代碼合法而且有一個合理的可理解的邏輯時,編譯器生成的拷貝賦值運(yùn)算符才會有我所描述的行為方式。如果這兩項(xiàng)檢測中的任一項(xiàng)失敗了,編譯器將拒絕為你的類生成一個 operator=。
例如,假設(shè) NamedObject 如下定義,nameValue 是一個 string 的引用,而 objectValue 是一個 const T:
template
class NamedObject {
public:
// this ctor no longer takes a const name, because nameValue
// is now a reference-to-non-const string. The char* constructor
// is gone, because we must have a string to refer to.
NamedObject(std::string& name, const T& value);
... // as above, assume no
// operator= is declared
private:
std::string& nameValue; // this is now a reference
const T objectValue; // this is now const
};
現(xiàn)在,考慮這里會發(fā)生什么:
std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject p(newDog, 2); // when I originally wrote this, our
// dog Persephone was about to
// have her second birthday
NamedObject s(oldDog, 36); // the family dog Satch (from my
// childhood) would be 36 if she
// were still alive
p = s; // what should happen to
// the data members in p?
賦值之前,p.nameValue 和 s.nameValue 都引向 string 對象,但并非同一個。那個賦值對 p.nameValue 產(chǎn)生了什么影響呢?賦值之后,p.nameValue 所引向的字符串是否就是 s.nameValue 所引向的那一個呢,也就是說,引用本身被改變了?如果是這樣,就違反了常規(guī),因?yàn)?C++ 并沒有提供使一個引用引向另一個對象的方法。換一種思路,是不是 p.nameValue 所引向的那個 string 對象被改變了,從而保持指針或引用還是指向那個對象,也就是說,賦值并沒有直接影響對象?這是編譯器產(chǎn)生的拷貝賦值運(yùn)算符應(yīng)該做的事情嗎?
面對這個難題,C++ 拒絕編譯器產(chǎn)生代碼。如果你希望一個包含引用成員的類支持賦值,你必須自己定義拷貝賦值運(yùn)算符。面對含有 const 成員的類時,編譯器也會如此行事(就象上面那個改變后的類中的 objectValue)。改變 const 成員是不合法的,所以編譯器隱式產(chǎn)生的賦值函數(shù)無法確定該如何對待它們。最后,如果基類將拷貝賦值運(yùn)算符聲明為 private,編譯器拒絕為其派生類產(chǎn)生隱式的拷貝賦值運(yùn)算符。畢竟,編譯器為派生類產(chǎn)生的拷貝賦值運(yùn)算符也要處理其基類部分,但如果這樣做,它們當(dāng)然無法調(diào)用那些派生類無權(quán)調(diào)用的成員函數(shù)。
Things to Remember
編譯器可以隱式產(chǎn)生一個類的缺省構(gòu)造函數(shù),拷貝構(gòu)造函數(shù),拷貝賦值運(yùn)算符和析構(gòu)函數(shù)。
什么時候一個空的類將變得不空?答案是當(dāng) C++ 得到了它。如果你自己不聲明一個拷貝構(gòu)造函數(shù),一個拷貝賦值運(yùn)算符和一個析構(gòu)函數(shù),編譯器就會為這些東西聲明一個它自己的版本。而且,如果你自己連一個構(gòu)造函數(shù)都沒有聲明,編譯器就會為你聲明一個缺省構(gòu)造函數(shù)。所有這些函數(shù)都被聲明為 public 和 inline(參見 Item 30)。作為結(jié)果,如果你寫
class Empty{};
在本質(zhì)上和你如下寫是一樣的:
class Empty {
public:
Empty() { ... } // default constructor
Empty(const Empty& rhs) { ... } // copy constructor
~Empty() { ... } // destructor - see below
// for whether it’s virtual
Empty& operator=(const Empty& rhs) { ... } // copy assignment operator
};
這些函數(shù)只有在它們被需要的時候才會生成,但是并不需要做太多的事情,就會用到它們。下面的代碼會促使每一個函數(shù)生成:
Empty e1; // default constructor;
// destructor
Empty e2(e1); // copy constructor
e2 = e1; // copy assignment operator
假設(shè)編譯器為你寫成了這些函數(shù),那么它們做些什么呢?缺省構(gòu)造函數(shù)和析構(gòu)函數(shù)主要是給編譯器一個地方放置“幕后的”諸如調(diào)用基類和 non-static 數(shù)據(jù)成員的構(gòu)造函數(shù)和析構(gòu)函數(shù)的代碼。注意,生成的析構(gòu)函數(shù)是非虛擬(non-virtual)的,除非它從一個基類繼承而來,而基類聲明了一個虛析構(gòu)函數(shù)(這種情況下,函數(shù)的虛擬性來自基類)。
編譯器版本的拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符,只是簡單地從原對象拷貝每一個 non-static 數(shù)據(jù)成員到目標(biāo)對象。例如,考慮一個將名字和類型為 T 的對象聯(lián)系起來的 NamedObject 模板:
template
class NamedObject {
public:
NamedObject(const char *name, const T& value);
NamedObject(const std::string& name, const T& value);
..
private:
std::string nameValue;
T objectValue;
};
因?yàn)?NamedObject 中聲明了構(gòu)造函數(shù),編譯器就不會再生成缺省構(gòu)造函數(shù)。這一點(diǎn)非常重要,它意味著如果你足夠謹(jǐn)慎地設(shè)計(jì)你的類,使它需要構(gòu)造函數(shù)參數(shù),你就不必顧慮編譯器會不顧你的決定,輕率地增加一個不需要參數(shù)的構(gòu)造函數(shù)。
NamedObject 既沒有聲明拷貝構(gòu)造函數(shù)也沒有聲明拷貝賦值運(yùn)算符,所以編譯器將生成這些函數(shù)(當(dāng)然是在需要的時候)???,這就是拷貝構(gòu)造函數(shù)的用法:
NamedObject
NamedObject
編譯器生成的拷貝構(gòu)造函數(shù)一定會用 no1.nameValue 和 no1.objectValue 分別初始化 no2.nameValue 和 no2.objectValue。nameValue 的類型是 string,標(biāo)準(zhǔn) string 類型有一個拷貝構(gòu)造函數(shù),所以將以 no1.nameValue 作為參數(shù)調(diào)用 string 的拷貝構(gòu)造函數(shù)初始化 no2.nameValue。而另一方面,NamedObject
編譯器為 NamedObject
例如,假設(shè) NamedObject 如下定義,nameValue 是一個 string 的引用,而 objectValue 是一個 const T:
template
class NamedObject {
public:
// this ctor no longer takes a const name, because nameValue
// is now a reference-to-non-const string. The char* constructor
// is gone, because we must have a string to refer to.
NamedObject(std::string& name, const T& value);
... // as above, assume no
// operator= is declared
private:
std::string& nameValue; // this is now a reference
const T objectValue; // this is now const
};
現(xiàn)在,考慮這里會發(fā)生什么:
std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject
// dog Persephone was about to
// have her second birthday
NamedObject
// childhood) would be 36 if she
// were still alive
p = s; // what should happen to
// the data members in p?
賦值之前,p.nameValue 和 s.nameValue 都引向 string 對象,但并非同一個。那個賦值對 p.nameValue 產(chǎn)生了什么影響呢?賦值之后,p.nameValue 所引向的字符串是否就是 s.nameValue 所引向的那一個呢,也就是說,引用本身被改變了?如果是這樣,就違反了常規(guī),因?yàn)?C++ 并沒有提供使一個引用引向另一個對象的方法。換一種思路,是不是 p.nameValue 所引向的那個 string 對象被改變了,從而保持指針或引用還是指向那個對象,也就是說,賦值并沒有直接影響對象?這是編譯器產(chǎn)生的拷貝賦值運(yùn)算符應(yīng)該做的事情嗎?
面對這個難題,C++ 拒絕編譯器產(chǎn)生代碼。如果你希望一個包含引用成員的類支持賦值,你必須自己定義拷貝賦值運(yùn)算符。面對含有 const 成員的類時,編譯器也會如此行事(就象上面那個改變后的類中的 objectValue)。改變 const 成員是不合法的,所以編譯器隱式產(chǎn)生的賦值函數(shù)無法確定該如何對待它們。最后,如果基類將拷貝賦值運(yùn)算符聲明為 private,編譯器拒絕為其派生類產(chǎn)生隱式的拷貝賦值運(yùn)算符。畢竟,編譯器為派生類產(chǎn)生的拷貝賦值運(yùn)算符也要處理其基類部分,但如果這樣做,它們當(dāng)然無法調(diào)用那些派生類無權(quán)調(diào)用的成員函數(shù)。
Things to Remember
編譯器可以隱式產(chǎn)生一個類的缺省構(gòu)造函數(shù),拷貝構(gòu)造函數(shù),拷貝賦值運(yùn)算符和析構(gòu)函數(shù)。

