一個博客伏穆,這個博客記錄了他讀這本書的筆記耻卡,總結(jié)得不錯屿脐。
《深度探索C++對象模型》筆記匯總
1. C++對象模型與內(nèi)存布局
參考資料
在C++中有兩種類的數(shù)據(jù)成員:static和nonstatic挺益,以及三種類的成員函數(shù):static歉糜、nonstatic和virtual。在C++對象模型中望众,非靜態(tài)數(shù)據(jù)成員被配置于每一個類的對象之中匪补,靜態(tài)數(shù)據(jù)成員則被存放在所有的類對象之外;靜態(tài)及非靜態(tài)成員函數(shù)也唄放在類對象之外烂翰,虛函數(shù)則通過以下兩個步驟支持:
- 每一個類產(chǎn)生出一堆指向虛函數(shù)的指針夯缺,放在表格之中,這個表格被稱為虛函數(shù)表(virtual table, vtbl)甘耿。
- 每一個類對象被添加了一個指針踊兜,指向相關(guān)的虛函數(shù)表,通常這個指針被稱為vptr佳恬。vptr的設(shè)定和重置都由每一個類的構(gòu)造函數(shù)捏境、析構(gòu)函數(shù)和拷貝賦值運算符自動完成于游。另外,虛函數(shù)表地址的前面設(shè)置了一個指向type_info的指針垫言,RTTI(Run Time Type Identification)運行時類型識別是由編譯器在編譯器生成的特殊類型信息贰剥,包括對象繼承關(guān)系,對象本身的描述筷频,RTTI是為多態(tài)而生成的信息蚌成,所以只有具有虛函數(shù)的對象在會生成。
class Point
{
public:
Point(float xval);
virtual ~Point();
float x() const;
static int PointCount();
protected:
virtual ostream& printf(ostream& os) const;
float _x;
static int _point_count;
}
這個模型的主要優(yōu)點在于它的空間和存取時間的效率凛捏;主要缺點則是担忧,如果應(yīng)用程序代碼本身未曾改變,但所用到的類對象的非靜態(tài)數(shù)據(jù)成員有所增加坯癣、移除或修改涵妥,那么這些應(yīng)用程序的代碼同樣得重新編譯。
- 非static成員變量被放置于每一個類對象中坡锡,非static成員函數(shù)放在類的對象之外蓬网,且非static成員變量在內(nèi)存中的存放順序與其在類內(nèi)的聲明順序一致。
- static成員變量存放在類的對象之外鹉勒,static成員函數(shù)也放在類的對象之外帆锋。
- C++中的虛函數(shù)是通過虛函數(shù)表(vtbl)來實現(xiàn),每一個類為每一個virtual函數(shù)產(chǎn)生一個指針禽额,放在表格中锯厢,這個表格就是虛函數(shù)表。每一個類對象會被安插一個指針(vptr)脯倒,指向該類的虛函數(shù)表实辑。vptr的設(shè)定和重置都由每一個類的構(gòu)造函數(shù)、析構(gòu)函數(shù)和復(fù)制賦值運算符自動完成藻丢。在本人的環(huán)境中剪撬,類的對象的安插的vptr放在該對象所占內(nèi)存的最前面。
- 虛函數(shù)表中函數(shù)指針存放的順序與虛函數(shù)在類中聲明的順序一致悠反。
加上繼承
C++支持單一繼承和多重繼承残黑,甚至繼承關(guān)系也可以指定為virtual(也就是共享的意思)。在虛擬繼承的情況下斋否,基類不管在繼承串鏈中被派生多少次梨水,永遠只會存在一個實體。
-
單繼承(只有一個父類)
類的繼承關(guān)系為:class Derived : public Base
Derived類的對象的內(nèi)存布局為:虛函數(shù)表指針茵臭、Base類的非static成員變量疫诽、Derived類的非static成員變量。
-
多重繼承(多個父類)
類的繼承關(guān)系如下:class Derived : public Base1, public Base2
Derived類的對象的內(nèi)存布局為:基類Base1子對象和基類Base2子對象及Derived類的非static成員變量組成∑嫱剑基類子對象包括其虛函數(shù)表指針和其非static的成員變量雏亚。
-
重復(fù)繼承(繼承的多個父類中其父類有相同的超類)
類的繼承關(guān)系如下:
class Base1 : public Base
class Base2: public Base
class Derived : public Base1, public Base2
Derived類的對象的內(nèi)存布局與多繼承相似,但是可以看到基類Base的子對象在Derived類的對象的內(nèi)存中存在一份拷貝逼龟。這樣直接使用Derived中基類Base的相關(guān)成員時评凝,就會引發(fā)歧義追葡,可使用多重虛擬繼承消除之腺律。
-
多重虛擬繼承(使用virtual方式繼承,為了保證繼承后父類的內(nèi)存布局只會存在一份)
類的繼承關(guān)系如下:
class Base1 : virtual public Base
class Base2: virtual public Base
class Derived : public Base1, public Base2
Derived類的對象的內(nèi)存布局與重復(fù)繼承的類的對象的內(nèi)存分布類似宜肉,但是基類Base的子對象沒有拷貝一份匀钧,在對象的內(nèi)存中僅存在在一個Base類的子對象。但是它的非static成員變量放置在對象的末尾處谬返。
2. 構(gòu)造函數(shù)語意學(xué)
默認構(gòu)造函數(shù)的建構(gòu)操作
通常很多C++程序員存在兩種誤解:
- 沒有定義默認構(gòu)造函數(shù)的類都會被編譯器生成一個默認構(gòu)造函數(shù)之斯。
- 編譯器生成的默認構(gòu)造函數(shù)會明確初始化類中每一個數(shù)據(jù)成員。
C++標準規(guī)定:如果類的設(shè)計者并未為類定義任何構(gòu)造函數(shù)遣铝,那么會有一個默認 構(gòu)造函數(shù)被暗中生成佑刷,而這個暗中生成的默認構(gòu)造函數(shù)通常是不做什么事的(無用的),下面四種情況除外酿炸。
換句話說瘫絮,有以下四種情況編譯器必須為未聲明構(gòu)造函數(shù)的類生成一個會做點事 的默認構(gòu)造函數(shù)。我們會看到這些默認構(gòu)造函數(shù)僅“忠于編譯器”填硕,而可能不會按 照程序員的意愿程效命麦萤。
包含有帶默認構(gòu)造函數(shù)的對象成員的類
若一個類X沒有定義任何構(gòu)造函數(shù),但卻包含一個或以上定義有默認構(gòu)造函數(shù)的 對象成員扁眯,此時編譯器會為X合成默認構(gòu)造函數(shù)壮莹,該默認函數(shù)會調(diào)用對象成員的 默認構(gòu)造函數(shù)為之初始化。如果對象的成員沒有定義默認構(gòu)造函數(shù)姻檀,那么編譯器 合成的默認構(gòu)造函數(shù)將不會為之提供初始化命满。例如類A包含兩個數(shù)據(jù)成員對象, 分別為:string str
和char *Cstr
绣版,那么編譯器生成的默認構(gòu)造函數(shù)將只提 供對string類型成員的初始化周荐,而不會提供對char*類型的初始化。
假如類X的設(shè)計者為X定義了默認的構(gòu)造函數(shù)來完成對str的初始化僵娃,形如:A::A({Cstr=”hello”};
因為默認構(gòu)造函數(shù)已經(jīng)定義概作,編譯器將不能再生成一 個默認構(gòu)造函數(shù)。但是編譯器將會擴充程序員定義的默認構(gòu)造函數(shù)——在最前面插 入對初始化str的代碼默怨。若有多個定義有默認構(gòu)造函數(shù)的成員對象讯榕,那么這些成員 對象的默認構(gòu)造函數(shù)的調(diào)用將依據(jù)聲明順序排列。繼承自帶有默認構(gòu)造函數(shù)的基類的類
如果一個沒有定義任何構(gòu)造函數(shù)的類派生自帶有默認構(gòu)造函數(shù)的基類,那么編譯 器為它定義的默認構(gòu)造函數(shù)愚屁,將按照聲明順序為之依次調(diào)用其基類的默認構(gòu)造函 數(shù)济竹。若該類沒有定義默認構(gòu)造函數(shù)而定義了多個其他構(gòu)造函數(shù),那么編譯器擴充它的所有構(gòu)造函數(shù)——加入必要的基類默認構(gòu)造函數(shù)霎槐。另外送浊,編譯器會將基類的默認構(gòu)造函數(shù)代碼加在對象成員的默認構(gòu)造函數(shù)代碼之前。帶有虛函數(shù)的類
帶有虛函數(shù)的類丘跌,與其它類不太一樣袭景,因為它多了一個vptr,而vptr的設(shè)置是由 編譯器完成的闭树,因此編譯器會為類的每個構(gòu)造函數(shù)添加代碼來完成對vptr的初始化耸棒。帶有一個虛基類的類
在這種情況下,編譯器要將虛基類在類中的位置準備妥當报辱,提供支持虛基類的機 制与殃。也就是說要在所有構(gòu)造函數(shù)中加入實現(xiàn)前述功能的的代碼。沒有構(gòu)造函數(shù)將 合成以完成上述工作碍现。
總的來說幅疼,編譯器將對構(gòu)造函數(shù)動這些手腳:
- 如果類虛繼承自基類,編譯器將在所有構(gòu)造函數(shù)中插入準備虛基類位置的代 碼和提供支持虛基類機制的代碼昼接。
- 如果類聲明有虛函數(shù)爽篷,那么編譯器將為之生成虛函數(shù)表以存儲虛函數(shù)地址, 并將虛函數(shù)指針(vptr)的初始化代碼插入到類的所有構(gòu)造函數(shù)中辩棒。
- 如果類的父類有默認構(gòu)造函數(shù)狼忱,編譯將會對所有的默認構(gòu)造函數(shù)插入調(diào)用其 父類必要的默認構(gòu)造函數(shù)。必要是指設(shè)計者沒有顯示初始化其父類一睁,調(diào)用順 序钻弄,依照其繼承時聲明順序。
- 如果類包含帶有默認構(gòu)造函數(shù)的對象成員者吁,那么編譯器將會為所有的構(gòu)造函 數(shù)插入對這些對象成員的默認構(gòu)造函數(shù)進行必要的調(diào)用代碼窘俺,所謂必要是指 類設(shè)計者設(shè)計的構(gòu)造函數(shù)沒有對對象成員進行顯式初始化。成員對象默認構(gòu) 造函數(shù)的調(diào)用順序复凳,依照其聲明順序瘤泪。
- 若類沒有定義任何構(gòu)造函數(shù),編譯器會為其合成默認構(gòu)造函數(shù)育八,再執(zhí)行上述 四點对途。
拷貝構(gòu)造函數(shù)
拷貝構(gòu)造函數(shù)的定義:有一個參數(shù)類型是其類類型的構(gòu)造函數(shù)。
X::X( const X& x);
Y::Y( const Y& y, int =0 );//可以是多參數(shù)形式髓棋,但其第二個即后繼參數(shù)都有一個默認值
當一個類對象以另一個同類實體作為初值時实檀,大部分情況下會調(diào)用拷貝構(gòu)造函數(shù)惶洲。 一般是這三種具體情況:顯式地以一個類對象作為另一個類對象的初值,形如X xx=x;當類對象被作為參數(shù)交給函數(shù)時膳犹;當函數(shù)返回一個類對象時恬吕。
編譯器何時合成拷貝構(gòu)造函數(shù)
并不是所有未定義有拷貝構(gòu)造函數(shù)的類編譯器都會為其合成拷貝構(gòu)造函數(shù),只有在編譯器在普通手段無法解決“一個類對象以另一個同類實體作為初值”這一問題時须床,編譯器才會合成 拷貝構(gòu)造函數(shù)铐料。如果一個類沒有定義拷貝構(gòu)造函數(shù),通常按照“成員逐一初始化(Default Memberwise Initialization)”的手法來解決這一問題——即把內(nèi)建或派生的數(shù)據(jù)成員從某一個對象拷貝到另一個對象身上豺旬,如果數(shù)據(jù)成員是一個對象钠惩,則遞歸使用“成員逐一初始化”的手法。
成員逐一初始化具體的實現(xiàn)方式則是位逐次拷貝(Bitwise copy semantics)哈垢。也就是在能使用這種常規(guī)方式來解決“一個類對象以另一個同類實體作為初值”的時候妻柒,編譯器是不需要合成拷貝構(gòu)造函數(shù)的扛拨。但有些時候常規(guī)武器不那么管用耘分,我們就得祭出非常規(guī)武器了 ——拷貝構(gòu)造函數(shù)。有以下幾種情況绑警,位逐次拷貝將不能勝任或者不適合來完成 “一個類對象以另一個同類實體作為初值”的工作求泰。此時,如果類沒有定義拷貝構(gòu)造函數(shù)计盒,那么編譯器將必須為類合成一個拷貝構(gòu)造函數(shù)渴频。
- 當類內(nèi)含一個成員對象,而后者的類聲明有一個拷貝構(gòu)造函數(shù)時(不論是設(shè) 計者定義的還是編譯器合成的)北启。
- 當類繼承自一個聲明有拷貝構(gòu)造函數(shù)的類時(不論這個拷貝構(gòu)造函數(shù) 是被顯示聲明還是由編譯器合成的)卜朗。
- 類中聲明有虛函數(shù)。
- 當類的派生串鏈中包含有一個或多個虛基類咕村。
對于前兩種情況场钉,不論是基類還是對象成員,既然后者聲明有拷貝構(gòu)造函數(shù)時懈涛, 就表明其類的設(shè)計者或者編譯器希望以其聲明的拷貝構(gòu)造函數(shù)來完成“一個類對象以另一個同類實體作為初值”的工作逛万,而設(shè)計者或編譯器這樣做——聲明拷貝構(gòu)造函 數(shù),總有它們的理由批钠,而通常最直接的原因是他們想要做一些額外的工作或“位逐次拷貝”無法勝任宇植。
對于有虛函數(shù)的類,如果兩個對象的類型相同那么位逐次拷貝其實是可以勝任的埋心。 但問題將出現(xiàn)在指郁,如果基類由其繼承類進行初始化時,此時若按照位逐次拷貝來 完成這個工作拷呆,那么基類的vptr將指向其繼承類的虛函數(shù)表闲坎,這將導(dǎo)致無法預(yù)料的后果——調(diào)用一個錯誤的虛函數(shù)實體是無法避免的,輕則帶來程序崩潰,更糟糕的問題可能是這個錯誤被隱藏了箫柳。所以對于有虛函數(shù)的類編譯器將會明確的使被初始化的對象的vptr指向正確的虛函數(shù)表手形。因此有虛函數(shù)的類沒有聲明拷貝構(gòu)造 函數(shù),編譯將為之合成一個悯恍,來完成上述工作库糠,以及初始化各數(shù)據(jù)成員,聲明有拷貝構(gòu)造函數(shù)的話也會被插入完成上述工作的代碼涮毫。
對于繼承串鏈中有虛基類的情況瞬欧,問題同樣出現(xiàn)在繼承類向基類提供初值的情況, 此時位逐次拷貝有可能破壞對象中虛基類子對象的位置罢防。
成員初始化列表
對于初始化列表艘虎,有一個概念是非常重要的:在構(gòu)造函數(shù)中對于對象成員的初始化發(fā)生在初始化列表中——或者我們可以把初始化列表直接看做是對成員的定義,而構(gòu)造函數(shù)體中進行的則是賦值操作咒吐。所以不難理解有四種情況必須用到初始化列表:
- 有const成員
- 有引用類型成員
- 成員對象沒有默認構(gòu)造函數(shù)
- 基類對象沒有默認構(gòu)造函數(shù)
前兩者因為要求定義時初始化野建,所以必須明確的在初始化隊列中給它們提供初值。后兩者因為不提供默認構(gòu)造函數(shù)恬叹,所以必須顯式調(diào)用它們的帶參構(gòu)造函數(shù)來定義即初始化它們候生。顯而易見的是當類中含有對象成員或者繼承自基類的時候,在初始化隊列中初始化成員對象和基類子對象會在效率上得到提升——省去了一些賦值操作嘛绽昼。
3. Data語意學(xué)
C++類對象的大小
猜猜下面幾個類sizeof的大小唯鸭。
class X{};
class Y:virtual public X{};
class Z:virtual public X{};
class A:public Y, public Z{};
32位系統(tǒng)下VS2010的運行結(jié)果為
sizeof X yielded 1
sizeof Y yielded 4
sizeof Z yielded 4
sizeof Z yielded 8
事實上,對于像X這樣的一個的空類硅确,編譯器會對其動點手腳——隱晦的插入一個字節(jié)目溉。為 什么要這樣做呢?插入了這一個字節(jié)菱农,那么X的每一個對象都將有一個獨一無二的地址缭付。
再看看Y和Z,由于要實現(xiàn)虛繼承大莫,需要額外添加一個虛指針指向虛函數(shù)表蛉腌。到目前為止,對于一個32位的機器來說Y只厘、Z的大小應(yīng)該為5烙丛,而不 是8或者4。我們需要再考慮兩點因素:內(nèi)存對齊(memory alignment)和編譯器的優(yōu)化羔味。
對齊會將數(shù)值調(diào)整到某數(shù)的整數(shù)倍河咽,32位計算機上為4bytes。內(nèi)存對齊可以使得總線的運輸量達到最高效率赋元。所以Y忘蟹、Z的大小被補齊到8飒房。那么在vs2010中為什么Y、Z的大小是4而不是8呢媚值?我們先思考一個問題狠毯,X之所以被插入 1字節(jié)是因為本身為空,需要這一個字節(jié)為其在內(nèi)存中給它占領(lǐng)一個獨一無二的地址褥芒。但 是當這一字節(jié)被繼承到Y(jié)嚼松、Z后呢?它已經(jīng)完全失去了它存在的意義锰扶,為什么献酗?因為Y、Z各 自擁有一個虛基類指針坷牛,它們的大小不是0罕偎。既然這一字節(jié)在Y、Z中毫無意義京闰,那么就沒 必要留著颜及。也就是說vs2010對它們進行了優(yōu)化,優(yōu)化的結(jié)果是去掉了那一個字節(jié)忙干。
影響C++類的大小的三個因素:
- 支持特殊功能所帶來的額外負擔(對各種virtual的支持)器予。
- 編譯器對特殊情況的優(yōu)化處理浪藻。
- 內(nèi)存對齊操作捐迫。
數(shù)據(jù)成員的布局與存取
參考第一章中的兩篇參考文獻。
在VC中數(shù)據(jù)成員的布局順序為:
- vptr部分(如果基類有爱葵,則繼承基類的)
- vbptr (如果需要)
- 基類成員(按聲明順序)
- 自身數(shù)據(jù)成員
- 虛基類數(shù)據(jù)成員(按聲明順序)
4. 函數(shù)語意學(xué)
成員函數(shù)的調(diào)用
c++支持三種類型的成員函數(shù)施戴,分別為static,nostatic,virtual。每一種調(diào)用方式都不盡相同萌丈。
非靜態(tài)成員函數(shù)
為保證非靜態(tài)成員函數(shù)具有與普通函數(shù)調(diào)用相同的效率赞哗,編譯器內(nèi)部會將成員函數(shù)等價轉(zhuǎn)換為非成員函數(shù)。具體是這樣實現(xiàn)的:
- 改寫成員函數(shù)的簽名辆雾,使得其可以接受一個額外參數(shù)肪笋,this指針。如果成員函數(shù)是const的度迂,插入的參數(shù)類型將為 const Point* 類型藤乙。
float Point::X();
//成員函數(shù)X被插入額外參數(shù)this
float Point:: X(Point* this );
- 將每一個對非靜態(tài)數(shù)據(jù)成員的操作都改寫為經(jīng)過this操作。
- 將成員函數(shù)寫成一個外部函數(shù)惭墓,對函數(shù)名進行“mangling”處理坛梁,使之成為獨一無二的名稱。
關(guān)鍵在于為函數(shù)提供一個可直接讀寫成員數(shù)據(jù)的通道(this)腊凶,和對函數(shù)名進行修飾划咐,以免名字沖突拴念。
虛成員函數(shù)
如果function()是一個虛擬函數(shù),那么用指針或引用進行的調(diào)用將發(fā)生一點特別的轉(zhuǎn)換——一個中間層被引入進來褐缠。
例如:p->function()
將轉(zhuǎn)化為(*p->vptr[1])(p)
政鼠。其中vptr為指向虛函數(shù)表的指針,它由編譯器產(chǎn)生队魏。vptr也要進行名字處理缔俄,因為一個繼承體系可能有多個vptr。1是虛函數(shù)在虛函數(shù)表中的索引器躏,通過它關(guān)聯(lián)到虛函數(shù)function()俐载。
當通過指針調(diào)用的時候,要調(diào)用的函數(shù)實體無法在編譯期決定登失,必需待到執(zhí)行期才能獲得遏佣,所以上面引入一個間接層的轉(zhuǎn)換必不可少。但是當我們通過對象(不是引用揽浙,也不是指針)來調(diào)用的時候状婶, 進行上面的轉(zhuǎn)換就顯得多余了,因為在編譯器要調(diào)用的函數(shù)實體已經(jīng)被決定馅巷。此時調(diào)用發(fā)生的轉(zhuǎn)換膛虫,與一個非靜態(tài)成員函數(shù)調(diào)用發(fā)生的轉(zhuǎn)換一致。
靜態(tài)成員函數(shù)
靜態(tài)成員函數(shù)不能直接讀寫其類中的非靜態(tài)成員和調(diào)用非靜態(tài)成員函數(shù)钓猬,不能申明為const稍刀、voliatile或virtual,不需經(jīng)由對象調(diào)用(允許通過對象調(diào)用)敞曹。除了缺乏一個this指針他與非靜態(tài)成員函數(shù)沒有太大的差別账月。在這里通過對象調(diào)用和通過指針或引用調(diào)用,將被轉(zhuǎn)化為同樣的調(diào)用代碼澳迫。
虛成員函數(shù)
單繼承下的虛函數(shù)
虛函數(shù)的實現(xiàn)原理:編譯器為每個有虛函數(shù)的類配一張?zhí)摵瘮?shù)表局齿,用來存儲該類類型信息和所有虛函數(shù)執(zhí)行期的地址;并為每個有虛函數(shù)的類插入一個指針(vptr),這個指針指向該類的虛函數(shù)表橄登,最后 給每一個虛函數(shù)指派一個在表中的索引抓歼。
一個類的虛函數(shù)表中存儲有類型信息(VC2010中存儲在索引為-1的位置)和所有虛函數(shù)地址,這些虛函數(shù)地址包括三種:
- 這個類定義的虛函數(shù)拢锹,會改寫(overriding)一個可能存在的基類的虛函數(shù)實體——假如基類也定義有這個虛函數(shù)谣妻。
- 繼承自基類的虛函數(shù)實體,——基類定義有面褐,而這個類卻沒有定義拌禾。直接繼承之。
- 一個純虛函數(shù)實體展哭。用來在虛函數(shù)表中占座湃窍,有時候也可以當做執(zhí)行期異常處理函數(shù)闻蛀。
當一個類單繼承自有虛函數(shù)的基類的時候,將按如下步驟構(gòu)建虛函數(shù)表:
- 繼承基類中聲明的虛函數(shù)——這些虛函數(shù)的實體地址被拷貝到繼承類中的虛函數(shù)表中對于的slot中您市。
- 如果有改寫(override)基類的虛函數(shù)觉痛,那么在1中應(yīng)將改寫(override)的函數(shù)實體的地址放入對應(yīng)的slot中而不是拷貝基類的。
- 如果有定義新的虛函數(shù)茵休,那么將虛函數(shù)表擴大一個slot以存放新的函數(shù)實體地址薪棒。
每一個虛函數(shù)都被指派一個固定的索引值,這個索引值在整個繼承體系中保持前后關(guān)聯(lián)榕莺,例如,假如z()在Point虛函數(shù)表中的索引值為2俐芯,那么在Point3d虛函數(shù)表中的索引值也為2。
多重繼承下的虛函數(shù)
在多重繼承下钉鸯,繼承類需要為每一條繼承線路維護一個虛函數(shù)表(也有可能這些表被合成為一個吧史,但本質(zhì)意義并沒有變化)。當然這一切都發(fā)生在需要的情況下唠雕。
當使用第一繼承的基類指針來調(diào)用繼承類的虛函數(shù)的時候贸营,與單繼承的情況沒有什么異樣,問題出生在當以第二或后繼的基類指針(或引用)的使用上岩睁。例如:
//假設(shè)有這樣的繼承關(guān)系:class Derived:public base1,public base2;
//base1,base2都定義有虛析構(gòu)函數(shù)钞脂。
base2 *ptr = new derived;
//需要被轉(zhuǎn)換為,這個轉(zhuǎn)換在編譯期完成
base2 *ptr = temp ? temp + sizeof(base1) : 0 ;
如果不做出上面的轉(zhuǎn)換捕儒,那么 ptr 指向的并不是 derived 的 base2 subobject 冰啃。后果是,ptr 將一個derived類型當做base2類型來用肋层。
當要delete ptr時又面臨了一次轉(zhuǎn)換亿笤,因為在delete ptr的時候,需要對整個對象而不是其子對象施行delete運算符栋猖,這期間需要調(diào)整ptr指向完整的對象起點,因為不論是調(diào)用正確的析構(gòu)函數(shù)還是delete運算符都需要一個指向?qū)ο笃瘘c的指針汪榔,想一想給予一個derived類的成員函數(shù)指向base2 subobjuect 的this指針會發(fā)生什么吧蒲拉。因為ptr的具體類型并不知道,所以必須要等到執(zhí)行期來完成痴腌。
多繼承下的虛函數(shù)雌团,影響到虛函數(shù)的調(diào)用的實際質(zhì)上為this的調(diào)整。而this調(diào)整一般為兩種:
- 調(diào)整指針指向?qū)?yīng)的subobject士聪,一般發(fā)生在繼承類類型指針向基類類型指針賦值的情況下锦援。
- 將指向subobject的指針調(diào)整回繼承類對象的起始點,一般發(fā)生在基類指針對繼承類虛函數(shù)進行調(diào)用的時候剥悟。
第一點灵寺,使得該基類指針指向一個與其指針類型匹配的子對象曼库,唯有如此才能保證使得該指針在執(zhí)行與其指針類型相匹配的特定行為的正確性。比方調(diào)用基類的成員略板,獲得正確的虛函數(shù)地址毁枯。可以想象如果不調(diào)整叮称,用ptr存取base2 subobject的數(shù)據(jù)成員時种玛,會發(fā)生什么?調(diào)用base2的成員函數(shù)的時候瓤檐,其成員函數(shù)接受的this指針指向derived 類型對象赂韵,這又會發(fā)生什么?結(jié)果是整個對象的內(nèi)存結(jié)構(gòu)有可能都被破壞挠蛉。還有別忘了右锨,vptr也可以看做一個數(shù)據(jù)成員,要找到虛函數(shù)碌秸,前提是獲取正確的vptr偏移量绍移。
而第二點,顯然是讓一個繼承類的虛函數(shù)獲取一個正確的this指針讥电,因為一個繼承類虛函數(shù)要的是一個指向繼承類對象的this指針蹂窖,而不是指向其子對象。
第一順序繼承類之所以不需要進行調(diào)整的關(guān)鍵在于恩敌,其subobject的起點與繼承類對象的起點一致瞬测。
5. 構(gòu)造、解構(gòu)纠炮、拷貝 語意學(xué)
幾點類的設(shè)計原則
1.即使是一個抽象基類月趟,如果它有非靜態(tài)數(shù)據(jù)成員,也應(yīng)該給它提供一 個帶參數(shù)的構(gòu)造函數(shù)恢口,來初始化它的數(shù)據(jù)成員孝宗。或許你可以通過其派生 類來初始化它的數(shù)據(jù)成員(假如nostatic data member為publish或 protected),但這樣做的后果則是破壞了數(shù)據(jù)的封裝性耕肩,使類的維護和修 改更加困難因妇。由此引申,類的data member應(yīng)當被初始化猿诸,且只在其構(gòu)造函 數(shù)或其member function中初始化婚被。
2.不要將析構(gòu)函數(shù)設(shè)計為純虛的,這不是一個好的設(shè)計梳虽。將析構(gòu)函數(shù) 設(shè)計為純虛函數(shù)意味著址芯,即使純虛函數(shù)在語法上允許我們只聲明而不定義 純虛函數(shù),但還是必須實現(xiàn)該純虛析構(gòu)函數(shù),否則它所有的繼承類都將遇 到鏈接錯誤谷炸。一個不能派生繼承類的抽象類有什么存在的意義北专?必須定義 純虛析構(gòu)函數(shù),而不能僅僅聲明它的原因在于:每一個繼承類的析構(gòu)函數(shù) 會被編譯器加以擴展淑廊,以靜態(tài)調(diào)用方式其每一個基類的析構(gòu)函數(shù)(假如有 的話逗余,不論是顯示的還是編譯器合成的),所以只要任何一個基類的析構(gòu) 函數(shù)缺乏定義季惩,就會導(dǎo)致鏈接失敗录粱。矛盾就在這里,純虛函數(shù)的語法画拾,允 許只聲明而不定義純虛析構(gòu)函數(shù)啥繁,而編譯器則死腦筋的看到一個其基類的 析構(gòu)函數(shù)聲明,則去調(diào)用它的實體青抛,而不管它有沒有被定義旗闽。
3.真的必要的時候才使用虛函數(shù),不要濫用虛函數(shù)蜜另。虛函數(shù)意味著不 小的成本适室,編譯很可能給你的類帶來膨脹效應(yīng):
每一個對象要多負擔一個word的vptr。
給每一個構(gòu)造函數(shù)(不論是顯示的還是編譯器合成的)举瑰,插入一些代 碼來初始化vptr捣辆,這些代碼必須被放在所有基類構(gòu)造函數(shù)的調(diào)用之后, 但需在任意用戶代碼之前此迅。沒有構(gòu)造函數(shù)則需要合成汽畴,并插入代碼。
合成一個拷貝構(gòu)造函數(shù)和一個復(fù)制操作符(如果沒有的話)耸序,并插入 對vptr的初始化代碼忍些,有的話也需要插入vptr的初始化代碼。
意味著坎怪,如果具有bitwise語意罢坝,將不再具有,然后是變大的對象芋忿、沒 有那么高效的構(gòu)造函數(shù)炸客,沒有那么高效的復(fù)制控制。
4.不能決定一個虛函數(shù)是否需要 const ,那么就不要它。
5.決不在構(gòu)造函數(shù)或析構(gòu)函數(shù)中使用虛函數(shù)機制拜姿。在構(gòu)造函數(shù)中互躬,每次 調(diào)用虛函數(shù)會被決議為當前構(gòu)造函數(shù)所對應(yīng)類的虛函數(shù)實體,虛函數(shù)機制并 不起作用拟枚。當一個base類的構(gòu)造函數(shù)含有對虛函數(shù)vf()的調(diào)用薪铜,當其派生類 derived的構(gòu)造函數(shù)調(diào)用基類base的構(gòu)造函數(shù)的時候众弓,其中調(diào)用的虛函數(shù)vf() 是base中的實體,而不是derived中的實體隔箍。這是由vptr初始化的位置決定的 ——在所有基類構(gòu)造函數(shù)調(diào)用之后谓娃,在程序員供應(yīng)的代碼或是成員初始化隊列 之前。因構(gòu)造函數(shù)的調(diào)用順序是:有根源到末端蜒滩,由內(nèi)而外滨达,所以對象的構(gòu) 造過程可以看成是,從構(gòu)建一個最基礎(chǔ)的對象開始俯艰,一步步構(gòu)建成一個目標 對象捡遍。析構(gòu)函數(shù)則有著與構(gòu)造相反的順序,因此在構(gòu)造或析構(gòu)函數(shù)中使用虛 函數(shù)機制竹握,往往不是程序員的意圖画株。若要在構(gòu)造函數(shù)或析構(gòu)函數(shù)中調(diào)用虛函 數(shù),應(yīng)當直接以靜態(tài)方式調(diào)用啦辐,而不要通過虛函數(shù)機制谓传。