Inside The C++ Object Model 讀書筆記

c++主要有兩個復(fù)雜的地方康谆,一個是實現(xiàn)OO的對象模型领斥,一個是一開始被設(shè)計為小刀但后來發(fā)現(xiàn)是屠龍刀的模板。

OK沃暗,模板元編程不搞也能用c++寫東西(雖然我想用boost 和 STL 的人很難容忍在一點都不了解相關(guān)實現(xiàn)的情況下去使用那些對于傳統(tǒng)c++來說就像魔法一樣的代碼)月洛。

但是不學(xué)對象模型,用c++來寫OO代碼就沒意義了孽锥。因為用c++是為了效率嚼黔,而編譯器在背后干了太多事情,如果不知道一點點c++的OO實現(xiàn)忱叭,代碼跑起來可能會慢(例如重復(fù)構(gòu)造臨時對象)隔崎。Inside The c++ Object Model 這本書從出版到現(xiàn)在剛好二十年,其中不少內(nèi)容可能會過時(特別是設(shè)計具體編譯器的部分)韵丑。但是其中一些基本而且有效的模型估計還是不會變的,其中一些細(xì)節(jié)我也不寫(虛擬繼承轉(zhuǎn)換父子類指針的時候怎么去算offset我真的懶得管了)虚缎。還有這篇東西主要作用還是整理思路撵彻,很多本來因該畫圖來表達(dá)的東西就不搞了钓株,反正這篇東西主要還是給自己整理,方便回憶陌僵,老師大概也不怎么看(上一篇過了半年都還沒看)轴合。

這本書除了第一章和最后一章,標(biāo)題都含semantic(語義)碗短。之前一直不理解是啥意思∈芨穑現(xiàn)在的理解是,semantic就是編程語言內(nèi)置的邏輯功能偎谁,例如一個scope中的對象如果有析構(gòu)函數(shù)的話总滩,那么他一定會在scope結(jié)束的時候調(diào)用析構(gòu)函數(shù)。這本書實際上就是講c++中語言內(nèi)置的邏輯功能究竟是如何在c語言的基礎(chǔ)上實現(xiàn)的巡雨。
構(gòu)造函數(shù)

并不是每一個對象都擁有構(gòu)造函數(shù)闰渔。在程序員沒有寫構(gòu)造函數(shù)的情況下,類只有在三種情況下才會為對象生成構(gòu)造函數(shù)铐望,而且這個類的對象被定義才會調(diào)用冈涧。

1:成員對象或基類有無參構(gòu)造函數(shù)(或者說default constructor)。因為C++里面一個類的的基類和成員對象的構(gòu)造函數(shù)必須由該類的構(gòu)造函數(shù)調(diào)用正蛙,所以這個類必須生成一個構(gòu)造函數(shù)來放置基類和成員對象的構(gòu)造函數(shù)調(diào)用代碼督弓。

2:自己或者基類有虛函數(shù)。因為虛函數(shù)的實現(xiàn)方法是在每個對象的在內(nèi)容中的區(qū)域里面插入一個vptr(指向虛函數(shù)表的指針)來實現(xiàn)虛函數(shù)乒验,所以安置vptr的代碼就被放在構(gòu)造函數(shù)里面愚隧。因此如果一個類使用了虛函數(shù),則編譯器會保證這個類有構(gòu)造函數(shù)徊件。

3:有virtual base class奸攻。存取對象成員變量時,靠的是對象在內(nèi)存中的起始地址和成員變量在整個對象內(nèi)存區(qū)域中的位置來實現(xiàn)的虱痕。但由于virtual base class的作為subobject在子類對象中的區(qū)域是會改變的(如果這個使用了虛擬繼承的子類的子類使用多重繼承睹耐,而且該使用了虛擬繼承的類的對象在子類的內(nèi)存區(qū)域中不是排頭),那么部翘,當(dāng)用被虛擬集成的基類指針來操作它自己的成員時硝训,由于在編譯期間不能這個指針究竟是指向子類還是子類的子類,這個指針就不知道基類數(shù)據(jù)成員的位置新思,所以存取操作必須延遲到運行時期才能確定具體找那一塊內(nèi)存窖梁。具體的行為是,使用了虛擬繼承的子類會在自己的內(nèi)存區(qū)域里面加一個指針來指向虛擬繼承得來的基類(微軟用的是vpbc夹囚,其它不清楚)纵刘。所以,如使用了虛函數(shù)一樣荸哟,使用了虛擬繼承的子類需要構(gòu)造函數(shù)來調(diào)用安置這個指針的代碼假哎。

