前言
這本書是之前京東做活動(dòng)買的很多本書中的一本(主要閱讀時(shí)間是周末裸扶、每天早上起來吃早餐的時(shí)候,以及下班回來時(shí)候优床,前后花了一個(gè)月左右吧)蛤售。
C++ 只是在大一下的時(shí)候大量使用過,之后就沒怎么用過 C++ 了喂江。起初以為看這本書可能比較吃力召锈,因?yàn)榍把哉f目標(biāo)讀者是 C++ 有經(jīng)驗(yàn)的人。但是看的時(shí)候感覺沒那么難懂获询,基本上寫到的東西都能理解涨岁。
此書應(yīng)該是上世紀(jì)末成書的,書中的內(nèi)容可能有些過時(shí)了(比如內(nèi)存布局)吉嚣,但是仍然是值得一看的書梢薪。總體上感覺就是:為了程序員寫代碼爽尝哆,同時(shí)又為了保證程序語意等的正確和執(zhí)行期的效率秉撇,編譯器為我們做了很多事情。
這里我對(duì)每一章內(nèi)容都寫一下我的一些總結(jié)和想法秋泄,由于是看完全書以后才寫的琐馆,前面很多內(nèi)容其實(shí)有些忘了(??),盡量憑回憶和粗略的回看總結(jié)一下恒序。瘦麸。。
關(guān)于對(duì)象
本書第一章講的是對(duì)象歧胁。
c++ 的對(duì)象(類)使用其實(shí)是非常高效的滋饲,這里的高效指的是:對(duì)于 C,C++ 大部分簡單類的屬性存取操作其實(shí)是和 C 一樣高效的喊巍。其他語言沒有 C++ 高效有一部分原因是其存取可能涉及到很多間接內(nèi)存操作屠缭,而 C/C++ 大部分存取操作可能只會(huì)涉及到一次內(nèi)存讀寫。
C++ 帶來比 C 低效的大部分原因是要支持 virtual 機(jī)制:
- 虛函數(shù)
- 虛繼承
什么是對(duì)象模型玄糟?我的理解是語言本身是如何處理類的內(nèi)存分布的策略勿她,由于現(xiàn)在大部分語言支持 OOP 編程,該策略一個(gè)關(guān)注點(diǎn)就是如何實(shí)現(xiàn)繼承阵翎,還有語言本身的其他涉及到內(nèi)存分布的特性逢并,比如虛函數(shù)、虛繼承郭卫、多重繼承砍聊、RTTI 的支持等。
書中列舉了幾個(gè)可能的對(duì)象模型:
- 簡單對(duì)象模型
- 表格驅(qū)動(dòng)對(duì)象模型
- C++對(duì)象模型(被具體實(shí)現(xiàn)的模型)
關(guān)于對(duì)象模型優(yōu)劣的角度主要有:
- 間接性是否足夠少(越少內(nèi)存訪問越快贰军,帶來的問題是靈活性不足)
- 對(duì)象的可拓展性(這是我自己加的玻蝌,主要指的是蟹肘,如果基類發(fā)生變化,子類是否需要重新編譯)
構(gòu)造函數(shù)語意學(xué)
這一章講的是 C++ 的構(gòu)造函數(shù)相關(guān)內(nèi)容俯树。
默認(rèn)構(gòu)造函數(shù)帘腹。
一個(gè) C++ 類程序員可以不定義其構(gòu)造函數(shù),但是在有必要的情況下许饿,編譯器會(huì)自動(dòng)幫助我們合成一個(gè)構(gòu)造函數(shù)阳欲,雖然有時(shí)候這個(gè)構(gòu)造函數(shù)可能并沒有什么用。
為什么默認(rèn)構(gòu)造函數(shù)是必須的(如果程序員不顯式的定義一個(gè)構(gòu)造函數(shù))陋率?
- 當(dāng)當(dāng)前類定義包含一個(gè)成員對(duì)象球化,該成員對(duì)象有一個(gè)默認(rèn)構(gòu)造函數(shù),那么編譯器會(huì)為當(dāng)前類合成一個(gè)默認(rèn)構(gòu)造函數(shù)瓦糟,然后在該函數(shù)內(nèi)部調(diào)用成員對(duì)象的默認(rèn)構(gòu)造函數(shù)筒愚。
- 當(dāng)當(dāng)前定義的類是另一個(gè)類的子類,父類有一個(gè)默認(rèn)構(gòu)造函數(shù)菩浙,那么編譯器也會(huì)生成一個(gè)默認(rèn)構(gòu)造函數(shù)巢掺,在該函數(shù)內(nèi)部調(diào)用父類的默認(rèn)構(gòu)造函數(shù)。
- 當(dāng)當(dāng)前類帶有虛函數(shù)的時(shí)候芍耘,編譯器需要設(shè)定好虛函數(shù)表和虛函數(shù)表指針址遇。
- 當(dāng)當(dāng)前類虛繼承于另外一個(gè)類。
如果不是上述這四種情況斋竞,是否合成默認(rèn)構(gòu)造函數(shù)是可有可無的倔约。默認(rèn)構(gòu)造函數(shù)的合成是出于編譯器的角度。而且默認(rèn)構(gòu)造函數(shù)中編譯器關(guān)注的內(nèi)容主要是成員對(duì)象的初始化坝初,內(nèi)建類型(也就是基本類型)的初始化是程序員的責(zé)任浸剩。
拷貝構(gòu)造函數(shù)
有三種情況需要拷貝構(gòu)造函數(shù):
- 顯式的將一個(gè)對(duì)象賦值給另一個(gè)對(duì)象
- 將對(duì)象作為參數(shù)傳遞
- 將對(duì)象作為函數(shù)返回值返回
程序員可以不定義或者定義一個(gè)拷貝構(gòu)造函數(shù),當(dāng)不定義的時(shí)候鳄袍,默認(rèn)的拷貝構(gòu)造函數(shù)行為是按成員從當(dāng)前對(duì)象拷貝到另一個(gè)對(duì)象上的绢要。如果某個(gè)成員帶有一個(gè)顯式定義的拷貝構(gòu)造函數(shù)的話,編譯器就需要為我們合成一個(gè)拷貝構(gòu)造函數(shù)拗小,在函數(shù)內(nèi)部調(diào)用該成員的顯式拷貝構(gòu)造函數(shù)重罪。否則,編譯器就不需要為我們合成拷貝構(gòu)造函數(shù)哀九。
所以什么情況下編譯器必須為我們合成一個(gè)拷貝構(gòu)造函數(shù)剿配?(如果程序員不顯式的定義一個(gè)拷貝構(gòu)造函數(shù))
- 當(dāng)前類中某個(gè)成員對(duì)象定義(或者編譯器為它合成)了一個(gè)拷貝構(gòu)造函數(shù)
- 當(dāng)前類繼承的父類定義了一個(gè)拷貝構(gòu)造函數(shù)
- 當(dāng)前類有虛函數(shù)
- 當(dāng)前類虛繼承與另外一個(gè)類
1和2比較簡單,編譯器簡單的將成員對(duì)象和父類的拷貝構(gòu)造函數(shù)插入到合成的拷貝構(gòu)造函數(shù)中即可阅束。
對(duì)于3呼胚,當(dāng)將一個(gè)子類賦值給父類的時(shí)候,如果父類含有虛函數(shù)息裸,那么不能簡單的將子類的 vptr 賦值給父類對(duì)象蝇更,否則當(dāng)父類對(duì)象虛函數(shù)調(diào)用的時(shí)候會(huì)執(zhí)行子類的虛函數(shù)(這里沒有多態(tài))沪编。所以編譯器合成的拷貝構(gòu)造函數(shù)要合理的設(shè)置 vptr 為父類的 vtable。(即使在程序員顯式的定義了一個(gè)拷貝構(gòu)造函數(shù)年扩,vptr 的指定也是靠編譯器來完成蚁廓,因?yàn)?vptr 對(duì)于程序員是透明的)
對(duì)于4,由于虛繼承的特殊性常遂,編譯器要很仔細(xì)的幫助我們處理子類對(duì)象賦值給父類對(duì)象這種情況纳令,以便合理的設(shè)置父類對(duì)象的某些值。
程序轉(zhuǎn)換語意學(xué)
這一部分主要是為了告訴我們克胳,在用到構(gòu)造函數(shù)的地方,編譯器為我們的代碼做的轉(zhuǎn)換圈匆,主要是確保默認(rèn)構(gòu)造函數(shù)漠另、拷貝構(gòu)造函數(shù)或者顯式定義的構(gòu)造函數(shù)們被正確的調(diào)用。主要從函數(shù)返回值跃赚、函數(shù)參數(shù)笆搓、初始化等幾個(gè)角度討論。
接著討論如何優(yōu)化編譯器的轉(zhuǎn)換代碼纬傲。比如 NRV 優(yōu)化满败,主要是為了盡量避免多余的構(gòu)造函數(shù)的調(diào)用。
成員對(duì)象的初始化列表
為了避免構(gòu)造函數(shù)中多余的成員對(duì)象和臨時(shí)對(duì)象的構(gòu)造函數(shù)的調(diào)用叹括,一種比較好的方法是使用初始化列表算墨。但是要注意,初始化列表中的初始化順序不是按照程序員寫的順序執(zhí)行的汁雷,而是按照成員被聲明的順序初始化的净嘀。
成員初始化列表最終會(huì)被編譯器所轉(zhuǎn)換,注入到用戶定義的構(gòu)造函數(shù)的代碼內(nèi)容的最前面侠讯。
Data 語意學(xué)
這一章講解的是類中的數(shù)據(jù)挖藏。一個(gè)類的大小可以使用 sizeof
來取得。決定類的大小有三個(gè)因素:
- 語言本身的額外負(fù)擔(dān)厢漩,比如為了支持虛函數(shù)調(diào)用機(jī)制膜眠,類的定義中會(huì)被安插一個(gè) vptr 指針。
- 某些特殊情況下編譯器的優(yōu)化溜嗜。比如空的虛繼承基類的大小可能會(huì)反應(yīng)到子類上宵膨。
- 內(nèi)存對(duì)齊的需要
類數(shù)據(jù)成員的綁定
類數(shù)據(jù)成員在類的成員函數(shù)中使用似乎不應(yīng)該存在太大問題。但是在早期的編譯器中卻是有問題的粱胜。比如類的成員函數(shù)在類數(shù)據(jù)成員定義之前用到了該數(shù)據(jù)成員柄驻,如果剛好在類定義外部有這么一個(gè)數(shù)據(jù),那么編譯器就要決議應(yīng)該使用的是類內(nèi)部的數(shù)據(jù)成員而不是外部的變量焙压。
數(shù)據(jù)成員的布局
類的靜態(tài)數(shù)據(jù)成員是被存放在類外部的鸿脓,不會(huì)影響到對(duì)象的內(nèi)存布局抑钟。非靜態(tài)數(shù)據(jù)成員的內(nèi)存布局依賴各個(gè)編譯器具體的實(shí)現(xiàn),C++ 標(biāo)準(zhǔn)并不加以限制野哭。類的另外一些對(duì)程序員透明的成員在塔,比如 vptr,它的位置是哪兒呢拨黔?編譯器可以放在類定義的最前端或者最后面蛔溃。編譯器也可以合理的組織不同訪問權(quán)限的(public、private篱蝇、protect)成員在各自的區(qū)域贺待。
數(shù)據(jù)成員的訪問
首先是靜態(tài)數(shù)據(jù)成員的訪問。由于靜態(tài)數(shù)據(jù)成員是存儲(chǔ)在程序的數(shù)據(jù)段中零截,無論是通過指針還是對(duì)象來訪問麸塞,這兩種方式訪問沒有任何效率或者其他實(shí)質(zhì)的區(qū)別。
如果有兩個(gè)類含有相同名字的靜態(tài)數(shù)據(jù)成員涧衙,由于靜態(tài)數(shù)據(jù)是存在數(shù)據(jù)段中的哪工,為了防止名字沖突,編譯器會(huì)進(jìn)行名字處理弧哎,也就是 name-mangling雁比。
其次是非靜態(tài)數(shù)據(jù)的訪問。在成員函數(shù)中訪問成員函數(shù)撤嫩,編譯器其實(shí)會(huì)做代碼上的一些轉(zhuǎn)換偎捎,將當(dāng)前this 對(duì)象作為函數(shù)的第一個(gè)參數(shù),函數(shù)中對(duì)成員數(shù)據(jù)的訪問是通過改 this 指針來完成的非洲。
繼承和數(shù)據(jù)成員
一般來說鸭限,子類的數(shù)據(jù)成員會(huì)被定義在父類的下面(當(dāng)然,這方面沒有絕對(duì)的要求)
在這一小節(jié)两踏,作者分兩個(gè)角度討論:
- 一個(gè)是繼承無多態(tài)
- 含多態(tài)的繼承
- 多重繼承
- 虛繼承
對(duì)于1败京,在編譯器設(shè)計(jì)實(shí)現(xiàn)的時(shí)候需要特別注意一個(gè)問題,就是對(duì)于為了內(nèi)存對(duì)齊所額外使用的空間的使用梦染。父類存在為了內(nèi)存對(duì)齊而多占用了幾個(gè)字節(jié)大小赡麦,如果子類是緊隨在父類之后的,并且為了節(jié)省空間考慮而將自己的數(shù)據(jù)成員填充到父類的內(nèi)存對(duì)齊的幾個(gè)字節(jié)中帕识,那么泛粹,在將一個(gè)父類對(duì)象賦值給子類對(duì)象的時(shí)候,父類對(duì)象的內(nèi)存對(duì)齊字節(jié)會(huì)覆蓋了子類對(duì)象的起始數(shù)據(jù)成員肮疗,這顯然是錯(cuò)誤的晶姊。
對(duì)于2,含多態(tài)的類的關(guān)注點(diǎn)是伪货,將 vptr 放在類對(duì)象的哪兒呢们衙?可以是開始钾怔,也可以是最后。
對(duì)于3蒙挑,多重繼承要關(guān)注的是如何正確的處理派生類實(shí)例和其第二個(gè)父類的轉(zhuǎn)換宗侦。答案是通過偏移量來處理。
虛擬繼承要解決的問題是多重繼承中可能存在的菱形繼承問題忆蚀。虛擬繼承的實(shí)現(xiàn)有兩種方式:
- 指針策略
- 虛函數(shù)表偏移策略
1是指矾利,通過附加一個(gè)每個(gè)虛擬繼承子類添加一個(gè)指向共享部分的一個(gè)指針。2是指馋袜,在虛函數(shù)表中放置虛擬繼承基類的偏移量
指向數(shù)據(jù)成員的指針
當(dāng)需要了解類中成員對(duì)象的底層內(nèi)存布局的時(shí)候男旗,使用這類指針會(huì)特別有用,因?yàn)檫@類指針取到的就是該數(shù)據(jù)成員在類模型中的偏移量欣鳖。
為了區(qū)分“沒有指向任何數(shù)據(jù)成員” 和指向第一個(gè)數(shù)據(jù)成員這兩種情況剑肯,所有的指針都會(huì)被加1,在具體使用的時(shí)候需要減1
函數(shù)語意學(xué)
非靜態(tài)成員函數(shù)
非靜態(tài)成員函數(shù)是如何實(shí)現(xiàn)的呢观堂?
- 編譯器會(huì)改寫該成員函數(shù)的簽名,安插一個(gè)額外的參數(shù)到函數(shù)中呀忧,也就是 this 指針师痕。
- 所有對(duì)非靜態(tài)成員數(shù)據(jù)的訪問都是通過 this 指針來完成的
- 該非靜態(tài)成員函數(shù)的名字會(huì)被 name mangling,使得它的名字在程序中是獨(dú)一無二的而账,又能反應(yīng)函數(shù)的簽名胰坟、所屬類等的信息
虛擬成員函數(shù)
虛擬成員函數(shù)是如何實(shí)現(xiàn)的呢?
- 每一個(gè)類對(duì)象中會(huì)又一個(gè) vptr 指針泞辐,用來指向存儲(chǔ)虛函數(shù)列表笔横。由于繼承體系的復(fù)雜,vptr 指針很有可能被 mangling
- 對(duì)虛函數(shù)的調(diào)用是通過 vptr 指針咐吼,和其中虛函數(shù)列表中的索引值來的吹缔。
- 在虛函數(shù)第一個(gè)參數(shù)位置傳遞調(diào)用者指針,也就是 this 指針
- 在單一繼承體系中锯茄,每一個(gè)類對(duì)象如果有實(shí)現(xiàn)一個(gè)虛函數(shù)厢塘,則會(huì)重寫其 vptr 相應(yīng)索引值的來自直接基類的函數(shù)指針,使其指向自己的實(shí)現(xiàn)肌幽。
- 在多重繼承體系中晚碾,虛擬函數(shù)的實(shí)現(xiàn)比較復(fù)雜。一種是調(diào)整 this 指針喂急,缺點(diǎn)是效率底下格嘁,解決方法是使用所謂的 thunk 函數(shù)。比較好的解決方法是用派生類中實(shí)現(xiàn)的虛函數(shù)指針重寫基類們的 vptr 對(duì)于索引值的值廊移。
靜態(tài)成員函數(shù)
靜態(tài)成員函數(shù)是如何實(shí)現(xiàn)的糕簿?
一個(gè)非靜態(tài)成員函數(shù)如果沒有用到非靜態(tài)成員變量的話探入,其實(shí)沒有必要讓它通過類實(shí)例來調(diào)用。但是如果類中存在一個(gè) nonpublic 的靜態(tài)成員函數(shù)冶伞,那么類必須提供成員函數(shù)來訪問它新症。解決之道是引入靜態(tài)成員函數(shù)。
靜態(tài)成員函數(shù)最大特點(diǎn)是沒有 this 指針响禽,所以它又如下特點(diǎn):
- 不能直接訪問類中的非靜態(tài)成員
- 不能被聲明為 const徒爹、 volatile 或者 virtual
- 不需要通過類實(shí)例來調(diào)用(雖然語法上可以,但是最終轉(zhuǎn)換的代碼并不需要類的實(shí)例)
靜態(tài)成員函數(shù)由于沒有 this 指針芋类,看起來很像是非成員函數(shù)隆嗅。
成員函數(shù)指針
- 非虛擬成員函數(shù)的指針實(shí)際上是函數(shù)在內(nèi)存中的指針
- 虛函數(shù)指針實(shí)際上是虛函數(shù)在虛函數(shù)表中的索引值
- 多繼承下的成員函數(shù)指針是一個(gè)結(jié)構(gòu)體,index侯繁、 faddr 和 delta胖喳。如果是非虛函數(shù),則 index 為 -1贮竟,否則 index 指的是虛函數(shù)表中的索引值丽焊。faddr 是非虛擬成員函數(shù)的內(nèi)存地址,delta 是一個(gè)可能的 this 指針調(diào)整值咕别。
內(nèi)斂函數(shù)
處理內(nèi)斂函數(shù)有兩個(gè)步驟:
- 根據(jù)復(fù)雜度分析函數(shù)是否可以被內(nèi)斂技健,若不能,則被轉(zhuǎn)換成靜態(tài)函數(shù)惰拱。
- 內(nèi)斂函數(shù)的擴(kuò)展是在調(diào)用的點(diǎn)上的雌贱。
內(nèi)斂函數(shù)的擴(kuò)展會(huì)帶來兩個(gè)問題:
- 參數(shù)求值
- 臨時(shí)對(duì)象管理
臨時(shí)對(duì)象管理一個(gè)可能的情況是內(nèi)斂函數(shù)的參數(shù)是另一個(gè)函數(shù)調(diào)用返回的值(基本類型或者類類型)。編譯器要正確的處理臨時(shí)對(duì)象的釋放問題偿短。
內(nèi)斂函數(shù)是 C 中的 #define 的一個(gè)安全替代欣孤,特別是宏有它的負(fù)作用。但是內(nèi)斂函數(shù)被使用太多的話昔逗,會(huì)產(chǎn)生大量的代碼降传,使得程序代碼增大。同時(shí)內(nèi)斂函數(shù)要管理可能產(chǎn)生的臨時(shí)對(duì)象纤子,還有就是內(nèi)斂嵌套內(nèi)斂這種復(fù)雜的情況搬瑰。所以內(nèi)斂函數(shù)有其優(yōu)點(diǎn)但是要小心使用。
構(gòu)造控硼、析構(gòu)泽论、拷貝語意學(xué)
未完待續(xù)。卡乾。翼悴。