2 構(gòu)造/析構(gòu)/賦值運(yùn)算
條款05:了解 C++ 默默編寫并調(diào)用哪些函數(shù)
如果自己沒聲明,編譯器就會(huì)為類聲明(編譯器版本的)一個(gè)copy構(gòu)造函數(shù),一個(gè)copy assignment操作符和一個(gè)析構(gòu)函數(shù)财破。此外如果沒有聲明任何構(gòu)造函數(shù),編譯器也會(huì)聲明一個(gè)default構(gòu)造函數(shù)赁还。所有這些函數(shù)都是public且inline的沼溜。
惟有當(dāng)這些函數(shù)被需要(被調(diào)用),它們才會(huì)被編譯器創(chuàng)建出來骗灶。即有需求惨恭,編譯器才會(huì)創(chuàng)建它們。
default構(gòu)造函數(shù)和析構(gòu)函數(shù)主要是給編譯器一個(gè)地方用來放置“藏身幕后”的代碼耙旦,像是調(diào)用base classes和non-static成員變量的構(gòu)造函數(shù)和析構(gòu)函數(shù)脱羡。注意:編譯器產(chǎn)生的析構(gòu)函數(shù)是個(gè)non-virtual,除非這個(gè)類的基類自身聲明有virtual析構(gòu)函數(shù)免都。
至于拷貝構(gòu)造函數(shù)和拷貝賦值操作符锉罐,編譯器創(chuàng)建的版本只是單純地將來源對(duì)象的每一個(gè)非靜態(tài)成員變量拷貝到目標(biāo)對(duì)象。
如果一個(gè)類聲明了一個(gè)構(gòu)造函數(shù)(無論有沒參數(shù))绕娘,編譯器就不再為它創(chuàng)建默認(rèn)構(gòu)造函數(shù)脓规。
編譯器生成的copy assignment操作符:對(duì)于成員變量中有指針,引用险领,常量類型侨舆,我們都應(yīng)考慮建立自己“合適”的拷貝賦值操作符。因?yàn)橹赶蛲瑝K內(nèi)存的指針是個(gè)潛在危險(xiǎn)绢陌,引用不可改變挨下,常量不可改變。
** note: **
編譯器可以暗自為類創(chuàng)建default構(gòu)造函數(shù)脐湾、copy構(gòu)造函數(shù)臭笆、copy assignment操作符,以及析構(gòu)函數(shù)沥割。
條款06:若不想使用編譯器自動(dòng)生成的函數(shù)耗啦,就該明確拒絕
通常如果你不希望類支持某一特定技能,只要不說明對(duì)應(yīng)函數(shù)就是了机杜。但這個(gè)策略對(duì)拷貝構(gòu)造函數(shù)和拷貝賦值操作符卻不起作用帜讲。因?yàn)榫幾g器會(huì)“自作多情”的聲明它們,并在需要的時(shí)候調(diào)用它們椒拗。
由于編譯器產(chǎn)生的函數(shù)都是public類型似将,因此可以將拷貝構(gòu)造函數(shù)或拷貝賦值操作符聲明為private获黔。通過這個(gè)小“伎倆”可以阻止人們?cè)谕獠空{(diào)用它,但是類中的成員函數(shù)和友元函數(shù)還是可以調(diào)用private函數(shù)在验。解決方法可能是在一個(gè)專門為了阻止拷貝動(dòng)作而設(shè)計(jì)的基類玷氏。(Boost提供的那個(gè)類名為noncopyable)。
不允許用戶進(jìn)行對(duì)象的拷貝腋舌。一般編譯器會(huì)提供默認(rèn)拷貝盏触,可將相應(yīng)的成員函數(shù)聲明為private。但這時(shí)有個(gè)問題块饺,member(成員)函數(shù)和friend(友元)函數(shù)仍然可以調(diào)用赞辩。
在不想實(shí)現(xiàn)的函數(shù)中不寫函數(shù)參數(shù)的名稱。當(dāng)客戶企圖copy授艰,編譯器會(huì)阻撓他辨嗽。如果不慎在member或friend函數(shù)內(nèi)copy,連接器會(huì)出問題淮腾。
class HomeForSale{
public:
...
private:
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&);
};
- 將連接器錯(cuò)誤移至編譯期糟需,更早地發(fā)現(xiàn)錯(cuò)誤往往更好。定義一個(gè)Uncopyable的基類谷朝,其它類繼承該類洲押,當(dāng)執(zhí)行拷貝時(shí),要調(diào)用基類拷貝構(gòu)造函數(shù)圆凰,就會(huì)出現(xiàn)問題诅诱。
class Uncopyable{
protected:
Uncopyable();
~Uncopyable();
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
// 繼承Uncopyable
class HomeForSale: private Uncopyable{
... ...
}
** note: **
為駁回編譯器自動(dòng)(暗自)提供的機(jī)能,可將相應(yīng)的成員函數(shù)聲明為private并且不予實(shí)現(xiàn)送朱。使用像Uncopyable這樣的基類也是一種做法。
條款07:為多態(tài)基類聲明 virtual 析構(gòu)函數(shù)
當(dāng)基類的指針指向派生類的對(duì)象的時(shí)候干旁,當(dāng)我們使用完驶沼,對(duì)其調(diào)用delete的時(shí)候,其結(jié)果將是未有定義——基類成分通常會(huì)被銷毀争群,而派生類的成分可能還留在堆里回怜。這會(huì)形成資源泄漏,敗壞之?dāng)?shù)據(jù)結(jié)構(gòu)换薄,在調(diào)試器上消費(fèi)許多時(shí)間玉雾。
消除這個(gè)問題的做法很簡(jiǎn)單:給基類一個(gè)virtual析構(gòu)函數(shù)。此后刪除派生類對(duì)象就會(huì)銷毀整個(gè)對(duì)象轻要,包括所有derived class成分复旬。
任何類只要帶有virtual函數(shù)都幾乎確定應(yīng)該也有一個(gè)virtual析構(gòu)函數(shù)。
如果一個(gè)類不含virtual函數(shù)冲泥,通常表示它并不意圖被用做一個(gè)基類驹碍,當(dāng)類不企圖被當(dāng)做基類的時(shí)候壁涎,令其析構(gòu)函數(shù)為virtual往往是個(gè)餿主意。因?yàn)閷?shí)現(xiàn)virtual函數(shù)志秃,需要額外的開銷(指向虛函數(shù)表的指針vptr)怔球。 因此,只有當(dāng)class內(nèi)含至少一個(gè)virtual函數(shù)浮还,才為它聲明virtual析構(gòu)函數(shù)竟坛。
STL容器都不帶virtual析構(gòu)函數(shù),所以最好別派生它們钧舌。
某些classes的設(shè)計(jì)目的是作為基類使用担汤,但不是為了多態(tài)用途,如條款06的Uncopyable延刘,因此它們不需要virtual析構(gòu)函數(shù)漫试。
** note: **
- 帶有多態(tài)性質(zhì)的基類應(yīng)該聲明一個(gè)virtual析構(gòu)函數(shù)。如果一個(gè)類帶有任何virtual函數(shù)碘赖,它就應(yīng)該擁有一個(gè)virtual析構(gòu)函數(shù)驾荣。
- 一個(gè)類的設(shè)計(jì)目的不是作為基類使用,或不是為了具備多態(tài)性普泡,就不該聲明virtual析構(gòu)函數(shù)播掷。
條款08:別讓異常逃離析構(gòu)函數(shù)
C++并不禁止析構(gòu)函數(shù)吐出異常,但它不鼓勵(lì)你這樣做撼班。C++不喜歡析構(gòu)函數(shù)吐出異常歧匈。
class DBConnection{
public:
static DBConnection create();
void close();
};
class DBManager{
public:
~DBManager(){
db.close(); //析構(gòu)函數(shù)關(guān)閉數(shù)據(jù)庫連接
}
private:
DBConnection db;
};
//調(diào)用析構(gòu)函數(shù)時(shí)可能會(huì)發(fā)生異常
DBManager dbM(DBConnection::create());
如果析構(gòu)函數(shù)調(diào)用的函數(shù)拋出異常:
- 如果拋出異常,就結(jié)束程序砰嘁。強(qiáng)迫結(jié)束程序是個(gè)合理選項(xiàng)件炉,畢竟它可以阻止異常從析構(gòu)函數(shù)傳播出去(那會(huì)導(dǎo)致不明確的行為)。通常通過調(diào)用about完成矮湘。
DBManager::~DBManager(){
try { db.close(); }
catch(...){
// 可以記錄錯(cuò)誤后退出程序
std::abort();
}
}
- 吞下發(fā)生的異常斟冕。捕獲異常,但什么也不做缅阳。 雖然吞下異常是個(gè)壞主意磕蛇,但吞下異常也比負(fù)擔(dān)“草率結(jié)束程序”或“不明確行為帶來的風(fēng)險(xiǎn)”好。
DBManager::~DBManager(){
try { db.close(); }
catch(...){
//可以記錄錯(cuò)誤后退出程序
}
但是十办,兩者都無法對(duì)“導(dǎo)致close拋出異承闫玻”的情況做出反應(yīng)。即使析構(gòu)函數(shù)捕獲到異常向族,客戶也無法處理異常呵燕,客戶需要對(duì)某個(gè)函數(shù)運(yùn)行期間拋出的異常進(jìn)行反應(yīng)。因此炸枣,class應(yīng)該提供一個(gè)普通函數(shù)來執(zhí)行該操作虏等。
class DBManager(){
public:
void close(){
db.close();
closed = true;
}
~DBManager(){
if (!closed){
try { db.close(); }
catch(...) {
//錯(cuò)誤日志...
}
}
}
private:
bool closed;
DBConnection db;
};
這里面加了一個(gè)close函數(shù)弄唧,客戶可以自己調(diào)用close函數(shù),當(dāng)發(fā)生異常時(shí)霍衫,進(jìn)行異常處理候引,析構(gòu)函數(shù)再重新關(guān)閉一次(如果這次仍然異常,就和上面的結(jié)果一樣了)敦跌。如果客戶沒有調(diào)用close函數(shù)澄干,則可以在析構(gòu)函數(shù)中自動(dòng)調(diào)用。所以柠傍,在寫程序時(shí)麸俘,一定要將會(huì)發(fā)生異常的函數(shù)作為一個(gè)普通函數(shù),這樣可以提供更多的選擇惧笛。
如果某個(gè)操作可能在失敗時(shí)拋出異常从媚,而又存在某種需要必須處理該異常,那么這個(gè)異常必須來自析構(gòu)函數(shù)以外的某個(gè)函數(shù)患整。
** note: **
- 析構(gòu)函數(shù)絕對(duì)不要吐出異常拜效。如果一個(gè)被析構(gòu)函數(shù)調(diào)用的函數(shù)可能拋出異常,析構(gòu)函數(shù)應(yīng)該捕捉任何異常各谚,然后吞下它們(不傳播)或結(jié)束程序紧憾。
- 如果客戶需要對(duì)某個(gè)操作函數(shù)運(yùn)行期間拋出的異常做出反應(yīng),那么類應(yīng)該提供一個(gè)普通函數(shù)(而非在析構(gòu)函數(shù)中)執(zhí)行該操作昌渤。