但是瞬捕,使用了虛函數(shù)或者多重繼承的子對象內(nèi)存布局也和基類不一樣啊舵抹?當(dāng)vptr是放在對象開頭(微軟和g++都是這么干的)肪虎,而原本基類沒有使用虛函數(shù),那基類的成員不再從開頭開始排下來惧蛹,而是要放在vptr之后扇救。而使用了多重繼承的子類,它只有一個基類的成員能放開頭香嗓,其它基類的成員必須跟在后面迅腔。這兩種情況都會使基類和子類的內(nèi)存布局不一樣。但是陶缺,這個不同實際上在子類指針upcast的時候就已經(jīng)被解決了钾挟。

Base * pb = new Derived;

假設(shè)Base沒有虛函數(shù),而Derived有饱岸。這個語句其實在編譯期干了很多事情掺出。new出來的子類指針會先檢查自己是不是0,不是0的話它會加上一個offset苫费,讓子類指針指向內(nèi)存區(qū)域中基類成員的那部分汤锨,再傳給pb。由于基類沒有虛函數(shù)百框,只用這塊區(qū)域的內(nèi)存就能表現(xiàn)出基類完整的行為闲礼,因為它的行為只跟自己的成員變量有關(guān),所以這樣轉(zhuǎn)換后pb使用起來是安全的铐维。而多重繼承也是這么干的柬泽,也是在編譯期加一個offset。但是這樣的話嫁蛇,這個不是位于內(nèi)存開頭的基類subobject的行為是能夠完整嗎锨并?畢竟如果它有虛函數(shù)的話,基類指針就會訪問子類其它區(qū)域的內(nèi)存睬棚!沒錯第煮!這里是會有問題的!所以在調(diào)用虛函數(shù)的時候抑党,還要把作為虛函數(shù)參數(shù)的this指針轉(zhuǎn)回去包警!具體怎么轉(zhuǎn)已經(jīng)沒有不好玩了,它依賴編譯器底靠,而且知不知道也沒啥關(guān)系害晦。(展開了好多,現(xiàn)在回到正題)

如果不符合以上的情況暑中,則類的構(gòu)造函數(shù)是trivial的篱瞎,實際上不會被合成出來苟呐。聲明一個對象只會分配內(nèi)存痒芝,不會調(diào)用構(gòu)造函數(shù)俐筋。當(dāng)類符合上述情況,則構(gòu)造函數(shù)是nontrivial严衬,構(gòu)造函數(shù)會在對象聲明的那個編譯單元(vc里面的obj文件)里面被調(diào)用澄者,為了防止被編譯器生成的構(gòu)造函數(shù)在多個編譯單元中重復(fù)定義,這些構(gòu)造函數(shù)都要是internal linkage请琳,要么是inline粱挡,要么是explicit non-inline static。

如果程序員自己寫了構(gòu)造函數(shù)俄精,上述三點所需要的代碼都會被插入到程序員所定義的構(gòu)造函數(shù)里面询筏。

拷貝構(gòu)造函數(shù)

當(dāng)程序員沒有定義拷貝構(gòu)造函數(shù)。

如果對象的成員都是C++內(nèi)置成員竖慧,那么它是Plain OI' Data,它的拷貝是bitwise copy semantics嫌套,拷貝構(gòu)造函數(shù)是trivial的,實際上不會被合成圾旨。

會被合成的有一下三種情況(而且必須存在相關(guān)的拷貝對象的代碼踱讨,因為既然沒有拷貝代碼那拷貝構(gòu)造函數(shù)就沒必要了):

