1. 讓自己習(xí)慣C++
條款01:視C++為一個(gè)語(yǔ)言聯(lián)邦
為了更好的理解C++,我們將C++分解為四個(gè)主要次語(yǔ)言:
- C。說(shuō)到底C++仍是以C為基礎(chǔ)。區(qū)塊,語(yǔ)句,預(yù)處理器,內(nèi)置數(shù)據(jù)類型,數(shù)組,指針統(tǒng)統(tǒng)來(lái)自C。
- Object-Oreinted C++。這一部分是面向?qū)ο笤O(shè)計(jì)之古典守則在C++上的最直接實(shí)施。類,封裝,繼承,多態(tài),virtual函數(shù)等等...
- Template C++。這是C++泛型編程部分。
- STL。STL是個(gè)template程序庫(kù)。容器(containers),迭代器(iterators),算法(algorithms)以及函數(shù)對(duì)象(function objects)...
請(qǐng)記?。?/strong>這四個(gè)次語(yǔ)言,當(dāng)你從某個(gè)次語(yǔ)言切換到另一個(gè),導(dǎo)致高效編程守則要求你改變策略。C++高效編程守則視狀況而變化,取決于你使用C++的哪一部分。
條款02:盡量以const,enum,inline替換#define
這條可理解為**讓編譯器代替預(yù)處理器做更多的事情。
預(yù)處理過(guò)程掃描源代碼,對(duì)其進(jìn)行初步的轉(zhuǎn)換,產(chǎn)生新的源代碼提供給編譯器。檢查包含預(yù)處理指令的語(yǔ)句和宏定義,并對(duì)源代碼進(jìn)行相應(yīng)的轉(zhuǎn)換。預(yù)處理過(guò)程還會(huì)刪除程序中的注釋和多余的空白字符??梢?jiàn)預(yù)處理過(guò)程先于編譯器對(duì)源代碼進(jìn)行處理。預(yù)處理指令是以#號(hào)開(kāi)頭的代碼行。
例:
#define ASPECT_RATIO 1.653
記號(hào)名稱ASPECT_RATIO也許從未被編譯器看見(jiàn),也許在編譯器開(kāi)始處理源代碼之前它就被預(yù)處理器移走了。即編譯源代碼時(shí)ASPECT_RATIO已被1.653取代。ASPECT_RATIO可能并未進(jìn)入記號(hào)表(symbol table)。
替換:const double AspectRatio = 1.653;
好處應(yīng)該有:多了類型檢查,因?yàn)?define 只是單純的替換,而這種替換在目標(biāo)碼中可能出現(xiàn)多份1.653;改用常量絕不會(huì)出現(xiàn)相同情況。
請(qǐng)記住:對(duì)于單純常量,最好以const對(duì)象或enums替換#defines;對(duì)于形似函數(shù)的宏,最好改用inline函數(shù)替換#defines。
條款03:盡可能使用const
const的多才多藝:
- 在classes外部修飾global或namespace作用域中的常量,或修飾文件、函數(shù)、或被聲明為static的對(duì)象。
- 修飾classes內(nèi)部的static和non-static成員變量。
- 可以指出指針自身所指事物或兩者都(或都不)是const。
- 在一個(gè)函數(shù)聲明式內(nèi),const可以修飾函數(shù)返回值、各參數(shù)、函數(shù)自身(如果是成員函數(shù))
char greeting[] = "Hello";
char *p = greeting; //指針p及所指的字符串都可改變;
const char *p = greeting; //指針p本身可以改變,如p = &Anyother;p所指的字符串不可改變;
char * cosnt p = greeting; //指針p不可改變,所指對(duì)象可改變;
const char * const p = greeting; //指針p及所致對(duì)象都不可改變;
請(qǐng)記?。?/strong>如果關(guān)鍵字const出現(xiàn)在星號(hào)左邊,表示被指物事常量。const char *p和char const *p兩種寫法意義一樣,都說(shuō)明所致對(duì)象為常量;如果關(guān)鍵字const出現(xiàn)在星號(hào)右邊,表示指針自身是常量。
將const實(shí)施于成員函數(shù),是為了1)判斷哪個(gè)函數(shù)可以改動(dòng)對(duì)象內(nèi)容而哪個(gè)函數(shù)不行,2)使操作const對(duì)象稱為可能。
常量對(duì)象不能調(diào)用非常量成員函數(shù),只能調(diào)用常量成員函數(shù);常量成員函數(shù)不能改變對(duì)象的成員變量,除非該變量聲明為mutable。
請(qǐng)記?。?/strong>
- 將某些東西聲明為const可幫助編譯器偵測(cè)出錯(cuò)誤用法。const可被施加于任何作用域內(nèi)的對(duì)象、函數(shù)參數(shù)、函數(shù)返回類型、成員函數(shù)本體;
- 編譯器強(qiáng)制實(shí)施bitwise constness,但你編寫程序時(shí)應(yīng)該使用“概念上的車輛”(conceptual constness);
- 當(dāng)cosnt和non-const成員函數(shù)有著實(shí)質(zhì)等價(jià)的實(shí)現(xiàn)時(shí),令non-const版本調(diào)用const版本可避免代碼重復(fù)。
條款04:確定對(duì)象被使用前已先被初始化
永遠(yuǎn)在使用對(duì)象之前先將它初始化。對(duì)于無(wú)任何成員的內(nèi)置類型,你必須手工完成此事。至于內(nèi)置類型以外的任何其它東西,初始化責(zé)任落在構(gòu)造函數(shù)身上,確保每一個(gè)構(gòu)造函數(shù)都將對(duì)象的每一個(gè)成員初始化。
賦值和初始化:
C++規(guī)定,對(duì)象的成員變量的初始化動(dòng)作發(fā)生在進(jìn)入構(gòu)造函數(shù)本體之前。所以應(yīng)將成員變量的初始化置于構(gòu)造函數(shù)的初始化列表中。
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones)
{ theName = name; //這些都是賦值,而非初始化
theAddress = address; //這些成員變量在進(jìn)入函數(shù)體之前已調(diào)用默認(rèn)構(gòu)造函數(shù),接著又調(diào)用賦值函數(shù),
thePhones = phones; //即要經(jīng)過(guò)兩次的函數(shù)調(diào)用。
numTimesConsulted = 0;
}
ABEntry::ABEntry(const std::string& name, const std::string& address,
const std::list<PhoneNumber>& phones) :
theName(name), //這些才是初始化
theAddress(address), //這些成員變量只用相應(yīng)的值進(jìn)行拷貝構(gòu)造函數(shù),所以通常效率更高。
thePhones(phones),
numTimesConsulted(0)
{ }
所以,對(duì)于非內(nèi)置類型變量的初始化應(yīng)在初始化列表中完成,以提高效率。而對(duì)于內(nèi)置類型對(duì)象,其初始化和賦值的成本相同,但為了一致性最好也通過(guò)成員初始化表來(lái)初始化。如果成員變量時(shí)const或reference,它們就一定需要初值,不能被賦值。
C++有著十分固定的“成員初始化次序”?;惪偸窃谂缮愔氨怀跏蓟惖某蓡T變量總是以其說(shuō)明次序被初始化。所以:當(dāng)在成員初始化列表中列各成員時(shí),最好總是以其聲明次序?yàn)榇涡颉?/strong>
請(qǐng)記住:
- 為內(nèi)置對(duì)象進(jìn)行手工初始化,因?yàn)镃++不保證初始化它們;
- 構(gòu)造函數(shù)最好使用成員初始化列表,而不要在構(gòu)造函數(shù)本體內(nèi)使用賦值操作。初始化列表列出的成員變量,其排列次序應(yīng)該和它們?cè)陬愔械穆暶鞔涡蛳嗤?/li>
- 為免除“跨編譯單元之初始化次序”問(wèn)題,請(qǐng)以local static對(duì)象替換non-local static對(duì)象。
2. 構(gòu)造、析構(gòu)、賦值運(yùn)算
條款05:了解C++默默編寫并調(diào)用哪些函數(shù)
如果你在類聲明時(shí)沒(méi)有聲明拷貝構(gòu)造函數(shù)、拷貝賦值操作符、析構(gòu)函數(shù)、構(gòu)造函數(shù),編譯器會(huì)自動(dòng)為你提供一份聲明。惟有當(dāng)這些函數(shù)被需要(被調(diào)用),它們才會(huì)被編譯器創(chuàng)建出來(lái)。
默認(rèn)構(gòu)造函數(shù)和析構(gòu)函數(shù)主要是給編譯器一個(gè)地方用來(lái)放置“藏身幕后”的代碼,像是調(diào)用基類和非靜態(tài)成員變量的構(gòu)造函數(shù)和析構(gòu)函數(shù);至于拷貝構(gòu)造函數(shù)和拷貝賦值操作符,編譯器創(chuàng)建的版本只是單純地將來(lái)源對(duì)象的每一個(gè)非靜態(tài)成員變量拷貝到目標(biāo)對(duì)象。
請(qǐng)記?。?/strong>
- 編譯器可以暗自為類創(chuàng)建默認(rèn)構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù)、拷貝賦值操作符,以及析構(gòu)函數(shù)。
- 如一個(gè)類聲明了一個(gè)構(gòu)造函數(shù)(無(wú)論有沒(méi)參數(shù)),編譯器就不再為它創(chuàng)建默認(rèn)構(gòu)造函數(shù)。
- 編譯器生成的拷貝賦值操作符:對(duì)于成員變量中有指針,引用,常量類型,我們都應(yīng)考慮建立自己“合適”的拷貝賦值操作符。因?yàn)橹赶蛲瑝K內(nèi)存的指針是個(gè)潛在危險(xiǎn),引用不可改變,常量不可改變。
條款06:若不想使用編譯器自動(dòng)生成的函數(shù),就該明確拒絕
通常如果你不希望類支持某一特定技能,只要不說(shuō)明對(duì)應(yīng)函數(shù)就是了。但這個(gè)策略對(duì)拷貝構(gòu)造函數(shù)和拷貝賦值操作符卻不起作用。因?yàn)榫幾g器會(huì)“自作多情”的聲明它們,并在需要的時(shí)候調(diào)用它們。
由于編譯器產(chǎn)生的函數(shù)都是public類型,因此可以將拷貝構(gòu)造函數(shù)或拷貝賦值操作符聲明為private,來(lái)阻止人們?cè)谕獠空{(diào)用它;但是類中的成員函數(shù)和友元函數(shù)還是可以調(diào)用private函數(shù),解決方法可能是在一個(gè)專門為了阻止拷貝動(dòng)作而設(shè)計(jì)的基類。(Boost提供的那個(gè)類名為noncopyable)。
條款07:為多態(tài)基類聲明virtual析構(gòu)函數(shù)
當(dāng)基類的指針指向派生類的對(duì)象的時(shí)候,當(dāng)我們使用完,對(duì)其調(diào)用delete的時(shí)候,其結(jié)果將是未有定義——基類成分通常會(huì)被銷毀,而派生類的充分可能還留在堆里。這可是形成資源泄漏、敗壞之?dāng)?shù)據(jù)結(jié)構(gòu)、在調(diào)試器上消費(fèi)許多時(shí)間。
消除以上問(wèn)題的做法很簡(jiǎn)單:給基類一個(gè)virtual析構(gòu)函數(shù)。此后刪除派生類對(duì)象就會(huì)如你想要的那般。
任何類只要帶有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ù),需要額外的開(kāi)銷(指向虛函數(shù)表的指針vptr)。
條款08:別讓異常逃離析構(gòu)函數(shù)
C++并不禁止析構(gòu)函數(shù)吐出異常,但它不鼓勵(lì)你這樣做。C++不喜歡析構(gòu)函數(shù)吐出異常。
如果可能導(dǎo)致異常:
如果拋出異常,就結(jié)束程序。(強(qiáng)迫結(jié)束程序是個(gè)合理選項(xiàng),畢竟它可以阻止異常從析構(gòu)函數(shù)傳播出去。)
捕獲異常,但什么也不做。
如果某個(gè)操作可能在失敗時(shí)拋出異常,而又存在某種需要必須處理該異常,那么這個(gè)異常必須來(lái)自析構(gòu)函數(shù)以外的某個(gè)函數(shù)。
請(qǐng)記?。?/strong>
- 析構(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í)行該操作。
條款09:決不在構(gòu)造和析構(gòu)過(guò)程中調(diào)用virtual函數(shù)
你不該在構(gòu)造函數(shù)和析構(gòu)函數(shù)中調(diào)用virtual函數(shù),因?yàn)檫@樣的調(diào)用不會(huì)帶來(lái)你預(yù)想的結(jié)果。
基類的構(gòu)造函數(shù)的執(zhí)行要早于派生類的構(gòu)造函數(shù),當(dāng)基類的構(gòu)造函數(shù)執(zhí)行時(shí),派生類的成員變量尚未初始化。派生類的成員變量沒(méi)初始化,即為指向虛函數(shù)表的指針vptr沒(méi)被初始化又怎么去調(diào)用派生類的virtual函數(shù)呢?析構(gòu)函數(shù)也相同,派生類先于基類被析構(gòu),又如何去找派生類相應(yīng)的虛函數(shù)?
唯一好的做法是:確定你的構(gòu)造函數(shù)和析構(gòu)函數(shù)都沒(méi)有調(diào)用虛函數(shù),而它們調(diào)用的所有函數(shù)也不應(yīng)該調(diào)用虛函數(shù)。
解決的方法可能是:既然你無(wú)法使用虛函數(shù)從基類向下調(diào)用,那么我們可以使派生類將必要的構(gòu)造信息向上傳遞至基類構(gòu)造函數(shù)。即在派生類的構(gòu)造函數(shù)的成員初始化列表中顯示調(diào)用相應(yīng)基類構(gòu)造函數(shù),并傳入所需傳遞信息。
條款10:令operator= 返回一個(gè)reference to *this
對(duì)于賦值操作符,我們常常要達(dá)到這種類似效果,即連續(xù)賦值:
int x, y, z;
x = y = z = 15;
為了實(shí)現(xiàn)“連鎖賦值”,賦值操作符必須返回一個(gè)“引用”指向操作符的左側(cè)實(shí)參。
Widget & operator = (const Widget &rhs)
{
...
return *this;
}
所有內(nèi)置類型和標(biāo)準(zhǔn)程序庫(kù)提供的類型如string,vector,complex或即將提供的類型共同遵守。
條款11:在operator =中處理“自我賦值”
看下面的例子:
Widget& Widget::operator=(const Widget& rhs)
{
delete pb; //這里對(duì)pb指向內(nèi)存對(duì)象進(jìn)行delete,試想 *this == rhs?情況會(huì)如何
pb = new Bitmap(*rhs.pb); //如果*this == rhs,那么這里還能new嗎?“大事不妙”。
return *this;
}
也許以下代碼能解決以上問(wèn)題:
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs)
return *this; //解決了自我賦值的問(wèn)題。
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
“許多時(shí)候一群精心安排的語(yǔ)句就可以導(dǎo)出異常安全(以及自我賦值安全)的代碼”,以上代碼同樣存在異常安全問(wèn)題。如果new Bitmap導(dǎo)致異常,Widget最終會(huì)指向一塊被刪除的Bitmap。
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; //記住原先的pb
pb = new Bitmap(*rhs.pb); //令pb指向*pb的一個(gè)復(fù)本
delete pOrig; //刪除原先的pb
return *this; //這樣既解決了自我賦值,又解決了異常安全問(wèn)題。自我賦值,將pb所指對(duì)象換了個(gè)存儲(chǔ)地址。
}
條款12:復(fù)制對(duì)象時(shí)勿忘其每一個(gè)成員
如果你在類中添加一個(gè)成員變量,你必須同時(shí)修改相應(yīng)的copying函數(shù)(所有的構(gòu)造函數(shù),拷貝構(gòu)造函數(shù)以及拷貝賦值操作符)。在派生類的構(gòu)造函數(shù),拷貝構(gòu)造函數(shù)和拷貝賦值操作符中應(yīng)當(dāng)顯示調(diào)用基類相對(duì)應(yīng)的函數(shù),否則編譯器可能又“自作聰明了”。
當(dāng)你編寫一個(gè)copying函數(shù),請(qǐng)確保:
- 復(fù)制所有l(wèi)ocal成員變量;
- 調(diào)用所有基類內(nèi)的適當(dāng)copying函數(shù)。
但是,我們不該令拷貝賦值操作符調(diào)用拷貝構(gòu)造函數(shù),也不該令拷貝構(gòu)造函數(shù)調(diào)用拷貝賦值操作符。想想,一個(gè)是拷貝(建立對(duì)象),一個(gè)是賦值(對(duì)象已經(jīng)存在)
3. 資源管理
所謂資源就是,一旦用了它,將來(lái)必須還給系統(tǒng)。C++程序中最常使用的資源就好似動(dòng)態(tài)分配內(nèi)存(如果你new了,卻忘了delete,會(huì)導(dǎo)致內(nèi)存泄露),但內(nèi)存只是你必須管理的眾多資源之一。其它常見(jiàn)的有文件描述符(file descriptors)、互斥器(mutex)、圖形界面中的字形和畫刷。數(shù)據(jù)庫(kù)連接以及網(wǎng)絡(luò)sockets。當(dāng)你不使用它們時(shí),記得還給系統(tǒng)。
條款13:以對(duì)象管理資源
把資源放進(jìn)對(duì)象內(nèi),我們便可依賴C++的“析構(gòu)函數(shù)自動(dòng)調(diào)用機(jī)制”確保資源被釋放。
許多資源被動(dòng)態(tài)分配于堆內(nèi)而后被用于單一區(qū)塊或函數(shù)內(nèi)。它們應(yīng)該在控制流離開(kāi)那個(gè)區(qū)塊或函數(shù)時(shí)被釋放。標(biāo)準(zhǔn)程序庫(kù)提供的auto_ptr正是針對(duì)這種形勢(shì)而設(shè)計(jì)的特制產(chǎn)品。auto_ptr是個(gè)“類指針對(duì)象”,也就是所謂的“智能指針”,其析構(gòu)函數(shù)自動(dòng)對(duì)其所指對(duì)象調(diào)用delete。
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
...
} //函數(shù)退出,auto_ptr調(diào)用析構(gòu)函數(shù)自動(dòng)調(diào)用delete,刪除pInv;無(wú)需顯示調(diào)用delete。
“以對(duì)象管理資源”的兩個(gè)關(guān)鍵想法:
- 獲得資源后立刻放進(jìn)管理對(duì)象內(nèi)(如auto_ptr)。每一筆資源都在獲得的同時(shí)立刻被放進(jìn)管理對(duì)象中?!百Y源取得時(shí)機(jī)便是初始化時(shí)機(jī)”(Resource Acquisition Is Initialization;RAII)。
- 管理對(duì)象運(yùn)用析構(gòu)函數(shù)確保資源被釋放。即一旦對(duì)象被銷毀,其析構(gòu)函數(shù)被自動(dòng)調(diào)用來(lái)釋放資源。
由于auto_ptr被銷毀時(shí)會(huì)自動(dòng)刪除它所指之物,所以不能讓多個(gè)auto_ptr同時(shí)指向同一對(duì)象。所以auto_ptr若通過(guò)copying函數(shù)復(fù)制它們,它們會(huì)變成NULL,而復(fù)制所得的指針將取得資源的唯一擁有權(quán)!
std::auto_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
std::auto_ptr<Investment> pInv2(pInv1); //現(xiàn)在pInv2指向?qū)ο?,而pInv1被設(shè)為NULL;
pInv1 = pInv2; //現(xiàn)在pInv1指向?qū)ο螅鴓In2被設(shè)為NULL;
受auto_ptr管理的資源必須絕對(duì)沒(méi)有一個(gè)以上的auto_ptr同時(shí)指向它。即“有你沒(méi)我,有我沒(méi)你”。auto_ptr的替代方案是“引用計(jì)數(shù)型智能指針”(reference-counting smart pointer;SCSP)、它可以持續(xù)跟蹤共有多少對(duì)象指向某筆資源,并在無(wú)人指向它時(shí)自動(dòng)刪除該資源。
TR1的tr1::shared_ptr就是一個(gè)"引用計(jì)數(shù)型智能指針"。
void f()
{
...
std::tr1::shared_ptr<Investment> pInv1(createInvestment()); //pInv1指向createInvestment()返回物;
std::tr1::shared_ptr<Investment> pInv2(pInv1); //pInv1,pInv2指向同一個(gè)對(duì)象;
pInv1 = pInv2; //同上,無(wú)變化
...
} //函數(shù)退出,pInv1,pInv2被銷毀,它們所指的對(duì)象也竟被自動(dòng)釋放。
auto_ptr和tr1::shared_ptr都在其析構(gòu)函數(shù)內(nèi)做delete而不是delete[],也就意味著在動(dòng)態(tài)分配而得的數(shù)組身上使用auto_ptr或tr1::shared_ptr是個(gè)潛在危險(xiǎn),資源得不到釋放。也許boost::scoped_array和boost::shared_array能提供幫助。還有,vector和string幾乎總是可以取代動(dòng)態(tài)分配而得的數(shù)組。
請(qǐng)記住:
- 為防止資源泄漏,請(qǐng)使用RAII對(duì)象,它們?cè)跇?gòu)造函數(shù)中獲得資源并在析構(gòu)函數(shù)中釋放資源。
- 兩個(gè)常被使用的RAII類分別是auto_ptr和tr1::shared_ptr。后者通常是較佳選擇,因?yàn)槠淇截愋袨楸容^直觀。若選擇auto_ptr,復(fù)制動(dòng)作會(huì)使他(被復(fù)制物)指向NULL。
條款14:在資源管理類中小心拷貝行為
我們?cè)跅l款13中討論的資源表現(xiàn)在堆上申請(qǐng)的資源,而有些資源并不適合被auto_ptr和tr1::shared_ptr所管理??赡芪覀冃枰⒆约旱馁Y源管理類。
例:假設(shè)我們使用Mutex類型的互斥器對(duì)象,共有l(wèi)ock和unlock兩函數(shù)可用
void lock(Mutex *pm); //鎖定pm所指的互斥量
unlock(Mutex *pm); //將pm解除鎖定
我們建立的資源管理類可能會(huì)是這樣:
class Lock
{
public:
explicit Lock(Mutex *pm)
: mutexPtr(pm)
{
lock(mutexPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex *mutexPtr;
};
“當(dāng)一個(gè)RAII對(duì)象被復(fù)制,會(huì)發(fā)生什么事?”大多數(shù)時(shí)候你會(huì)選擇一下兩種可能:
- 禁止復(fù)制。如果復(fù)制動(dòng)作對(duì)RAII類并不合理,你便應(yīng)該禁止之。禁止復(fù)制類的copying函數(shù)參見(jiàn)條款6。
-
對(duì)底層資源使用”引用計(jì)數(shù)法“。有時(shí)候我們又希望保有資源,直到它的最后一個(gè)使用者被銷毀。這種情況下復(fù)制RAII對(duì)象時(shí),應(yīng)該將資源的”被引用計(jì)數(shù)“遞增。
tr1::shared_ptr便是如此。通常只要內(nèi)含一個(gè)tr1::shared_ptr成員變量,RAII類便可實(shí)現(xiàn)”引用計(jì)數(shù)“行為。
條款15:在資源管理類中提供對(duì)原始資源的訪問(wèn)
前幾個(gè)條款提到的資源管理類很棒。它們是你對(duì)抗資源泄漏的堡壘。但這個(gè)世界并不完美,許多APIs直接指涉資源,這時(shí)候我們需要直接訪問(wèn)原始資源。這時(shí)候需要一個(gè)函數(shù)可將RAII對(duì)象(如tr1::shared_ptr)轉(zhuǎn)換為其所內(nèi)含之原始資源。有兩種做法可以達(dá)成目標(biāo):顯示轉(zhuǎn)換和隱式轉(zhuǎn)換。
tr1::shared_ptr和auto_ptr都提供一個(gè)get成員函數(shù),用來(lái)執(zhí)行顯示轉(zhuǎn)換,也就是返回智能指針內(nèi)部的原始指針(的復(fù)件)。就像所有智能指針一樣, tr1::shared_ptr和auto_ptr也重載了指針取值操作符(operator->和operator),它們?cè)试S隱式轉(zhuǎn)換至底部原始指針。(即在對(duì)智能指針對(duì)象實(shí)施->和操作時(shí),實(shí)際被轉(zhuǎn)換為被封裝的資源的指針。)
class Font
{
public:
...
FontHandle get() const //FontHandle 是資源; 顯示轉(zhuǎn)換函數(shù)
{
return f;
}
operator FontHandle() const //隱式轉(zhuǎn)換 這個(gè)值得注意,可能引起“非故意之類型轉(zhuǎn)換”
{
return f;
}
...
};
是否該提供一個(gè)顯示轉(zhuǎn)換函數(shù)(例如get成員函數(shù))將RAII類轉(zhuǎn)換為其底部資源,或是應(yīng)該提供隱式轉(zhuǎn)換,答案主要取決于RAII類被設(shè)計(jì)執(zhí)行的特定工作,以及它被使用的情況。顯示轉(zhuǎn)換可能是比較受歡迎的路子,但是需要不停的get,get;而隱式轉(zhuǎn)換又可能引起“非故意之類型轉(zhuǎn)換”。
條款16:成對(duì)使用new和delete時(shí)要采取相同形式
當(dāng)我們使用new,有兩件事情發(fā)生:第一,內(nèi)存被分配出來(lái);第二,針對(duì)此內(nèi)存會(huì)有一個(gè)(或更多)構(gòu)造函數(shù)被調(diào)用。當(dāng)你使用delete,也有兩件事發(fā)生:針對(duì)此內(nèi)存會(huì)有一個(gè)(或多個(gè))析構(gòu)函數(shù)被調(diào)用,然后內(nèi)存才被釋放。delete的最大問(wèn)題在于:即將被刪除的內(nèi)存之內(nèi)究竟有多少對(duì)象?這個(gè)問(wèn)題的答案決定了有多少個(gè)析構(gòu)函數(shù)必須被調(diào)用起來(lái)。
解決以上問(wèn)題事實(shí)上很簡(jiǎn)單:如果你調(diào)用new時(shí)使用[],你必須在對(duì)應(yīng)調(diào)用delete時(shí)也使用[]。如果你調(diào)用new時(shí)沒(méi)有使用[],那么也不該在對(duì)應(yīng)調(diào)用delete時(shí)使用[]。
條款17:以獨(dú)立語(yǔ)句將newed對(duì)象置入智能指針
為了避免資源泄漏的危險(xiǎn),最好在單獨(dú)語(yǔ)句內(nèi)以智能指針存儲(chǔ)newed所得對(duì)象。
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
std::tr1::shared_ptr<Widget> pw(new Widget); //即在傳入函數(shù)之前對(duì)智能指針初始化,而不是在傳入?yún)?shù)中
//對(duì)其初始化,因?yàn)槟菢涌赡芤鸩僮餍蛄械膯?wèn)題。
processWidget(pw, priority());
請(qǐng)記住:以獨(dú)立語(yǔ)句將newed對(duì)象存儲(chǔ)于(置入)智能指針內(nèi)。如果不這樣做,一旦異常拋出,有可能導(dǎo)致難以察覺(jué)的資源泄漏。
4. 設(shè)計(jì)與聲明
所謂軟件設(shè)計(jì),是“令軟件做出你希望它做的事情”的步驟和做法,通常以頗為一般性的構(gòu)想開(kāi)始,最終變成十足的細(xì)節(jié),以允許特殊接口的開(kāi)發(fā)。
條款18:讓接口容易被正確使用,不易被誤用
- 好的接口很容易被正確使用,不容易被誤用。你應(yīng)該在你的所有接口中努力達(dá)成這些性質(zhì)。
- “促進(jìn)正確使用”的辦法包括接口的一致性,以及與內(nèi)置類型的行為兼容。
- “阻止誤用”的辦法包括建立新類型、限制類型上的操作,束縛對(duì)象值,以及消除客戶的資源管理責(zé)任。
- tr1::shared_ptr支持定制刪除器。這可防范DLL問(wèn)題,可被用來(lái)自動(dòng)解除互斥量等等。
條款19:設(shè)計(jì)class猶如設(shè)計(jì)type
設(shè)計(jì)一個(gè)良好的類,或者稱作類型,考慮一下設(shè)計(jì)規(guī)范:
- 新類型的對(duì)象應(yīng)該如何被創(chuàng)建和銷毀?
- 對(duì)象的初始化和對(duì)象的賦值該有什么樣的差別?
- 新類型的對(duì)象如果被passed by value(值傳遞),意味著什么?
- 什么是新類型的“合法值”?
- 你的新類型需要配合某個(gè)繼承圖系嗎?
- 你的新類型需要什么樣的轉(zhuǎn)換?
- 什么樣的操作符和函數(shù)對(duì)此新類型而言是合理的?
- 什么樣的標(biāo)準(zhǔn)函數(shù)應(yīng)該駁回?
- 誰(shuí)該取用新類型的成員?
- 什么是新類型的“未聲明接口”?
- 你的新類型有多么一般化?
- 你真的需要一個(gè)新類型嗎?
條款20:寧以pass-by-reference-to-const替代psss-by-value
缺省情況下C++以by value方式傳遞對(duì)象至函數(shù)。除非你另外指定,否則函數(shù)參數(shù)都是以實(shí)際實(shí)參的副本為初值,而調(diào)用端所獲得的亦是返回值的一個(gè)副本。這些副本由對(duì)象的拷貝構(gòu)造函數(shù)產(chǎn)生。所以在以對(duì)象為by value時(shí),可能會(huì)調(diào)用相應(yīng)的構(gòu)造函數(shù)(成員對(duì)象的構(gòu)造、基類對(duì)象的構(gòu)造),然后調(diào)用對(duì)應(yīng)的析構(gòu)函數(shù)。所以以by value的形式開(kāi)銷還是比較大的。
如果我們用pass-by-reference-to-const,例如:bool validateStudent(const Student& s);,這種傳遞方式效率高得多:沒(méi)有任何構(gòu)造函數(shù)或析構(gòu)函數(shù)被調(diào)用,因?yàn)闆](méi)有任何新對(duì)象被創(chuàng)建。以傳引用方式傳遞參數(shù)也可以避免對(duì)象切割問(wèn)題:即當(dāng)一個(gè)派生類對(duì)象以傳值的方式傳遞并被視為一個(gè)基類對(duì)象,基類對(duì)象的拷貝構(gòu)造函數(shù)會(huì)被調(diào)用,而“造成此對(duì)象的行為像個(gè)派生類對(duì)象”的那些特化性質(zhì)全被切割掉了,僅僅留下了基類對(duì)象。這一般不是你想要的。
所以我們一般的做法應(yīng)該是這樣:內(nèi)置對(duì)象和STL的迭代器和函數(shù)對(duì)象,我們一般以傳值的方式傳遞,而其它的任何東西都以傳引用的方式傳遞。
條款21:必須返回對(duì)象時(shí),別妄想返回其reference
- 不要返回一個(gè)指向局部變量的指針或引用,因?yàn)榫植孔兞吭诤瘮?shù)返回后就沒(méi)有存在的意義。
- 不要返回一個(gè)在堆上對(duì)象的引用,這會(huì)埋下了資源泄漏的危險(xiǎn)。誰(shuí)該對(duì)這對(duì)象實(shí)施delete呢?別把這種對(duì)資源的管理寄托完全寄托于用戶。
- 不要返回一個(gè)被定義于函數(shù)內(nèi)部的靜態(tài)對(duì)象的引用。
- “必須返回新對(duì)象”的函數(shù)的正確寫法是:就讓那個(gè)函數(shù)返回一個(gè)新對(duì)象。
條款22:將成員變量聲明為private
將成員變量隱藏在函數(shù)接口的背后,可以為“所有可能的實(shí)現(xiàn)”提供彈性。例如,這可使得成員變量被讀或?qū)憰r(shí)輕松通知其它對(duì)象、可以驗(yàn)證calss的約束條件以及函數(shù)的前提和事后狀態(tài)、可以在多線程環(huán)境中執(zhí)行同步控制......
不封裝意味不可改變!成員變量的封裝性與“成員變量的內(nèi)容改變時(shí)所壞量的代碼數(shù)量”成反比。
條款23:寧以non-member、non-friend替換member函數(shù)
寧可拿non-member non-friend函數(shù)替代member函數(shù)。這樣做可以增加封裝性、包裹彈性和機(jī)能擴(kuò)充性。
一般我們相當(dāng)然以為類中的成員函數(shù)更具封裝性,而實(shí)際上并不是那么一回事,因?yàn)槌蓡T函數(shù)不僅可以訪問(wèn)private成員變量,也可以取用private函數(shù)、enums、typedefs等等。而非成員非友元函數(shù)能實(shí)現(xiàn)更大的封裝性,因?yàn)樗荒茉L問(wèn)public函數(shù)。
將所有便利函數(shù)放在多個(gè)頭文件內(nèi)但隸屬同一個(gè)命名空間,意味客戶可以輕松擴(kuò)展這一組便利函數(shù)。需要做的就是添加更多non-member non-friend函數(shù)到此命名空間內(nèi)。
條款24:若所有參數(shù)皆需類型轉(zhuǎn)換,請(qǐng)為此采用non-member函數(shù)
通常,令類支持隱式類型轉(zhuǎn)換通常是個(gè)糟糕的主意。當(dāng)然這條規(guī)則有其例外,最常見(jiàn)的例外是在建立數(shù)值類型時(shí)。
const Rational operator*(const Rational& rhs) const;
//如果定義一個(gè)有理數(shù)類,并實(shí)現(xiàn)*操作符為成員函數(shù),如上所示;那么考慮一下調(diào)用:
Rational oneHalf(1, 2);
result = oneHalf * 2; // 正確,2被隱式轉(zhuǎn)換為Rational(2,1)
//編譯器眼中應(yīng)該是這樣:const Rational temp(2); result = oneHalf * temp;
result = 2 * oneHalf; // 錯(cuò)誤,2,可不被認(rèn)為是Rational對(duì)象;因此無(wú)法調(diào)用operator*
可見(jiàn),這樣并不準(zhǔn)確,因?yàn)槌朔☉?yīng)該滿足交換律,不是嗎?所以,支持混合式算術(shù)運(yùn)算的可行之道應(yīng)該是:讓operator*成為一個(gè)non-member函數(shù),允許編譯器在每一個(gè)實(shí)參上執(zhí)行隱式類型轉(zhuǎn)換:
class Rational
{
... // contains no operator*
};
const Rational operator*(const Rational& lhs, Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2;
result = 2 * oneFourth; //這下兩個(gè)都工作的很好,通過(guò)隱式轉(zhuǎn)換實(shí)現(xiàn)
成員函數(shù)的方面是非成員函數(shù),而不是友元函數(shù)??梢杂妙愔械膒ublic接口實(shí)現(xiàn)的函數(shù),最好就是非成員函數(shù),而不是采用友元函數(shù)。
請(qǐng)記?。?/strong>如果你需要為某個(gè)函數(shù)的所有參數(shù)(包括被this指針?biāo)傅哪莻€(gè)隱喻參數(shù))進(jìn)行類型轉(zhuǎn)換,那么這個(gè)函數(shù)必須是個(gè)non-member。
條款25:考慮寫出一個(gè)不拋異常的swap函數(shù)
- 當(dāng)std::swap對(duì)你的類型效率不高時(shí),提供一個(gè)swap成員函數(shù),并確定這個(gè)函數(shù)不拋出異常。
- 如果你提供一個(gè)member swap,也該提供一個(gè)non-member swap用來(lái)調(diào)用前者。對(duì)于class(而非templates),也請(qǐng)?zhí)鼗痵td::swap。
- 調(diào)用swap時(shí)應(yīng)針對(duì)std::swap使用using聲明式,然后調(diào)用swap并且不帶任何“命名空間資格修飾”。
- 為“用戶定義類型”進(jìn)行std templates全特化是好的,但千萬(wàn)不要嘗試在std內(nèi)加入某些對(duì)std而言全新的東西。
5. 實(shí)現(xiàn)
大多數(shù)情況下,適當(dāng)提出你的類定義及函數(shù)聲明,是花費(fèi)最多心力的兩件事。盡管如此,還是有很多東西需要小心:太快定義變量可能造成效率上的拖延;過(guò)度使用轉(zhuǎn)型(casts)可能導(dǎo)致代碼變慢又難維護(hù),又招來(lái)微妙難解的錯(cuò)誤;返回對(duì)象“內(nèi)部數(shù)據(jù)之號(hào)碼牌(handls)”可能會(huì)破壞封裝并留給客戶虛吊號(hào)碼牌;為考慮異常帶來(lái)的沖擊則可能導(dǎo)致資源泄漏和數(shù)據(jù)敗壞;過(guò)度使用inline function可能引起代碼膨脹;過(guò)度耦合則可能導(dǎo)致讓人不滿意的冗長(zhǎng)build times。
條款26:盡可能延后變量定義式的出現(xiàn)時(shí)間
只要你定義了一個(gè)變量而其類型帶有一個(gè)構(gòu)造函數(shù)或析構(gòu)函數(shù),那么當(dāng)程序的控制流到達(dá)這個(gè)變量定義式時(shí),你便得承受構(gòu)造成本;當(dāng)這個(gè)變量離開(kāi)其作用域時(shí),你便得承受析構(gòu)成本。即使這個(gè)變量最終并未被使用,仍需耗費(fèi)這些成本,所以應(yīng)該盡量避免這種情形。因此,最好延后該變量的定義式,直到非得使用該變量的前一刻為止,甚至應(yīng)該嘗試延后這份定義直到能夠給它初值實(shí)參為止。
std::string encryptPassword(const std::string& password)
{
using namespace std;
string encrypted; //1
if (password.length() < MinimumPasswordLength)
{
throw logic_error("Password is too short"); //注意:可能拋出異常
}
string encrypted; //2
...
return encrypted;
}
如上代碼,encrypted在2處定義是個(gè)不錯(cuò)的選擇,因?yàn)槿绻麙伋霎惓?,那么encrypted的構(gòu)造和析構(gòu)可是做了無(wú)用功??!
條款27:盡量少做轉(zhuǎn)型動(dòng)作
C++規(guī)則的設(shè)計(jì)目標(biāo)之一是,保證“類型錯(cuò)誤”絕不可能發(fā)生。不幸的是,轉(zhuǎn)型(casts)破壞了類型系統(tǒng)。那可能導(dǎo)致任何種類的麻煩,有些容易辨識(shí),有些非常隱晦。
兩種舊式轉(zhuǎn)型:
(T)expression //C風(fēng)格的轉(zhuǎn)型動(dòng)作
T(expression) //函數(shù)風(fēng)格的轉(zhuǎn)型動(dòng)作
C++還提供四種新式轉(zhuǎn)型:
- const_cast:通常被用來(lái)將對(duì)象的常量性轉(zhuǎn)除;即去掉const。
- dynamic_cast:主要用來(lái)執(zhí)行“安全向下轉(zhuǎn)型”,也就是用來(lái)決定某對(duì)象是否歸屬繼承體系中的某個(gè)類型。
- reinterpret_cast:意圖執(zhí)行低級(jí)轉(zhuǎn)型,實(shí)際動(dòng)作可能取決于編譯器,這也就表示它不可移植。
- static_cast:用來(lái)強(qiáng)迫隱式轉(zhuǎn)換,例如將non-const轉(zhuǎn)型為const,int轉(zhuǎn)型為double等等。
盡量使用新式轉(zhuǎn)型:它們很容易在代碼中被辨識(shí)出來(lái),因而得以簡(jiǎn)化“找出類型系統(tǒng)在哪個(gè)地點(diǎn)被破壞”的過(guò)程;各轉(zhuǎn)型動(dòng)作的目標(biāo)愈窄化,編譯器愈可能診斷出錯(cuò)誤的運(yùn)用。
請(qǐng)記?。?/strong>
- 如果可以,盡量避免轉(zhuǎn)型,特別是在注重效率的代碼中避免dynamic_casts。如果有個(gè)設(shè)計(jì)需要轉(zhuǎn)型動(dòng)作,試著發(fā)展無(wú)需轉(zhuǎn)型的替代設(shè)計(jì)。
- 如果轉(zhuǎn)型是必要的,試著將它隱藏于某個(gè)函數(shù)背后。客戶隨后可以調(diào)用該函數(shù),而不需將轉(zhuǎn)型放進(jìn)他們自己的代碼內(nèi)。
- 寧可使用C++-style(新式)轉(zhuǎn)型,不要使用舊式轉(zhuǎn)型。前者很容易辨識(shí)出來(lái),而且也比較有著分門別類的執(zhí)掌。
條款28:避免返回handls指向?qū)ο髢?nèi)部成分
struct RectData
{
Point ulhc;
Point lrhc;
};
class Rectangle
{
public:
...
Point& upperLeft() const { return pData->ulhc; }//1 const只對(duì)函數(shù)內(nèi)進(jìn)行保護(hù),函數(shù)返回后呢??
Point& lowerRight() const { return pData->lrhc; } //2 const只對(duì)函數(shù)內(nèi)進(jìn)行保護(hù),函數(shù)返回后呢??
private:
std::tr1::shared_ptr<RectData> pData;
...
};
1,2兩函數(shù)都返回引用,指向private內(nèi)部數(shù)據(jù),調(diào)用者于是可通過(guò)這些引用更改內(nèi)部數(shù)據(jù)!這嚴(yán)重破壞了數(shù)據(jù)的封裝性,對(duì)私有成員進(jìn)行直接操作?太不可思議了!
const Point& upperLeft() const { return pData->ulhc; } //3
const Point& lowerRight() const { return pData->lrhc; } //4
或者將1,2改為3,4,這就限制了客戶的“涂改權(quán)”,只有“讀取權(quán)”。但終究“返回一個(gè)handle代表對(duì)象內(nèi)部成分”總是危險(xiǎn)的。特別是將返回的指針或引用賦值給其它指針或引用,那么久造成了“懸空”。
請(qǐng)記?。?/strong>避免返回handles(包括reference、指針、迭代器)指向?qū)ο髢?nèi)部。遵守這個(gè)條款可增加封裝性,幫助const成員函數(shù)的行為像個(gè)const,并將發(fā)生“虛吊號(hào)碼牌”(dangling handles)的可能性降至最低。
條款29:為“異常安全”而努力是值得的
- 異常安全函數(shù)(Exception-safe functions)即使發(fā)生異常也不會(huì)泄漏資源或允許任何數(shù)據(jù)結(jié)構(gòu)敗壞。這樣的函數(shù)區(qū)分為三種可能的保證:基本型、強(qiáng)烈型、不拋異常型。
- “強(qiáng)烈保證”往往能夠以copy-and-swap實(shí)現(xiàn)出來(lái),但“強(qiáng)烈保證”并非對(duì)所有函數(shù)都可實(shí)現(xiàn)或具備現(xiàn)實(shí)意義。
- 函數(shù)提供的“異常安全保證”通常最高只等于其所調(diào)用之各個(gè)函數(shù)的“異常安全保證”中的最弱者。
條款30:透徹了解inlining的里里外外
Inline函數(shù)看起來(lái)像函數(shù),動(dòng)作像函數(shù),比宏好得多,可以調(diào)用它們又不需蒙受函數(shù)調(diào)用所招致的額外開(kāi)銷。你實(shí)際獲得的比想象的還多,編譯器有能力對(duì)執(zhí)行語(yǔ)境相關(guān)最優(yōu)化。然而編寫程序就像現(xiàn)實(shí)生活一樣,沒(méi)有白吃的午餐。inline函數(shù)也不例外,這樣做可能增加你的目標(biāo)碼。
如果inline函數(shù)的本體很小,編譯器針對(duì)“函數(shù)本體”所產(chǎn)生的碼可能比針對(duì)“函數(shù)調(diào)用”所產(chǎn)出的碼更小。果真如此,將函數(shù)inlining確實(shí)可能導(dǎo)致較小的目標(biāo)碼和較高的指令高速緩存裝置擊中率。
Inline函數(shù)通常一定被置于頭文件內(nèi),因?yàn)榇蠖鄶?shù)建置環(huán)境在編譯過(guò)程中進(jìn)行inlining,而為了將一個(gè)“函數(shù)調(diào)用”替換為“被調(diào)用函數(shù)的本體”,編譯器必須知道那個(gè)函數(shù)長(zhǎng)什么樣子。Template通常也被置于頭文件內(nèi),因?yàn)樗坏┍皇褂?,編譯器為了將它具現(xiàn)化,需要知道哦啊它長(zhǎng)什么樣子。Template的具現(xiàn)化與inlining無(wú)關(guān)。如果你正在寫一個(gè)template而你認(rèn)為所有根據(jù)此template具現(xiàn)出來(lái)的函數(shù)都應(yīng)該inlined,請(qǐng)將此template聲明為inline;但如果你寫的template煤油理由要求它所具現(xiàn)的每一個(gè)函數(shù)都是inlined,就應(yīng)該避免將這個(gè)template聲明為inline。
一個(gè)表面上看似inline的函數(shù)是否真實(shí)inline,主要取決于編譯器。有時(shí)候雖然編譯器有意愿inlining某個(gè)函數(shù),還是可能為該函數(shù)生成一個(gè)函數(shù)本體(函數(shù)指針,構(gòu)造函數(shù),析構(gòu)函數(shù))。對(duì)程序開(kāi)發(fā)而言,將上述所有考慮牢記在新很是重要,但若從純粹實(shí)用觀點(diǎn)出發(fā),有一個(gè)事實(shí)比其它因素更重要:大部分調(diào)試器面對(duì)inline函數(shù)都束手無(wú)策。
這使我們?cè)跊Q定哪些函數(shù)該被聲明為inline而哪些函數(shù)不該時(shí),掌握一個(gè)合乎邏輯的策略。一開(kāi)始先不要將任何函數(shù)聲明為inline,或至少將inlining施行范圍局限在那些“一定成為inline”或“十分平淡無(wú)奇”的函數(shù)身上。
條款31:將文件間的編譯依存關(guān)系降至最低
6 繼承與面對(duì)對(duì)象設(shè)計(jì)
條款32:確定你的public繼承塑模出is-a關(guān)系
以C++進(jìn)行面向?qū)ο缶幊?,最重要的一個(gè)規(guī)則是:public inheritance(公有繼承)意味is-a(是一種)的關(guān)系。如果你令class D以public形式繼承class B,你便是告訴C++編譯器(以及你的代碼讀者)說(shuō),每一個(gè)類型為D的對(duì)象同時(shí)也是一個(gè)類型為B的對(duì)象,反之不成立。B對(duì)象可派上用場(chǎng)的任何地方,D對(duì)象一樣可以派上用場(chǎng)。
在C++領(lǐng)域中,任何函數(shù)如果期望獲得一個(gè)類型為基類的實(shí)參(而不管是傳指針或是引用),都也愿意接受一個(gè)派生類對(duì)象(而不管是傳指針或是引用)。(只對(duì)public繼承才成立。)
條款33:避免遮掩繼承而來(lái)的名稱
C++的名稱遮掩規(guī)則所做的唯一事情就是:遮掩名稱。至于名稱的類型是否相同并不重要。即只要名稱相同就覆蓋基類相應(yīng)的成員,類型和參數(shù)個(gè)數(shù)都無(wú)關(guān)緊要。
派生類的作用域嵌套在基類的作用域內(nèi)。C++的繼承關(guān)系的遮掩名稱也并不管成員函數(shù)是純虛函數(shù),非純虛函數(shù)或非虛函數(shù)等。只和名稱有關(guān)。 如果你真的需要用到基類的被名稱遮掩的函數(shù),可以使用using聲明式或轉(zhuǎn)交函數(shù)(forwarding function),引入基類的成員函數(shù)。若是基類中存在函數(shù)重載的情況,在派生類中應(yīng)注意同名函數(shù)覆蓋的問(wèn)題!
條款34:區(qū)分接口繼承和實(shí)現(xiàn)繼承
表面上直截了當(dāng)?shù)膒ublic繼承概念,經(jīng)過(guò)更嚴(yán)密的檢查之后,發(fā)現(xiàn)它由兩部分組成:函數(shù)接口繼承和函數(shù)實(shí)現(xiàn)繼承。
- 成員函數(shù)的接口總是會(huì)被繼承。
- 聲明一個(gè)純虛函數(shù)的目的是為了讓派生類只繼承函數(shù)接口。
- 聲明一個(gè)虛函數(shù)的目的是讓派生類繼承該函數(shù)的接口和缺省實(shí)現(xiàn)。
- 聲明一個(gè)非虛函數(shù)的目的是為了令派生類繼承函數(shù)的接口及一份強(qiáng)制性實(shí)現(xiàn)。
由于不同類型的聲明意味著本質(zhì)并不相同的事情,當(dāng)你聲明你的函數(shù)成員時(shí),必須謹(jǐn)慎選擇。如果將所以函數(shù)都聲明為非虛函數(shù),這將使得派生類沒(méi)有余??臻g進(jìn)行特化工作,而基類的非虛析構(gòu)函數(shù)尤其會(huì)帶來(lái)問(wèn)題,實(shí)際上任何被當(dāng)作基類來(lái)使用的類都會(huì)擁有若干虛函數(shù)。將所有成員函數(shù)聲明為虛函數(shù)也是一種常見(jiàn)的錯(cuò)誤,當(dāng)然有時(shí)也是正確的,如果你的不變性凌駕于特異性,別害怕說(shuō)出來(lái)。
條款35:考慮virtual函數(shù)以外的其它選擇
條款36:絕不重新定義繼承而來(lái)的non-virtual函數(shù)
假設(shè)基類B中有一個(gè)非虛函數(shù)mf,派生類D在繼承實(shí)現(xiàn)時(shí)定義了自己的mf版本,將會(huì)導(dǎo)致基類的mf被隱藏。如果定義一個(gè)基類指針指向一個(gè)派生類對(duì)象并調(diào)用mf函數(shù)時(shí),其結(jié)果將會(huì)調(diào)用基類版本的mf函數(shù),而不是派生類版本的mf函數(shù)!這與虛函數(shù)的動(dòng)態(tài)綁定并不一樣,非虛函數(shù)是靜態(tài)綁定的。
條款37:絕不重新定義繼承而來(lái)的缺省參數(shù)值
對(duì)于non-virtual函數(shù),上一條款說(shuō)到,“絕不重新定義繼承而來(lái)的non-virtual函數(shù)”,而對(duì)于繼承一個(gè)帶有缺省參數(shù)值的virtual函數(shù),也是如此。即絕不重新定義繼承而來(lái)的缺省參數(shù)值。因?yàn)椋簐irtual函數(shù)是動(dòng)態(tài)綁定,而缺省參數(shù)值卻是靜態(tài)綁定。
一個(gè)指向派生類的基類指針,如果兩者的一個(gè)虛函數(shù)指定了不同的缺省參數(shù)值 ,那么,利用基類指針調(diào)用派生類虛函數(shù)值時(shí),其缺省參數(shù)值將是基類指定的!這是因?yàn)槿笔?shù)值卻是靜態(tài)綁定,而該指針是一個(gè)基類對(duì)象。
條款38:通過(guò)復(fù)合塑模出has-a或“根據(jù)某物實(shí)現(xiàn)出”
復(fù)合(composition)的意義和public繼承(is-a)完全不同。當(dāng)你的對(duì)象是你所塑造的世界中的某些事物,復(fù)合意味著has-a的關(guān)系,如人擁有名字、地址、電話等對(duì)象;當(dāng)你的對(duì)象純粹是實(shí)現(xiàn)細(xì)節(jié)上的人工制品(如鏈表、集合、互斥器等),復(fù)合則是表現(xiàn)的“由某某實(shí)現(xiàn)出”的關(guān)系,比如由鏈表實(shí)現(xiàn)出集合。
條款39、40:明智而審慎地使用私有繼承和多重繼承
私有繼承意味著“由某物實(shí)現(xiàn)出”的關(guān)系,但是實(shí)現(xiàn)中,盡可能使用復(fù)合而不是私有繼承來(lái)實(shí)現(xiàn)這一關(guān)系。