為什么寫這篇BLOG
我動手寫這篇博文——或者說總結(jié)——的想法已經(jīng)很久了针肥,《Effective C++》這本書的作者和譯者都是C++大師宪迟,這篇著作有也已享譽(yù)全球很多年躯枢。但是書無完書歉井、人無完人欺嗤,這本書也因?yàn)檫@樣或那樣的原因(我更愿稱之為引起我不適的問題)讓我有必要為此寫一篇總結(jié)汽摹,使得這篇總結(jié)更像《Effective C++》對應(yīng)的工具書版本驮履,幫助我在未來想要回顧某一條款的內(nèi)容時,最大限度地節(jié)約我的時間鹦肿。如果沒有讀過這本書的讀者因?yàn)榉g或者是其他問題沒有耐心再讀下去的時候矗烛,不妨也看看這篇文章,我會從一個中國人的邏輯角度箩溃,使用大陸人的語言習(xí)慣(原譯者是中國臺灣同胞)瞭吃,盡可能直接并清楚得涵蓋每一個條款最重要的知識點(diǎn),讓你在最短的時間抓住核心涣旨,再逐個擊破各個問題歪架。我并不覺得這篇文章可以就此替代《Effective C++》,其實(shí)是遠(yuǎn)遠(yuǎn)不夠霹陡,我并不會在文章中涵蓋太多的代碼和細(xì)節(jié)和蚪,如果你想要探究每一個細(xì)節(jié),請拿起原著烹棉,乖乖把每一頁看完攒霹。
首先,我想說一說這本書讓我不適的地方:
- 內(nèi)容有點(diǎn)老舊浆洗。這本書沒有涵蓋C++11催束,可以說,有了更高版本的編譯器伏社,許多條款使用C++98解決問題的思路和方式都顯得有些冗余了抠刺,我會在每一條款的總結(jié)中直接指出在更高版本C++下的解決方案塔淤,個人看來,書中提出的解決問題的方法就可以淘汰了矫付,這些地方包括但不限于
final, override, shared_ptr, = delete
凯沪。 - 翻譯僵硬。這并不能怪侯捷买优,因?yàn)槊鎸σ粋€大師的作品,我們肯定要在保留語言的原汁原味和盡量符合各國讀者的語言風(fēng)格面前搖擺取舍挺举,但這也造成了相當(dāng)英文的表達(dá)出現(xiàn)在了文中杀赢,比如“在這個故事結(jié)束之前”,“那就進(jìn)入某某某變奏曲了”湘纵,讓不太熟悉英文的讀者感到莫名其妙——”變奏曲在哪脂崔?“。說實(shí)在話梧喷,就是知道英文原文的我讀起這樣的翻譯也覺得怪怪的砌左。因此在我的總結(jié)中,面對各種因果關(guān)系我會把舌頭捋直了說铺敌,畢竟我不是大師汇歹,只注重效率就可以了。
- 作者的行文之風(fēng)讓讀者必須以讀一本小說的心態(tài)去拜讀這部著作偿凭。在了解每一個條款時产弹,作者精心準(zhǔn)備了各種玩笑、名人名言弯囊、典故以及例子痰哨,盡量讓你感覺不到教科書般的迂腐之氣,也用了俏皮的語言使授課不那么僵硬(盡管上述翻譯還是讓它僵硬了起來)匾嘱。但對于第二甚至是第三次讀這本書的我來說斤斧,我更希望這本書像一本工具書。例如某條款解決了一個問題霎烙,在第一遍讀的時候我重點(diǎn)去體會解決問題的方法是什么撬讽,第二遍我可能更想知道這種問題在什么情況下可能會發(fā)生——什么時候去用,這是我最關(guān)心的吼过。不幸的是锐秦,出現(xiàn)這一問題的三個場景可能分布在這個條款的角角落落,我必須重新去讀一遍那些已經(jīng)不好笑的笑話盗忱、已經(jīng)不經(jīng)典的典故酱床,才能把他們整理好。所以趟佃,這篇博文替我把上述我在翻閱時更c(diǎn)are的內(nèi)容總結(jié)起來扇谣,爭取兩分鐘可以把一個條款的綱要回憶起來,這便是這個博文的目的。
最后,再次向Meyers和侯捷大師致敬毒返。
二、構(gòu)造、析構(gòu)和賦值運(yùn)算
構(gòu)造和析構(gòu)一方面是對象的誕生和終結(jié);另一方面兔乞,它們也意味著資源的開辟和歸還。這些操作犯錯誤會導(dǎo)致深遠(yuǎn)的后果——你需要產(chǎn)生和銷毀的每一個對象都面臨著風(fēng)險淡溯。這些函數(shù)形成了一個自定義類的脊柱咱娶,所以如何確保這些函數(shù)的行為正確是“生死攸關(guān)”的大事。
條款05:了解C++默默編寫并調(diào)用了哪些函數(shù)
編譯器會主動為你編寫的任何類聲明一個拷貝構(gòu)造函數(shù)琼了、拷貝復(fù)制操作符和一個析構(gòu)函數(shù)昧诱,同時如果你沒有生命任何構(gòu)造函數(shù)燥爷,編譯器也會為你聲明一個default版本的拷貝構(gòu)造函數(shù)勺拣,這些函數(shù)都是public
且inline
的。注意,上邊說的是聲明哦,只有當(dāng)這些函數(shù)有調(diào)用需求的時候,編譯器才會幫你去實(shí)現(xiàn)它們蜘澜。但是編譯器替你實(shí)現(xiàn)的函數(shù)可能在類內(nèi)引用、類內(nèi)指針鄙信、有const
成員以及類型有虛屬性的情形下會出問題。
- 對于拷貝構(gòu)造函數(shù)银受,你要考慮到類內(nèi)成員有沒有深拷貝的需求,如果有的話就需要自己編寫拷貝構(gòu)造函數(shù)/操作符鸦采,而不是把這件事情交給編譯器來做。
- 對于拷貝構(gòu)造函數(shù),如果類內(nèi)有引用成員或
const
成員,你需要自己定義拷貝行為鲜侥,因?yàn)榫幾g器替你實(shí)現(xiàn)的拷貝行為在上述兩個場景很有可能是有問題的崎苗。 - 對于析構(gòu)函數(shù),如果該類有多態(tài)需求舀寓,請主動將析構(gòu)函數(shù)聲明為
virtual
胆数,具體請看條款07 。
除了這些特殊的場景以外互墓,如果不是及其簡單的類型必尼,請自己編寫好構(gòu)造、析構(gòu)篡撵、拷貝構(gòu)造和賦值操作符判莉、移動構(gòu)造和賦值操作符(C++11、如有必要)這六個函數(shù)育谬。
條款06:若不想使用編譯器自動生成的函數(shù)券盅,就該明確拒絕。
承接上一條款膛檀,如果你的類型在語義或功能上需要明確禁止某些函數(shù)的調(diào)用行為锰镀,比如禁止拷貝行為,那么你就應(yīng)該禁止編譯器去自動生成它咖刃。作者在這里給出了兩種方案來實(shí)現(xiàn)這一目標(biāo):
- 將被禁止生成的函數(shù)聲明為
private
并省略實(shí)現(xiàn)泳炉,這樣可以禁止來自類外的調(diào)用。但是如果類內(nèi)不小心調(diào)用了(成員函數(shù)僵缺、友元)胡桃,那么會得到一個鏈接錯誤。 - 將上述的可能的鏈接錯誤轉(zhuǎn)移到編譯期間磕潮。設(shè)計一不可拷貝的工具基類翠胰,將真正不可拷貝的基類私有繼承該基類型即可,但是這樣的做法過于復(fù)雜自脯,對于已經(jīng)有繼承關(guān)系的類型會引入多繼承之景,同時讓代碼晦澀難懂。
但是有了C++11膏潮,我們可以直接使用= delete來聲明拷貝構(gòu)造函數(shù)锻狗,顯示禁止編譯器生成該函數(shù)。
條款07:為多態(tài)基類聲明virtual
該條款的核心內(nèi)容為:帶有多態(tài)性質(zhì)的基類必須將析構(gòu)函數(shù)聲明為虛函數(shù),防止指向子類的基類指針在被釋放時只局部銷毀了該對象轻纪。如果一個類有多態(tài)的內(nèi)涵油额,那么幾乎不可避免的會有基類的指針(或引用)指向子類對象,因?yàn)榉翘摵瘮?shù)沒有動態(tài)類型刻帚,所以如果基類的析構(gòu)函數(shù)不是虛函數(shù)潦嘶,那么在基類指針析構(gòu)時會直接調(diào)用基類的析構(gòu)函數(shù),造成子類對象僅僅析構(gòu)了基類的那一部分崇众,有內(nèi)存泄漏的風(fēng)險掂僵。除此之外,還需注意:
- 需要注意的是顷歌,普通的基類無需也不應(yīng)該有虛析構(gòu)函數(shù)锰蓬,因?yàn)樘摵瘮?shù)無論在時間還是空間上都會有代價,詳情《More Effective C++》條款24眯漩。
- 如果一個類型沒有被設(shè)計成基類芹扭,又有被誤繼承的風(fēng)險,請在類中聲明為
final
(C++ 11)赦抖,這樣禁止派生可以防止誤繼承造成上述問題冯勉。 - 編譯器自動生成的析構(gòu)函數(shù)時非虛的,所以多態(tài)基類必須將析構(gòu)函數(shù)顯示聲明為
virtual
摹芙。
條款08:別讓異常逃離析構(gòu)函數(shù)
析構(gòu)函數(shù)一般情況下不應(yīng)拋出異常,因?yàn)楹艽罂赡馨l(fā)生各種未定義的問題宛瞄,包括但不限于內(nèi)存泄露浮禾、程序異常崩潰、所有權(quán)被鎖死等份汗。
一個直觀的解釋:析構(gòu)函數(shù)是一個對象生存期的最后一刻盈电,負(fù)責(zé)許多重要的工作,如線程杯活,連接和內(nèi)存等各種資源所有權(quán)的歸還匆帚。如果析構(gòu)函數(shù)執(zhí)行期間某個時刻拋出了異常,就說明拋出異常后的代碼無法再繼續(xù)執(zhí)行旁钧,這是一個非常危險的舉動——因?yàn)槲鰳?gòu)函數(shù)往往是為類對象兜底的吸重,甚至是在該對象其他地方出現(xiàn)任何異常的時候,析構(gòu)函數(shù)也有可能會被調(diào)用來給程序擦屁股歪今。在上述場景中嚎幸,如果在一個異常環(huán)境中執(zhí)行的析構(gòu)函數(shù)又拋出了異常,很有可能會讓程序直接崩潰寄猩,這是每一個程序員都不想看到的嫉晶。
話說回來,如果某些操作真的很容易拋出異常,如資源的歸還等替废,并且你又不想把異常吞掉箍铭,那么就請把這些操作移到析構(gòu)函數(shù)之外,提供一個普通函數(shù)做類似的清理工作椎镣,在析構(gòu)函數(shù)中只負(fù)責(zé)記錄诈火,我們需要時刻保證析構(gòu)函數(shù)能夠執(zhí)行到底。
條款09:絕不在構(gòu)造和析構(gòu)過程中調(diào)用virtual函數(shù)衣陶。
結(jié)論正如該條款的名字:請不要在構(gòu)造函數(shù)和析構(gòu)函數(shù)中調(diào)用virtual
函數(shù)柄瑰。
在多態(tài)環(huán)境中,我們需要重新理解構(gòu)造函數(shù)和析構(gòu)函數(shù)的意義剪况,這兩個函數(shù)在執(zhí)行過程中教沾,涉及到了對象類型從基類到子類,再從子類到基類的轉(zhuǎn)變译断。
一個子類對象開始創(chuàng)建時授翻,首先調(diào)用的是基類的構(gòu)造函數(shù),在調(diào)用子類構(gòu)造函數(shù)之前孙咪,該對象將一直保持著“基類對象”的身份而存在堪唐,自然在基類的構(gòu)造函數(shù)中調(diào)用的虛函數(shù)——將會是基類的虛函數(shù)版本,在子類的構(gòu)造函數(shù)中翎蹈,原先的基類對象變成了子類對象淮菠,這時子類構(gòu)造函數(shù)里調(diào)用的是子類的虛函數(shù)版本。這是一件有意思的事情荤堪,這說明在構(gòu)造函數(shù)中虛函數(shù)并不是虛函數(shù)合陵,在不同的構(gòu)造函數(shù)中,調(diào)用的虛函數(shù)版本并不同澄阳,因?yàn)殡S著不同層級的構(gòu)造函數(shù)調(diào)用時拥知,對象的類型在實(shí)時變化。那么相似的碎赢,析構(gòu)函數(shù)在調(diào)用的過程中低剔,子類對象的類型從子類退化到基類。
因此肮塞,如果你指望在基類的構(gòu)造函數(shù)中調(diào)用子類的虛函數(shù)襟齿,那就趁早打消這個想法好了。但很遺憾的是峦嗤,你可能并沒有意識到自己做出了這樣的設(shè)計蕊唐,例如將構(gòu)造函數(shù)的主要工作抽象成一個init()
函數(shù)以防止不同構(gòu)造函數(shù)的代碼重復(fù)是一個很常見的做法,但是在init()
函數(shù)中是否調(diào)用了虛函數(shù)烁设,就要好好注意一下了替梨,同樣的情況在析構(gòu)函數(shù)中也是一樣钓试。
條款10:令operator =返回一個reference to *this
簡單來說:這樣做可以讓你的賦值操作符實(shí)現(xiàn)“連等”的效果:
x = y = z = 10;
在設(shè)計接口時一個重要的原則是,讓自己的接口和內(nèi)置類型相同功能的接口盡可能相似副瀑,所以如果沒有特殊情況弓熏,就請讓你的賦值操作符的返回類型為ObjectClass&
類型并在代碼中返回*this
吧。
條款11:在operator=中處理“自我賦值”
自我賦值指的是將自己賦給自己糠睡。這是一種看似愚蠢無用但卻在代碼中出現(xiàn)次數(shù)比任何人想象的多得多的操作挽鞠,這種操作常常需要假借指針來實(shí)現(xiàn):
*pa = *pb; //pa和pb指向同一對象,便是自我賦值狈孔。
arr[i] = arr[j]; //i和j相等信认,便是自我賦值
那么對于管理一定資源的對象重載的operator = 中,一定要對是不是自我賦值格外小心并且增加預(yù)判均抽,因?yàn)闊o論是深拷貝還是資源所有權(quán)的轉(zhuǎn)移嫁赏,原先的內(nèi)存或所有權(quán)一定會被清空才能被賦值,如果不加處理油挥,這套邏輯被用在自我賦值上會發(fā)生——先把自己的資源給釋放掉了潦蝇,然后又把以釋放掉的資源賦給了自己——出錯了。
第一種做法是在賦值前增加預(yù)判深寥,但是這種做法沒有異常安全性攘乒,試想如果在刪除掉原指針指向的內(nèi)存后,在賦值之前任何一處跑出了異常惋鹅,那么原指針就指向了一塊已經(jīng)被刪除的內(nèi)存则酝。
SomeClass& SomeClass::operator=(const SomeClass& rhs) {
if (this == &rhs) return *this;
delete ptr;
ptr = new DataBlock(*rhs.ptr); //如果此處拋出異常,ptr將指向一塊已經(jīng)被刪除的內(nèi)存闰集。
return *this;
}
如果我們把異常安全性也考慮在內(nèi)堤魁,那么我們就會得到如下方法,令人欣慰的是這個方法也解決了自我賦值的問題返十。
SomeClass& SomeClass::operator=(const SomeClass& rhs) {
DataBlock* pOrg = ptr;
ptr = new DataBlock(*rhs.ptr); //如果此處拋出異常,ptr仍然指向之前的內(nèi)存椭微。
delete pOrg;
return *this;
}
另一個使用copy and swap技術(shù)的替代方案將在條款29中作出詳細(xì)解釋。
條款12:復(fù)制對象時勿忘其每一個成分
所謂“每一個成分”,作者在這里其實(shí)想要提醒大家兩點(diǎn):
- 當(dāng)你給類多加了成員變量時妈拌,請不要忘記在拷貝構(gòu)造函數(shù)和賦值操作符中對新加的成員變量進(jìn)行處理棺禾。如果你忘記處理,編譯器也不會報錯本慕。
- 如果你的類有繼承排拷,那么在你為子類編寫拷貝構(gòu)造函數(shù)時一定要格外小心復(fù)制基類的每一個成分,這些成分往往是private的锅尘,所以你無法訪問它們监氢,你應(yīng)該讓子類使用子類的拷貝構(gòu)造函數(shù)去調(diào)用相應(yīng)基類的拷貝構(gòu)造函數(shù):
//在成員初始化列表顯示調(diào)用基類的拷貝構(gòu)造函數(shù)
ChildClass::ChildClass(const ChildClass& rhs) : BaseClass(rhs) {
// ...
}
除此之外布蔗,拷貝構(gòu)造函數(shù)和拷貝賦值操作符,他們兩個中任意一個不要去調(diào)用另一個浪腐,這雖然看上去是一個避免代碼重復(fù)好方法纵揍,但是是荒謬的。其根本原因在于拷貝構(gòu)造函數(shù)在構(gòu)造一個對象——這個對象在調(diào)用之前并不存在议街;而賦值操作符在改變一個對象——這個對象是已經(jīng)構(gòu)造好了的泽谨。因此前者調(diào)用后者是在給一個還未構(gòu)造好的對象賦值;而后者調(diào)用前者就像是在構(gòu)造一個已經(jīng)存在了的對象特漩。不要這么做吧雹!
三、資源管理
內(nèi)存只是眾多被管理的資源之一涂身,對待其他常見的資源如互斥鎖雄卷、文件描述器、數(shù)據(jù)庫連接等時访得,我們要遵循同一原則——如果你不再使用它們龙亲,確保將他們還給系統(tǒng)。本章正是在考慮異常悍抑、函數(shù)內(nèi)多重回傳路徑鳄炉、程序員不當(dāng)維護(hù)軟件的背景下嘗試和資源管理打交道。本章除了介紹基于對象的資源管理辦法搜骡,也專門對內(nèi)存管理提出了更深層次的建議拂盯。
條款13:以對象管理資源
本條款的核心觀點(diǎn)在于:以面向流程的方式管理資源(的獲取和釋放),總是會在各種意外出現(xiàn)時记靡,丟失對資源的控制權(quán)并造成資源泄露谈竿。以面向過程的方式管理資源意味著,資源的獲取和釋放都分別被封裝在函數(shù)中摸吠。這種管理方式意味著資源的索取者肩負(fù)著釋放它的責(zé)任空凸,但此時我們就要考慮一下以下幾個問題:調(diào)用者是否總是會記得釋放呢?調(diào)用者是否有能力保證合理地釋放資源呢寸痢?不給調(diào)用者過多義務(wù)的設(shè)計才是一個良好的設(shè)計呀洲。
首先我們看一下哪些問題會讓調(diào)用者釋放資源的計劃付諸東流:
- 一句簡單的
delete
語句并不會一定執(zhí)行,例如一個過早的return
語句或是在delete
語句之前某個語句拋出了異常啼止。 - 謹(jǐn)慎的編碼可能能在這一時刻保證程序不犯錯誤道逗,但無法保證軟件接受維護(hù)時,其他人在delete語句之前加入的return語句或異常重復(fù)第一條錯誤献烦。
為了保證資源的獲取和釋放一定會合理執(zhí)行滓窍,我們把獲取資源和釋放資源的任務(wù)封裝在一個對象中。當(dāng)我們構(gòu)造這個對象時資源自動獲取巩那,當(dāng)我們不需要資源時吏夯,我們讓對象析構(gòu)此蜈。這便是“Resource Acquisition Is Initialization; RAII”的想法,因?yàn)槲覀兛偸窃讷@得一筆資源后于同一語句內(nèi)初始化某個管理對象锦亦。無論控制流如何離開區(qū)塊舶替,一旦對象被銷毀(比如離開對象的作用域)其析構(gòu)函數(shù)會自動被調(diào)用。
具體實(shí)踐請參考C++11的shared_ptr<T>杠园。
四顾瞪、設(shè)計與聲明
接口的設(shè)計與聲明是一門學(xué)位,注意我說的是接口的設(shè)計——接口長什么樣子抛蚁,而不是接口內(nèi)部是怎么實(shí)現(xiàn)的陈醒。接口參數(shù)的類型選擇有什么學(xué)問?接口的返回類型又有什么要注意的地方瞧甩?接口應(yīng)該放在類內(nèi)部還是類的外部钉跷?這些問題的答案都將對接口的穩(wěn)定性、功能的正確性產(chǎn)生深遠(yuǎn)的影響肚逸。在這一章爷辙,我們將逐一討論這些問題。
條款18:讓接口容易被正確使用朦促,不易誤使用
本條款告教你如何幫助你的客戶在使用你的接口時避免他們犯錯誤膝晾。
在設(shè)計接口時,我們常常會錯誤地假設(shè)务冕,接口的調(diào)用者擁有某些必要的知識來規(guī)避一些常識性的錯誤血当。但事實(shí)上,接口的調(diào)用者并不總是像正在設(shè)計接口的我們一樣“聰明”或者知道接口實(shí)現(xiàn)的”內(nèi)幕信息“禀忆,結(jié)果就是臊旭,我們錯誤的假設(shè)使接口表現(xiàn)得不穩(wěn)定。這些不穩(wěn)定因素可能是由于調(diào)用者缺乏某些先驗(yàn)知識箩退,也有可能僅僅是代碼上的粗心錯誤离熏。接口的調(diào)用者可能是別人,也可能是未來的你戴涝。所以一個合理的接口撤奸,應(yīng)該盡可能的從語法層面并在編譯之時運(yùn)行之前,幫助接口的調(diào)用者規(guī)避可能的風(fēng)險喊括。
- 使用外覆類型(wrapper)提醒調(diào)用者傳參錯誤檢查,將參數(shù)的附加條件限制在類型本身
當(dāng)調(diào)用者試圖傳入數(shù)字“13”來表達(dá)一個“月份”的時候矢棚,你可以在函數(shù)內(nèi)部做運(yùn)行期的檢查郑什,然后提出報警或一個異常,但這樣的做法更像是一種責(zé)任轉(zhuǎn)嫁——調(diào)用者只有在嘗試過后才發(fā)現(xiàn)自己手殘把“12”寫成了“13”蒲肋。如果在設(shè)計參數(shù)類型時就把“月份”這一類型抽象出來蘑拯,比如使用enum class(強(qiáng)枚舉類型)钝满,就能幫助客戶在編譯時期就發(fā)現(xiàn)問題,把參數(shù)的附加條件限制在類型本身申窘,可以讓接口更易用弯蚜。
- 從語法層面限制調(diào)用者不能做的事
接口的調(diào)用者往往無意甚至沒有意識到自己犯了個錯誤,所以接口的設(shè)計者必須在語法層面做出限制剃法。一個比較常見的限制是加上const
碎捺,比如在operate*
的返回類型上加上const
修飾,可以防止無意錯誤的賦值if (a * b = c)
贷洲。
- 接口應(yīng)表現(xiàn)出與內(nèi)置類型的一致性
讓自己的類型和內(nèi)置類型的一致性收厨,比如自定義容器的接口在命名上和STL應(yīng)具備一致性,可以有效防止調(diào)用者犯錯誤优构∷腥或者你有兩個對象相乘的需求,那么你最好重載operator*
而并非設(shè)計名為”multiply”的成員函數(shù)钦椭。
- 從語法層面限制調(diào)用者必須做的事
別讓接口的調(diào)用者總是記得做某些事情拧额,接口的設(shè)計者應(yīng)在假定他們總是忘記這些條條框框的前提下設(shè)計接口。比如用智能指針代替原生指針就是為調(diào)用者著想的好例子彪腔。如果一個核心方法需要在使用前后設(shè)置和恢復(fù)環(huán)境(比如獲取鎖和歸還鎖)侥锦,更好的做法是將設(shè)置和恢復(fù)環(huán)境設(shè)置成純虛函數(shù)并要求調(diào)用者繼承該抽象類,強(qiáng)制他們?nèi)?shí)現(xiàn)漫仆。在核心方法前后對設(shè)置和恢復(fù)環(huán)境的調(diào)用捎拯,則應(yīng)由接口設(shè)計者操心。
當(dāng)方法的調(diào)用者(我們的客戶)責(zé)任越少盲厌,他們可能犯的錯誤也就越少署照。
條款19:設(shè)計class猶如設(shè)計type
本條款提醒我們設(shè)計class需要注意的細(xì)節(jié),但并沒有給每一個細(xì)節(jié)提出解決方案吗浩,只是提醒而已建芙。每次設(shè)計class時最好在腦中過一遍以下問題:
- 對象該如何創(chuàng)建銷毀:包括構(gòu)造函數(shù)、析構(gòu)函數(shù)以及new和delete操作符的重構(gòu)需求懂扼。
- 對象的構(gòu)造函數(shù)與賦值行為應(yīng)有何區(qū)別:構(gòu)造函數(shù)和賦值操作符的區(qū)別禁荸,重點(diǎn)在資源管理上。
- 對象被拷貝時應(yīng)考慮的行為:拷貝構(gòu)造函數(shù)阀湿。
- 對象的合法值是什么赶熟?最好在語法層面、至少在編譯前應(yīng)對用戶做出監(jiān)督陷嘴。
- 新的類型是否應(yīng)該復(fù)合某個繼承體系映砖,這就包含虛函數(shù)的覆蓋問題。
- 新類型和已有類型之間的隱式轉(zhuǎn)換問題灾挨,這意味著類型轉(zhuǎn)換函數(shù)和非explicit函數(shù)之間的取舍邑退。
- 新類型是否需要重載操作符竹宋。
- 什么樣的接口應(yīng)當(dāng)暴露在外,而什么樣的技術(shù)應(yīng)當(dāng)封裝在內(nèi)(public和private)
- 新類型的效率地技、資源獲取歸還蜈七、線程安全性和異常安全性如何保證。
- 這個類是否具備template的潛質(zhì)莫矗,如果有的話飒硅,就應(yīng)改為模板類。
條款20:寧以pass-by-reference-to-const替換pass-by-value
函數(shù)接口應(yīng)該以const
引用的形式傳參趣苏,而不應(yīng)該是按值傳參狡相,否則可能會有以下問題:
- 按值傳參涉及大量參數(shù)的復(fù)制,這些副本大多是沒有必要的食磕。
- 如果拷貝構(gòu)造函數(shù)設(shè)計的是深拷貝而非淺拷貝尽棕,那么拷貝的成本將遠(yuǎn)遠(yuǎn)大于拷貝某幾個指針。
對于多態(tài)而言彬伦,將父類設(shè)計成按值傳參滔悉,如果傳入的是子類對象,僅會對子類對象的父類部分進(jìn)行拷貝单绑,即部分拷貝回官,而所有屬于子類的特性將被丟棄,造成不可預(yù)知的錯誤搂橙,同時虛函數(shù)也不會被調(diào)用歉提。 - 小的類型并不意味著按值傳參的成本就會小。首先区转,類型的大小與編譯器的類型和版本有很大關(guān)系苔巨,某些類型在特定編譯器上編譯結(jié)果會比其他編譯器大得多。小的類型也無法保證在日后代碼復(fù)用和重構(gòu)之后废离,其類型始終很小侄泽。
盡管如此,面對內(nèi)置類型和STL的迭代器與函數(shù)對象蜻韭,我們通常還是會選擇按值傳參的方式設(shè)計接口悼尾。
條款21:必須返回對象時,別妄想返回其reference
這個條款的核心觀點(diǎn)在于肖方,不要把返回值寫成引用類型闺魏,作者在條款內(nèi)部詳細(xì)分析了各種可能發(fā)生的錯誤,無論是返回一個stack對象還是heap對象俯画,在這里不再贅述析桥。作者最后的結(jié)論是,如果必須按值返回,那就讓他去吧烹骨,多一次拷貝也是沒辦法的事,最多就是指望著編譯器來優(yōu)化材泄。
但是對于C++11以上的編譯器沮焕,我們可以采用給類型編寫“轉(zhuǎn)移構(gòu)造函數(shù)”以及使用std::move()
函數(shù)更加優(yōu)雅地消除由于拷貝造成的時間和空間的浪費(fèi)。
條款22:將成員變量聲明為private
先說結(jié)論——請對class內(nèi)所有成員變量聲明為private
拉宗,private
意味著對變量的封裝峦树。但本條款提供的更有價值的信息在于不同的屬性控制——public
, private
和protected
——代表的設(shè)計思想。
簡單的來說旦事,把所有成員變量聲明為private的好處有兩點(diǎn)魁巩。首先,所有的變量都是private了姐浮,那么所有的public和protected成員都是函數(shù)了谷遂,用戶在使用的時候也就無需區(qū)分,這就是語法一致性卖鲤;其次肾扰,對變量的封裝意味著,可以盡量減小因類型內(nèi)部改變造成的類外外代碼的必要改動蛋逾。
一旦所有變量都被封裝了起來集晚,外部無法直接獲取,那么所有類的使用者(我們稱為客戶区匣,客戶也可能是未來的自己偷拔,也可能是別人)想利用私有變量實(shí)現(xiàn)自己的業(yè)務(wù)功能時,就必須通過我們留出的接口亏钩,這樣的接口便充當(dāng)了一層緩沖莲绰,將類型內(nèi)部的升級和改動盡可能的對客戶不可見——不可見就是不會產(chǎn)生影響,不會產(chǎn)生影響就不會要求客戶更改類外的代碼铸屉。因此钉蒲,一個設(shè)計良好的類在內(nèi)部產(chǎn)生改動后,對整個項(xiàng)目的影響只應(yīng)是需要重新編輯而無需改動類外部的代碼彻坛。
我們接著說明顷啼,public
和protected
屬性在一定程度上是等價的。一個自定義類型被設(shè)計出來就是供客戶使用的昌屉,那么客戶的使用方法無非是兩種——用這個類創(chuàng)建對象或者繼承這個類以設(shè)計新的類——以下簡稱為第一類客戶和第二類客戶钙蒙。那么從封裝的角度來說,一個public
的成員說明了類的作者決定對類的第一種客戶不封裝此成員间驮,而一個protected
的成員說明了類的作者對類的第二種客戶不封裝此成員躬厌。也就是說,當(dāng)我們把類的兩種客戶一視同仁了以后,public
扛施、protected
和private
三者反應(yīng)的即類設(shè)計者對類成員封裝特性的不同思路——對成員封裝還是不封裝鸿捧,如果不封裝是對第一類客戶不封裝還是對第二類客戶不封裝。
條款23:寧以non-member, non-friend替換member函數(shù)
我寧愿多花一些口舌在這個條款上疙渣,一方面因?yàn)樗娴暮苤匾着硪环矫媸且驗(yàn)樽髡卟]有把這個條款說的很清楚。
在一個類里妄荔,我愿把需要直接訪問private成員的public和protected成員函數(shù)稱為功能顆粒度較低的函數(shù)泼菌,原因很簡單,他們涉及到對private成員的直接訪問啦租,說明他們處于封裝表面的第一道防線哗伯。由若干其他public(或protected)函數(shù)集成而來的public成員函數(shù),我愿稱之為顆粒度高的函數(shù)篷角,因?yàn)樗麄兗闪巳舾深w粒度較低的任務(wù)焊刹,這就是本條款所針對的對象——那些無需直接訪問private成員,而只是若干public函數(shù)集成而來的member函數(shù)内地。本條款告訴我們:這些函數(shù)應(yīng)該盡可能放到類外伴澄。
class WebBrowser { // 一個瀏覽器類
public:?
void clearCache(); // 清理緩存,直接接觸私有成員
void clearHistory(); // 清理歷史記錄阱缓,直接接觸私有成員
void clearCookies(); // 清理cookies非凌,直接接觸私有成員
void clear(); // 顆粒度較高的函數(shù),在內(nèi)部調(diào)用上邊三個函數(shù)荆针,不直接接觸私有成員敞嗡,本條款告訴我們這樣的函數(shù)應(yīng)該移至類外
}
如果高顆粒度函數(shù)設(shè)置為類內(nèi)的成員函數(shù),那么一方面他會破壞類的封裝性航背,另一方面降低了函數(shù)的包裹彈性喉悴。
類的封裝性
封裝的作用是盡可能減小被封裝成員的改變對類外代碼的影響——我們希望類內(nèi)的改變只影響有限的客戶。一個量化某成員封裝性好壞的簡單方法是:看類內(nèi)有多少(public或protected)函數(shù)直接訪問到了這個成員玖媚,這樣的函數(shù)越多箕肃,該成員的封裝性就越差——該成員的改動對類外代碼的影響就可能越大〗衲В回到我們的問題勺像,高顆粒度函數(shù)在設(shè)計之時,設(shè)計者的本意就是它不應(yīng)直接訪問任何私有成員错森,而只是公有成員的簡單集成吟宦,這樣會最大程度維護(hù)封裝性,但很可惜涩维,這樣的愿望并沒有在代碼層面體現(xiàn)出來殃姓。這個類未來的維護(hù)者(有可能是未來的你或別人)很可能忘記了這樣的原始設(shè)定,而在此本應(yīng)成為“高顆粒度”函數(shù)上大肆添加對私有成員的直接訪問,這也就是為什么封裝性可能會被間接損壞了蜗侈。但設(shè)計為非成員函數(shù)就從語法上避免了這種可能性篷牌。函數(shù)的包裹彈性與設(shè)計方法
將高顆粒度函數(shù)提取至類外部可以允許我們從更多維度組織代碼結(jié)構(gòu),并優(yōu)化編譯依賴關(guān)系踏幻。我們用上邊的例子說明什么是“更多維度”娃磺。clear()
函數(shù)是代碼的設(shè)計者最初從瀏覽器的角度對低顆粒度函數(shù)做出的集成,但是如果從“cache”叫倍、“history”、和“cookies”的角度豺瘤,我們又能夠做出其他的集成吆倦。比如將“搜索歷史記錄”和“清理歷史記錄”集成為“定向清理歷史記錄”函數(shù),將“導(dǎo)出緩存”和“清理緩存”集成為“導(dǎo)出并清理緩存”函數(shù)坐求,這時蚕泽,我們在瀏覽器類外做這樣的集成會有更大的自由度。通常利用一些工具類如class CacheUtils
桥嗤、class HistoryUtils
中的static函數(shù)來實(shí)現(xiàn)须妻;又或者采用不同namespace來明確責(zé)任,將不同的高顆粒度函數(shù)和瀏覽器類納入不同namespace和頭文件泛领,當(dāng)我們使用不同功能時就可以include不同的頭文件荒吏,而不用在面對cache的需求時不可避免的將cookies的工具函數(shù)包含進(jìn)來,降低編譯依存性渊鞋。這也是namespace
可以跨文件帶來的好處绰更。
// 頭文件 webbrowser.h 針對class WebBrowserStuff自身
namespace WebBrowserStuff {
class WebBrowser { ... }; //核心機(jī)能
}
// 頭文件 webbrowsercookies.h 針對WebBrowser和cookies相關(guān)的功能
namespace WebBrowserStuff {
... //與cookies相關(guān)的工具函數(shù)
}
// 頭文件 webbrowsercache.h 針對WebBrowser和cache相關(guān)的功能、
namespace WebBrowserStuff {
... //與cache相關(guān)的工具函數(shù)
}
最后要說的是锡宋,本條款討論的是那些不直接接觸私有成員的函數(shù)儡湾,如果你的public(或protected)函數(shù)必須直接訪問私有成員,那請忘掉這個條款执俩,因?yàn)榘涯莻€函數(shù)移到類外所需做的工作就比上述情況遠(yuǎn)大得多了徐钠。
條款24:若所有參數(shù)皆需類型轉(zhuǎn)換,請為此采用non-member函數(shù)
這個條款告訴了我們操作符重載被重載為成員函數(shù)和非成員函數(shù)的區(qū)別役首。作者想給我們提個醒尝丐,如果我們在使用操作符時希望操作符的任意操作數(shù)都可能發(fā)生隱式類型轉(zhuǎn)換,那么應(yīng)該把該操作符重載成非成員函數(shù)宋税。
我們首先說明:如果一個操作符是成員函數(shù)摊崭,那么它的第一個操作數(shù)(即調(diào)用對象)不會發(fā)生隱式類型轉(zhuǎn)換。
首先簡單講解一下當(dāng)操作符被重載成員函數(shù)時杰赛,第一個操作數(shù)特殊的身份呢簸。操作符一旦被設(shè)計為成員函數(shù),它在被使用時的特殊性就顯現(xiàn)出來了——單從表達(dá)式你無法直接看出是類的哪個對象在調(diào)用這個操作符函數(shù),不是嗎根时?例如下方的有理數(shù)類重載的操作符”+”瘦赫,當(dāng)我們在調(diào)用Rational z = x + y;
時,調(diào)用操作符函數(shù)的對象并沒有直接顯示在代碼中——這個操作符的this
指針指向x
還是y
呢蛤迎?
class Rational {
public:
//...
Rational operator+(const Rational rhs) const;
pricate:
//...
}
作為成員函數(shù)的操作符的第一個隱形參數(shù)”this指針
”總是指向第一個操作數(shù)确虱,所以上邊的調(diào)用也可以寫成Rational z = x.operator+(y);
,這就是操作符的更像函數(shù)的調(diào)用方法替裆。那么校辩,做為成員函數(shù)的操作符默認(rèn)操作符的第一個操作數(shù)應(yīng)當(dāng)是正確的類對象——編譯器正式根據(jù)第一個操作數(shù)的類型來確定被調(diào)用的操作符到底屬于哪一個類的。因而第一個操作數(shù)是不會發(fā)生隱式類型轉(zhuǎn)換的辆童,第一個操作數(shù)是什么類型宜咒,它就調(diào)用那個類型對應(yīng)的操作符。
我們舉例說明:當(dāng)Ratinoal
類的構(gòu)造函數(shù)允許int
類型隱式轉(zhuǎn)換為Rational
類型時把鉴,Rational z = x + 2;
是可以通過編譯的故黑,因?yàn)椴僮鞣潜?code>Rational類型的x
調(diào)用,同時將2
隱式轉(zhuǎn)換為Ratinoal
類型庭砍,完成加法场晶。但是Rational z = 2 + x;
卻會引發(fā)編譯器報錯,因?yàn)橛捎诓僮鞣牡谝粋€操作數(shù)不會發(fā)生隱式類型轉(zhuǎn)換怠缸,所以加號“+”實(shí)際上調(diào)用的是2
——一個int
類型的操作符诗轻,因此編譯器會試圖將Rational
類型的x
轉(zhuǎn)為int
,這樣是行不通的揭北。
因此在你編寫諸如加減乘除之類的(但不限于這些)操作符概耻、并假定允許每一個操作數(shù)都發(fā)生隱式類型轉(zhuǎn)換時,請不要把操作符函數(shù)重載為成員函數(shù)罐呼。因?yàn)楫?dāng)?shù)谝粋€操作數(shù)不是正確類型時鞠柄,可能會引發(fā)調(diào)用的失敗。解決方案是嫉柴,請將操作符聲明為類外的非成員函數(shù)厌杜,你可以選擇友元讓操作符內(nèi)的運(yùn)算更便于進(jìn)行,也可以為私有成員封裝更多接口來保證操作符的實(shí)現(xiàn)计螺,這都取決于你的選擇夯尽。
希望這一條款能解釋清楚操作符在作為成員函數(shù)與非成員函數(shù)時的區(qū)別。此條款并沒有明確說明該法則只適用于操作符登馒,但是除了操作符外匙握,我實(shí)在想不到更合理的用途了。
題外話:如果你想禁止隱式類型轉(zhuǎn)換的發(fā)生陈轿,請把你每一個單參數(shù)構(gòu)造函數(shù)后加上關(guān)鍵字explicit
圈纺。
條款25:考慮寫出一個不拋出異常的swap函數(shù)
六秦忿、繼承與面對對象設(shè)計
在設(shè)計一個與繼承有關(guān)的類時,有很多事情需要提前考慮:
- 什么類型的繼承蛾娶?
- 接口是虛函數(shù)還是非虛的灯谣?
- 缺省參數(shù)如何設(shè)計?
想要得到以上問題的合理答案蛔琅,需要考慮的事情就更多了:各種類型的繼承到底意味著什么胎许?虛函數(shù)的本質(zhì)需求是什么?繼承會影響名稱查找嗎罗售?虛函數(shù)是否是必須的呢辜窑?有哪些替代選擇?這些問題都在本章做出解答寨躁。
條款32:確定你的public繼承保證了is-a關(guān)系
public繼承的意思是:子類是一種特殊的父類谬擦,這就是所謂的“is-a”關(guān)系。但是本條款指出了其更深層次的意義:在使用public繼承時朽缎,子類必須涵蓋父類的所有特點(diǎn),必須無條件繼承父類的所有特性和接口谜悟。之所以單獨(dú)指出這一點(diǎn)话肖,是因?yàn)槿绻麊渭兤派罱?jīng)驗(yàn),會犯錯誤葡幸。
比如鴕鳥是不是鳥這個問題最筒,如果我們考慮飛行這一特性(或接口),那么鴕鳥類在繼承中就絕對不能用public繼承鳥類蔚叨,因?yàn)轼r鳥不會飛床蜘,我們要在編譯階段消除調(diào)用飛行接口的可能性;但如果我們關(guān)心的接口是下蛋的話蔑水,按照我們的法則邢锯,鴕鳥類就可以public繼承鳥類。同樣的道理搀别,面對矩形和正方形丹擎,生活經(jīng)驗(yàn)告訴我們正方形是特殊的矩形,但這并不意味著在代碼中二者可以存在public的繼承關(guān)系歇父,矩形具有長和寬兩個變量蒂培,但正方形無法擁有這兩個變量——沒有語法層面可以保證二者永遠(yuǎn)相等,那就不要用public繼承榜苫。
所以在確定是否需要public繼承的時候护戳,我們首先要搞清楚子類是否必須擁有父類每一個特性,如果不是垂睬,則無論生活經(jīng)驗(yàn)是什么媳荒,都不能視作”is-a”的關(guān)系抗悍。public繼承關(guān)系不會使父類的特性或接口在子類中退化,只會使其擴(kuò)充肺樟。
條款33:避免遮掩繼承而來的名稱
這個條款研究的是繼承中多次重載的虛函數(shù)的名稱遮蓋問題檐春,如果在你設(shè)計的類中沒有涉及到對同名虛函數(shù)做多次重載,請忽略本條款么伯。
在父類中疟暖,虛函數(shù)foo()
被重載了兩次,可能是由于參數(shù)類型重載(foo(int)
)田柔,也可能是由于const
屬性重載(foo() const
)俐巴。如果子類僅對父類中的foo()
進(jìn)行了覆寫,那么在子類中父類的另外兩個實(shí)現(xiàn)(foo(int)
,foo() const
)也無法被調(diào)用硬爆,這就是名稱遮蓋問題——名稱在作用域級別的遮蓋是和參數(shù)類型以及是否虛函數(shù)無關(guān)的欣舵,即使子類重載了父類的一個同名,父類的所有同名函數(shù)在子類中都被遮蓋缀磕,個人覺得是比較反直覺的一點(diǎn)缘圈。
如果想要重啟父類中的函數(shù)名稱,需要在子類有此需求的作用域中(可能是某成員函數(shù)中袜蚕,可能是public 或private內(nèi))加上using Base::foo;
糟把,即可把父類作用域匯總的同名函數(shù)拉到目標(biāo)作用域中,需要注意的是牲剃,此時父類中的foo(int)
和foo() const
都會被置為可用遣疯。
如果只想把父類某個在子類中某一個已經(jīng)不可見的同名函數(shù)復(fù)用,可使用inline forwarding function凿傅。
條款34:區(qū)分接口繼承和實(shí)現(xiàn)繼承
我們在條款32討論了public繼承的實(shí)際意義缠犀,我們在本條款將明確在public繼承體系中,不同類型的接口——純虛函數(shù)聪舒、虛函數(shù)和非虛函數(shù)——背后隱藏的設(shè)計邏輯辨液。
首先需要明確的是,成員函數(shù)的接口總是會被繼承箱残,而public繼承保證了室梅,如果某個函數(shù)可施加在父類上,那么他一定能夠被施加在子類上疚宇。不同類型的函數(shù)代表了父類對子類實(shí)現(xiàn)過程中不同的期望亡鼠。
- 在父類中聲明純虛函數(shù),是為了強(qiáng)制子類擁有一個接口敷待,并強(qiáng)制子類提供一份實(shí)現(xiàn)间涵。
- 在父類中聲明虛函數(shù),是為了強(qiáng)制子類擁有一個接口榜揖,并為其提供一份缺省實(shí)現(xiàn)勾哩。
- 在父類中聲明非虛函數(shù)抗蠢,是為了強(qiáng)制子類擁有一個接口以及規(guī)定好的實(shí)現(xiàn),并不允許子類對其做任何更改(條款36要求我們不得覆寫父類的非虛函數(shù))思劳。
在這其中迅矛,有可能出現(xiàn)問題的是普通虛函數(shù),這是因?yàn)楦割惖娜笔?shí)現(xiàn)并不能保證對所有子類都適用潜叛,因而當(dāng)子類忘記實(shí)現(xiàn)某個本應(yīng)有定制版本的虛函數(shù)時秽褒,父類應(yīng)從_代碼層面提醒子類的設(shè)計者做相應(yīng)的檢查_,很可惜威兜,普通虛函數(shù)無法實(shí)現(xiàn)這個功能销斟。一種解決方案是,在父類中為純虛函數(shù)提供一份實(shí)現(xiàn)椒舵,作為需要主動獲取的缺省實(shí)現(xiàn)蚂踊,當(dāng)子類在實(shí)現(xiàn)純虛函數(shù)時,檢查后明確缺省實(shí)現(xiàn)可以復(fù)用笔宿,則只需調(diào)用該缺省實(shí)現(xiàn)即可犁钟,這個主動調(diào)用過程就是在代碼層面提醒子類設(shè)計者去檢查缺省實(shí)現(xiàn)的適用性。
從這里我們可以看出泼橘,將純虛函數(shù)涝动、虛函數(shù)區(qū)分開的并不是在父類有沒有實(shí)現(xiàn)——純虛函數(shù)也可以有實(shí)現(xiàn),其二者本質(zhì)區(qū)別在于父類對子類的要求不同侥加,前者在于從編譯層面提醒子類主動實(shí)現(xiàn)接口,后者則側(cè)重于給予子類自由度對接口做個性化適配粪躬。非虛函數(shù)則沒有給予子類任何自由度担败,而是要求子類堅定的遵循父類的意志,保證所有繼承體系內(nèi)能有其一份實(shí)現(xiàn)镰官。
條款35:考慮virtual函數(shù)以外的其他選擇
條款36:絕不重新定義繼承而來的non-virtual函數(shù)
意思就是提前,如果你的函數(shù)有多態(tài)調(diào)用的需求,一定記得把它設(shè)為虛函數(shù)泳唠,否則在動態(tài)調(diào)用(基類指針指向子類對象)的時候是不會調(diào)用到子類重載過的函數(shù)的狈网,很可能會出錯。
反之同理笨腥,如果一個函數(shù)父類沒有設(shè)置為虛函數(shù)拓哺,你千萬千萬不要在子類重載它,也會犯上邊類似的錯誤脖母。
理由就是士鸥,多態(tài)的動態(tài)調(diào)用中,只有虛函數(shù)是動態(tài)綁定谆级,非虛函數(shù)是靜態(tài)綁定的——指針(或引用)的靜態(tài)類型是什么烤礁,就調(diào)用那個類型的函數(shù)讼积,和動態(tài)類型無關(guān)。
話說回來脚仔,虛函數(shù)的意思是“接口一定被繼承勤众,但實(shí)現(xiàn)可以在子類更改”,而非虛函數(shù)的意思是“接口和實(shí)現(xiàn)都必須被繼承”鲤脏,這就是“虛”的實(shí)際意義们颜。
條款37:絕不重新定義繼承而來的缺省參數(shù)值
這個條款包含雙重意義,在繼承中:
- 不要更改父類非虛函數(shù)的缺省參數(shù)值凑兰,其實(shí)不要重載父類非虛函數(shù)的任何東西掌桩,不要做任何改變!
- 虛函數(shù)不要寫缺省參數(shù)值姑食,子類自然也不要改波岛,虛函數(shù)要從始至終保持沒有缺省參數(shù)值。
第一條在條款36解釋過了音半,第二條的原因在于则拷,缺省參數(shù)值是屬于_靜態(tài)綁定_的,而虛函數(shù)屬于動態(tài)綁定曹鸠。虛函數(shù)在大多數(shù)情況是供動態(tài)調(diào)用煌茬,而在動態(tài)調(diào)用中,子類做出的缺省參數(shù)改變其實(shí)并沒有生效彻桃,反而會引起誤會坛善,讓調(diào)用者誤以為生效了。
缺省參數(shù)值屬于靜態(tài)綁定的原因是為了提高運(yùn)行時效率邻眷。
如果你真的想讓某一個虛函數(shù)在這個類中擁有缺省參數(shù)眠屎,那么就把這個虛函數(shù)設(shè)置成private,在public接口中重制非虛函數(shù)肆饶,讓非虛函數(shù)這個“外殼”擁有缺省參數(shù)值蚯妇,當(dāng)然候学,這個外殼也是一次性的——在被繼承后不要被重載髓绽。
條款38:通過復(fù)合塑膜出has-a關(guān)系撒穷,或“根據(jù)某物實(shí)現(xiàn)出”
兩個類的關(guān)系除了繼承之外,還有“一個類的對象可以作為另一個類的成員”板惑,我們稱這種關(guān)系為“類的復(fù)合”橄镜,這個條款解釋什么情況下我們應(yīng)該用類的復(fù)合。
第一種情況冯乘,非常簡單蛉鹿,說明某一個類“擁有”另一個類對象作為一個屬性,比如學(xué)生擁有鉛筆往湿、市民擁有身份證等妖异,不會出錯惋戏。
第二種情況被討論的更多,即“一個類根據(jù)另一個類實(shí)現(xiàn)”他膳。比如“用stack實(shí)現(xiàn)一個queue”响逢,更復(fù)雜一點(diǎn)的情況可能是“用一個老版本的Google Chrome內(nèi)核去實(shí)現(xiàn)一個紅芯瀏覽器”。
這里重點(diǎn)需要區(qū)分第二種情形和public繼承中提到的”is-a”的關(guān)系棕孙。請牢記“is-a”關(guān)系的唯一判斷法則舔亭,一個類的全部屬性和接口是否必須全部繼承到另一個類當(dāng)中?另一方面蟀俊,“用一個工具類去實(shí)現(xiàn)另一個類”這種情況钦铺,是需要對工具類進(jìn)行隱藏的,比如人們并不關(guān)心你使用stack實(shí)現(xiàn)的queue肢预,所以就藏好所有stack的接口矛洞,只把queue的接口提供給人們用就好了,而紅芯瀏覽器的開發(fā)者自然也不希望人們發(fā)現(xiàn)Google Chrome的內(nèi)核作為底層實(shí)現(xiàn)工具烫映,也需要“藏起來”的行為沼本。
條款39:明智而審慎地使用private繼承
與類的復(fù)合關(guān)系相似,private繼承正是表達(dá)“通過某工具類實(shí)現(xiàn)另一個類”锭沟。那么相似的抽兆,工具類在目標(biāo)類中自然應(yīng)該被隱藏——所有接口和變量都不應(yīng)對外暴露出來。這也解釋了private繼承的內(nèi)涵族淮,它本質(zhì)是一種技術(shù)封裝辫红,和public繼承不同的是,private繼承表達(dá)的是“只有實(shí)現(xiàn)部分被繼承祝辣,而接口部分應(yīng)略去”的思想贴妻。
與private繼承的內(nèi)涵相對應(yīng),在private繼承下揍瑟,父類的所有成員都轉(zhuǎn)為子類私有變量——不提供對外訪問的權(quán)限乍炉,外界也無需關(guān)心子類內(nèi)有關(guān)父類的任何細(xì)節(jié)滤馍。
當(dāng)我們擁有“用一個類去實(shí)現(xiàn)另一個類”的需求的時候,如何在類的復(fù)合與private繼承中做選擇呢巢株?
- 盡可能用復(fù)合,除非必要困檩,不要采用private繼承。
- 當(dāng)我們需要對工具類的某些方法(虛函數(shù))做重載時悼沿,我們應(yīng)選擇private繼承,這些方法一般都是工具類內(nèi)專門為繼承而設(shè)計的調(diào)用或回調(diào)接口糟趾,需要用戶自行定制實(shí)現(xiàn)慌植。
如果使用private繼承,我們無法防止當(dāng)前子類覆寫后的虛函數(shù)被它的子類繼續(xù)覆寫义郑,這種要求類似于對某個接口(函數(shù))加上關(guān)鍵字final一樣蝶柿。為了實(shí)現(xiàn)對目標(biāo)類的方法的防覆寫保護(hù),我們的做法是非驮,在目標(biāo)類中聲明一私有嵌套類交汤,該嵌套類public繼承工具類,并在嵌套類的實(shí)現(xiàn)中覆寫工具類的方法院尔。
class TargetClass { //目標(biāo)類
private:
class ToolHelperClass : public ToolClass { //嵌套類蜻展,public繼承工具類
public:
void someMethod() override; //本應(yīng)被目標(biāo)類覆寫的方法在嵌套類中實(shí)現(xiàn),這樣TargetClass的子類就無法覆寫該方法邀摆。
}
}
如此一來纵顾,目標(biāo)類的子類就無法再次覆寫我們想要保護(hù)的核心方法。
條款40:明智而審慎地使用多繼承
原則上不提倡使用多繼承栋盹,因?yàn)槎嗬^承可能會引起多父類共用父類施逾,導(dǎo)致在底層子類中出現(xiàn)多余一份的共同祖先類的拷貝。為了避免這個問題C++引入了虛繼承例获,但是虛繼承會使子類對象變大汉额,同時使成員數(shù)據(jù)訪問速度變慢,這些都是虛繼承應(yīng)該付出的代價榨汤。
在不得不使用多繼承時蠕搜,請慎重地設(shè)計類別,盡量不要出現(xiàn)菱形多重繼承結(jié)構(gòu)(“B收壕、C類繼承自A類妓灌,D類又繼承自B、C類”)蜜宪,即盡可能地避免虛繼承虫埂,一個完好的多繼承結(jié)構(gòu)不應(yīng)在事后被修改。虛基類中應(yīng)盡可能避免存放數(shù)據(jù)掉伏。