1:如果對象的成員對象或者基類有拷貝構(gòu)造函數(shù)(無論是程序員寫的還是編譯器合成的),那么這個對象就會被編譯器合成一個拷貝構(gòu)造函數(shù)砍的,來調(diào)用成員對象或者基類的拷貝構(gòu)造函數(shù)痹筛。

2:如果對象有虛函數(shù),而且存在用子類給基類賦值的代碼廓鞠。因為如果基類有虛函數(shù)帚稠,要重置虛函數(shù)表指針,把子類的vptr換成基類的vptr床佳。如果基類沒有虛函數(shù)滋早,要把虛函數(shù)那部分砍掉(這種情況貌似書沒說)。

3:使用了虛擬繼承夕土。因為子類布局跟基類不一樣馆衔,所以需要拷貝構(gòu)造函數(shù)來維護。具體怎么維護也算了怨绣,沒必要角溃。

需要用到上面三點功能,而程序員又定義了拷貝構(gòu)造函數(shù)篮撑,那么相關(guān)代碼會插入到拷貝構(gòu)造函數(shù)里面

析構(gòu)函數(shù)

當(dāng)程序員沒有定義析構(gòu)函數(shù)减细,只有在它的成員對象或者基類有析構(gòu)函數(shù),編譯器才會自動生成析構(gòu)函數(shù)赢笨。但是當(dāng)對象有虛函數(shù)的時候未蝌,沒有析構(gòu)函數(shù)重置vptr沒問題嗎驮吱?沒問題,因為既然基類沒有析構(gòu)函數(shù)左冬,那自從子類對象析構(gòu)開始,就沒有人會去調(diào)用虛函數(shù)了拇砰,所以還管它干什么。但是如果基類有析構(gòu)函數(shù)除破,那么子類的析構(gòu)函數(shù)必須重置vptr(當(dāng)然這是編譯器干的)。

賦值操作符

下面三種情況琼腔,賦值操作符是non-trivial的:

1:成員對象或者基類重載了賦值操作符

2:使用了虛函數(shù)瑰枫。道理跟拷貝構(gòu)造函數(shù)一樣丹莲。

3:使用了虛擬繼承。道理跟拷貝構(gòu)造函數(shù)一樣圾笨。

其它情況都死bitwise copy教馆。如果程序員定義了拷貝構(gòu)造函數(shù),由于賦值操作符沒有初始化列表擂达,所以成員對象和基類的賦值操作符必需由程序員調(diào)用土铺。

初始化列表

只有初始化列表才能直接構(gòu)造成員對象和基類板鬓。如果是在構(gòu)造函數(shù)的代碼里面初始化它們,就已經(jīng)太遲了俭令,因為這個時候編譯器保證成員對象和基類已經(jīng)調(diào)用了一次構(gòu)造函數(shù)。所以如果是在構(gòu)造函數(shù)的代碼塊里面用賦值操作符再設(shè)定它們瓢湃,就低效赫蛇。

函數(shù)中的轉(zhuǎn)換

1.對象定義會變成對象聲明加調(diào)用構(gòu)造函數(shù)調(diào)用绵患。

2.對象使用等好會變成拷貝構(gòu)造函數(shù)或者重載的賦值操作符悟耘。

3.值傳遞對象會變成先拷貝臨時對象,然后把臨時對象的引用傳給函數(shù)(thinking in c++里面說這個拷貝過程會比較特殊筏勒,會有個helper function幫助拷貝,而且這個臨時對象是建在被調(diào)用函數(shù)的棧上面管行。可能是時代不同編譯器改了吧)

  1. 以值返回對象的時候病瞳,實際上是把對象的引用(指針)作為一個外增參數(shù)傳進(jìn)函數(shù),然后將返回的對象拷貝到這個外增參數(shù)。如果return 語句是直接return 一個構(gòu)造函數(shù)(而且這個構(gòu)造函數(shù)是nontrivial的设易,如果是trivial就要程序員定義它才能優(yōu)化,雖然這個限制我有點懷疑),則執(zhí)行NRV優(yōu)化戏溺。直接用構(gòu)造函數(shù)構(gòu)造被被返回的對象屠尊。

對象布局與存取

對象的數(shù)據(jù)成員首先會按access section(public,private讼昆,protected)被分類,然后分別在內(nèi)存中按聲明順序排列闰围,靜態(tài)成員之會被放在全局?jǐn)?shù)據(jù)區(qū)既峡。非virtual的成員函數(shù)由于跟對象的綁定可以在編譯期完成羡榴,所以不會被放在對象內(nèi)存中运敢。virtual函數(shù)會被統(tǒng)一放在類的虛函數(shù)表里面,對象會被插入一個指向類的虛函數(shù)表的指針迄沫。

非虛擬繼承得來的基類的成員會被放在子類成員的前面涉枫。虛擬繼承的來的成員會放在后面,然后在子類內(nèi)存區(qū)域的最前面安插一個指向虛擬基類的指針,或者像vc那樣插入一個指向虛擬基類列表的vpbc乐纸。這是因為虛擬繼承是用來應(yīng)對在多重繼承下摇予,兩個以上基類有公共基類的情況。這個時候多重繼承的子類會重復(fù)擁有這個基類的成員侧戴。

如果對象是空的話,會被安插一個char积仗,所以空對象的大小是1byte蜕猫。如果對象沒有對象寂曹,但有虛函數(shù)或者用了虛擬繼承回右,vc和g++都會把那一個char省略掉,因為vptr已經(jīng)可以占用內(nèi)存了渺氧,即使這個類是繼承于一個沒有虛函數(shù)的空類也一樣蹬屹。

如果基類的數(shù)據(jù)成員加起來的大小不是4byte的倍數(shù),那子類繼承之后哩治,會先把基類內(nèi)存用空白補到4的倍數(shù)(alignment),然后在再后面放子類的成員憔杨。這是因為在拷貝的時候蒜胖,拷貝是整個4byte一起拷貝過去的,那么當(dāng)用一個基類對象拷貝給子類對象的時候台谢,應(yīng)該有的語義是只有基類的部分被修改,但如果子類占用了本來用空白占的地方朋沮,子類的部分也會被被用來拷貝的基類對象后面的未知數(shù)據(jù)給覆蓋掉蛇券。

存取靜態(tài)成員變量跟存取一個普通的靜態(tài)成員沒有區(qū)別,它跟類的綁定是在編譯期完成的塘慕。

在成員函數(shù)存取非靜態(tài)成員變量的方式是改寫成員函數(shù)蒂胞,把this指針傳進(jìn)成員函數(shù),然后成員函數(shù)用this指針操作成員變量骗随。在外部存取非靜態(tài)變量的方式跟c中的struct一樣鸿染,即使用了虛函數(shù)或多重繼承指蚜,因為這個可以在編譯期就決定好。

由于指針可以指向子類指針姚炕,而當(dāng)用了虛擬繼承的時候?qū)ο蟮膬?nèi)存布局可能會改變丢烘,所以用指針操作對象成員的指針要延遲到運行期并且要作一些判斷些椒,所以操作成員會變慢。其他情況都一樣免糕,即使用了虛函數(shù)和多重繼承石窑,因為它們都在upcast指針的時候已經(jīng)把指針轉(zhuǎn)換好了,這個在上面構(gòu)造函數(shù)里面寫了松逊。而直接用點操作符存取虛基類成員不會出現(xiàn)類型不確定的情況,所以可以在編譯期確定成員位置犀暑。

&Object::member 會得到成員在內(nèi)存布局中的位置烁兰。內(nèi)存中所有成員會在對象地址加1byte之后才開始排列,為了區(qū)分對象地址和對象成員地址(g++是這么干的)沪斟。

如果使用一個類的Pointer to Data Member,而且這個類用了多重繼承李根,那么當(dāng)把排列在后面的基類成員指針轉(zhuǎn)換為子類成員指針(它們都指向相同的成員)干发,就要對成員指針做offset調(diào)整。
成員函數(shù)指針

非虛函數(shù)無多重繼承的成員函數(shù)指針只是簡單的指向成員函數(shù)的指針冀续,調(diào)用起來也不會變慢必峰。

指向虛函數(shù)的成員函數(shù)指針不是地址,而是在虛函數(shù)表中的偏移量凭需,用它來調(diào)用虛函數(shù)實際上是用vptr加上它自己存的偏移量來調(diào)用肝匆。

多重繼承下的成員函數(shù)指針也不是指針,是struct旗国,還是union,還是什么度硝?我懶得管了寿冕。

異常

如果new操作符拋異常,那構(gòu)造函數(shù)還沒調(diào)用驼唱,所以不用去析構(gòu)對象。如果構(gòu)造函數(shù)拋異常捌治,那么成員對象和基類的析構(gòu)函數(shù)會被調(diào)用纽窟,也不用手動析構(gòu)對象。

catch語句的參數(shù)如果是值語義臂港,那就會像傳函數(shù)一樣調(diào)用一次拷貝構(gòu)造函數(shù)视搏。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末浑娜,一起剝皮案震驚了整個濱河市式散,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌暴拄,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件响驴,死亡現(xiàn)場離奇詭異撕蔼,居然都是意外死亡,警方通過查閱死者的電腦和手機琳骡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進(jìn)店門讼溺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事耘纱。” “怎么了艳馒?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵员寇,是天一觀的道長。 經(jīng)常有香客問我蝶锋,道長,這世上最難降的妖魔是什么慌闭? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮省古,結(jié)果婚禮上丧失,老公的妹妹穿的比我還像新娘。我一直安慰自己布讹,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布臀栈。 她就那樣靜靜地躺著挠乳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪盟蚣。 梳的紋絲不亂的頭發(fā)上卖怜,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天,我揣著相機與錄音奄抽,去河邊找鬼。 笑死甩鳄,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的档泽。 我是一名探鬼主播,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼馆匿,長吁一口氣:“原來是場噩夢啊……” “哼燥滑!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起腔稀,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎淡喜,沒想到半個月后诵闭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡疏尿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年褥琐,在試婚紗的時候發(fā)現(xiàn)自己被綠了猴蹂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刺洒。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖焙糟,靈堂內(nèi)的尸體忽然破棺而出落午,到底是詐尸還是另有隱情鲫咽,我是刑警寧澤谷异,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布歹嘹,位于F島的核電站寓落,受9級特大地震影響荞下,放射性物質(zhì)發(fā)生泄漏史飞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一抽诉、第九天 我趴在偏房一處隱蔽的房頂上張望吐绵。 院中可真熱鬧河绽,春花似錦唉窃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽件已。三九已至元暴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鉴未,已是汗流浹背援岩。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留享怀,地道東北人。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓梅屉,卻偏偏與公主長得像鳞贷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子搀愧,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,658評論 2 350

推薦閱讀更多精彩內(nèi)容

  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy閱讀 9,513評論 1 51
  • 一個博客搓幌,這個博客記錄了他讀這本書的筆記,總結(jié)得不錯溉愁∷乔鳎《深度探索C++對象模型》筆記匯總 1. C++對象模型與內(nèi)...
    Mr希靈閱讀 5,573評論 0 13
  • 1. 結(jié)構(gòu)體和共同體的區(qū)別撤蟆。 定義: 結(jié)構(gòu)體struct:把不同類型的數(shù)據(jù)組合成一個整體堂污,自定義類型。共同體uni...
    breakfy閱讀 2,118評論 0 22
  • 1. 讓自己習(xí)慣C++ 條款01:視C++為一個語言聯(lián)邦 為了更好的理解C++息楔,我們將C++分解為四個主要次語言:...
    Mr希靈閱讀 2,792評論 0 13
  • 事件扒披,和彥同事聊天后 應(yīng)對。指責(zé)(對自己) 感受:不安(我的熱情的給他說學(xué)習(xí)怎么好會有推銷嫌疑碟案?),懊惱(感覺自己...
    水玲瓏英子閱讀 196評論 0 0