C++ 多重繼承和虛擬繼承對(duì)象模型、效率分析
一、多態(tài)
C++多態(tài)通過(guò)繼承和動(dòng)態(tài)綁定實(shí)現(xiàn)。繼承是一種代碼或者功能的傳承共享,從語(yǔ)言的角度它是外在的、形式上的,極易理解。而動(dòng)態(tài)綁定則是從語(yǔ)言的底層實(shí)現(xiàn)保證了多態(tài)的發(fā)生——在運(yùn)行期根據(jù)基類指針或者引用指向的真實(shí)對(duì)象類型確定調(diào)用的虛函數(shù)功能!通過(guò)帶有虛函數(shù)的單一繼承我們可以清楚的理解繼承的概念、對(duì)象模型的分布機(jī)制以及動(dòng)態(tài)綁定的發(fā)生,即可以完全徹底地理解多態(tài)的思想。為了支持多態(tài),語(yǔ)言實(shí)現(xiàn)必須在時(shí)間和空間上付出額外的代價(jià)(畢竟沒(méi)有免費(fèi)的晚餐,更何況編譯器是毫無(wú)感情):
1、類實(shí)現(xiàn)時(shí)增加了virtual table,用來(lái)存放虛函數(shù)地址;
2、類對(duì)象中增加了指向虛函數(shù)表的指針vptr,以提供runtime的鏈接;
3、在類繼承層次的構(gòu)造函數(shù)中重復(fù)設(shè)定vptr的初值,以期待指針指向?qū)?yīng)類的virtual table;
4、在類繼承層次的析構(gòu)函數(shù)中重復(fù)還原vptr的初值;
5、多態(tài)發(fā)生時(shí)(base class指針調(diào)用虛函數(shù))需要通過(guò)vptr和virtual table表調(diào)用對(duì)應(yīng)函數(shù)實(shí)體,增加了 一層間接性。
第1、2兩點(diǎn)是多態(tài)帶來(lái)的空間代價(jià),后面三點(diǎn)則是時(shí)間效率上的代價(jià)。
二、多重繼承和虛擬繼承
多重繼承具有多個(gè)base class,有別于單一繼承(提供了一種“自然多態(tài)”形式)。單一繼承中,基類和派生類具有相同的內(nèi)存地址,它們之間的轉(zhuǎn)換十分自然不需要編譯器的介入。但如果基類中沒(méi)有虛函數(shù)而派生類中有,單一繼承的自然多態(tài)被打破。這種情況下,派生類轉(zhuǎn)換為基類需要編譯器的介入,用以調(diào)整this指針地址。多重繼承的對(duì)象模型較單一繼承復(fù)雜,根源在于derived class objects和其第二或后繼的base class objects之間的“非自然”關(guān)系 ,這一點(diǎn)可以從下面的對(duì)象模型中看到。派生類和基類之間的非自然多態(tài)引起了一個(gè)嚴(yán)重的問(wèn)題(在虛擬繼承中也存在):derived class和第二或后繼base class之間的轉(zhuǎn)換(不論是對(duì)象間的直接轉(zhuǎn)換或者經(jīng)由其所支持的virtual function機(jī)制做轉(zhuǎn)換)需要調(diào)整this指針的地址,以使其指向完整正確的class object 。
虛擬繼承是一種機(jī)制,類通過(guò)虛繼承指出它所希望共享虛基類的狀態(tài),虛基類在派生層次中只有一份實(shí)體。相比多重繼承,虛擬繼承的難點(diǎn)在于既要識(shí)別出相同的對(duì)象部分又要維持基類和派生類之間的多態(tài)關(guān)系 。通常情況下,實(shí)現(xiàn)虛擬繼承時(shí)編譯器將對(duì)象分割為一個(gè)不變局部和一個(gè)共享局部 。不變局部中的數(shù)據(jù),不管后繼如何衍化,總是擁有固定的offset,所以這一部分?jǐn)?shù)據(jù)可以被直接存取。至于共享局部,所表現(xiàn)的就是virtual base class subobject。這一部分的數(shù)據(jù),其位置會(huì)因?yàn)槊看蔚呐缮僮鞫凶兓运鼈冎荒鼙婚g接存取 。各家編譯器實(shí)現(xiàn)技術(shù)之間的差異在于間接存取方法不同。一般的策略就是先安排好派生類的不變部分,然后建立共享部分。虛擬繼承base class和derived class之間非自然的多態(tài)關(guān)系,它們之間相互轉(zhuǎn)換時(shí)需要對(duì)this指針地址進(jìn)行調(diào)整。由于對(duì)virtual base class的支持,虛擬繼承帶來(lái)了額外的負(fù)擔(dān)和模型復(fù)雜性。
三、多重繼承和虛擬繼承對(duì)象模型
造成多重繼承和虛擬繼承較普通單一繼承復(fù)雜、效率低的本質(zhì)在于 對(duì)象模型內(nèi)存分布的差異, 這一點(diǎn)從第二部分分析也可以看到。下面示例對(duì)比列出了普通單一繼承、多重繼承以及虛擬繼承的對(duì)象模型。需要說(shuō)明的是:C++標(biāo)準(zhǔn)中并沒(méi)有強(qiáng)制規(guī)定base class members和derived class members之間的次序關(guān)系,理論上可以自由安排之,但實(shí)際上大多數(shù)編譯器都會(huì)基類成員放在前面,但虛擬繼承除外。下面也是這種策略,同時(shí)把vptr作為類的第一個(gè)成員。
基類Base1、Base2以及派生類DerivedSingle、DerivedMulti類定義如下:
class Base1
{
public:
Base1(void);
~Base1(void);
virtual Base1* clone()const;
protected:
float data_Base1;
};
class Base2
{
public:
Base2(void);
~Base2(void);
virtual void mumble();
virtual Base2* clone()const;
protected:
float data_Base2;
};
class DerivedSingle: public Base1
{
public:
DerivedSingle(void);
virtual ~DerivedSingle(void);
virtual DerivedSingle* clone() const;
protectd:
float data_DerivedSingle;
};
class DerivedMulti :public Base1, public Base2
{
public:
DerivedMulti(void);
virtual ~DerivedMulti(void);
virtual DerivedMulti* clone() const;
protected:
float data_DerivedMulti;
};
對(duì)象模型如下,虛擬繼承和單一繼承類結(jié)構(gòu)相同,只是繼承改成了虛擬繼承。
單一繼承
:

多重繼承:

虛擬繼承:
為了保證memberwise復(fù)制的正確性(否則基類子對(duì)象復(fù)制給派生類時(shí)會(huì)發(fā)生錯(cuò)誤),C++中保證“基類子對(duì)象在派生類中的原樣性 ”。
單一繼承的對(duì)象模型呈現(xiàn)了一種“自然多態(tài)”的形式,基類和派生類之間的轉(zhuǎn)換十分自然簡(jiǎn)單。然而多重繼承有多個(gè)基類,對(duì)象有多個(gè)vptr指針,對(duì)于第二個(gè)或后繼基類和派生類之間的轉(zhuǎn)換需要地址調(diào)整,以指向完整的基類子對(duì)象。
虛擬繼承中,為了記住和共享虛擬基類,需要在類中添加指向該基類的指針。從上面的虛擬繼承對(duì)象模型中可以看到,雖然和單一繼承有相同的類層次結(jié)構(gòu),但虛擬繼承打破了單一繼承的“自然多態(tài)”形式,基類和派生類之間的轉(zhuǎn)換需要調(diào)整this指針的地址。如果是虛擬多重繼承,則虛擬基類/后繼基類和派生類之間的轉(zhuǎn)換需要this指針地址調(diào)整 。
一般規(guī)則,多重繼承經(jīng)由指向“第二個(gè)或者后繼base class”的指針(引用)來(lái)調(diào)用derived class virtual function,該操作所連帶的“必要的this指針調(diào)整”操作,必須在執(zhí)行期完成,也就是說(shuō)offset的大小、以及吧offset加到this指針上頭的那一小段程序代碼,必須有編譯器在某個(gè)地方插入。為了實(shí)現(xiàn)this指針調(diào)整引入thunk技術(shù),所謂thunk是一小段assembly代碼,用來(lái)以適當(dāng)?shù)膐ffset值調(diào)整this指針,并跳到virtual函數(shù)去。Thunk技術(shù)允許virtual table slot繼續(xù)內(nèi)含一個(gè)簡(jiǎn)單的指針,因此多重繼承不需要額外任何空間上的額外負(fù)擔(dān)。Slots中的地址可以直接指向virtual function,也可以指向一個(gè)相關(guān)的thunk(如果需要調(diào)整this指針)。調(diào)整this指針的第二個(gè)額外負(fù)擔(dān)就是,由于兩中不同的可能:(1)經(jīng)由derived class(或者第一個(gè)base class)調(diào)用,(2)經(jīng)由第二個(gè)(或者后繼)base class調(diào)用,同一個(gè)函數(shù)在virtual table中可能需要多筆對(duì)應(yīng)的slots。并且在第二個(gè)或者后繼base class中的虛函數(shù)表保存的是thunk代碼地址。
四、 效率
通過(guò)上面第三部分的分析,多重繼承和虛擬繼承對(duì)象模型的較單一繼承復(fù)雜的對(duì)象模型 ,造成了成員訪問(wèn)低效率, 表現(xiàn)在兩個(gè)方面:對(duì)象構(gòu)建時(shí)vptr的多次設(shè)定,以及this指針的調(diào)整。對(duì)于多種繼承情況的效率比較如下:
| 情形 | Vptr 設(shè)定 | Data member 訪問(wèn) | virtual Function member 訪問(wèn) | 效率分析 |
| 單一繼承 no vptr | 無(wú) | 指針/引用/對(duì)象訪問(wèn)效率相同 | 直接訪問(wèn) | 效率較高 |
| 單一繼承 | 一次 | 指針/引用/對(duì)象訪問(wèn)效率相同 | 通過(guò)vptr和vtable訪問(wèn) | 多態(tài)的引入,帶來(lái)了設(shè)定vptr和間接訪問(wèn)虛函數(shù)等效率的降低 |
| 多重繼承 | 多次 | 指針/引用/對(duì)象訪問(wèn)效率相同 | 通過(guò)vptr和vtable訪問(wèn),通過(guò)第二或者后繼base類指針訪問(wèn)需要調(diào)整this指針 | 除了單一繼承效率降低的情形,調(diào)整this指針也帶來(lái)了效率的降低 |
| 虛擬繼承 | 多次 | 對(duì)象/指針/應(yīng)用訪問(wèn)效率較低 | 通過(guò)vptr和vtable訪問(wèn),訪問(wèn)虛基類需要調(diào)整this指針 | 除了單一繼承效率降低的情形,調(diào)整this指針也帶來(lái)了效率的降低 |
多態(tài)中的data member訪問(wèn)
考察多態(tài)中幾種繼承情形的data member成員訪問(wèn)效率的關(guān)鍵是:members的offset位置在編譯期是否能夠確定。 如果訪問(wèn)的成員在編譯期就可以確定下offset位置,不會(huì)帶來(lái)額外的負(fù)擔(dān)。
理論上針對(duì)上面的繼承類型,通過(guò)類對(duì)象訪問(wèn),效率完全一樣,因?yàn)槌蓡T在類中的位置在編譯期是可以確定的。通過(guò)引用或者指針訪問(wèn),除了一種情形,上面的繼承類型效率也完全相同 。例外情形是:通過(guò)指針和引用訪問(wèn)虛擬基類的數(shù)據(jù)成員。因?yàn)樘摂M基類在不同的繼承層次中,其offset位置是變化的,并且無(wú)法通過(guò)指針或者引用類型確定指針指向?qū)ο蟮恼鎸?shí)類型,所以編譯期無(wú)法確定offset位置,只能在運(yùn)行期通過(guò)類型信息確定。
實(shí)際上具體繼承(非virtual繼承)并不會(huì)增加空間或者存取時(shí)間上的額外負(fù)擔(dān),但是虛擬繼承的“間接性”壓抑了“把所有運(yùn)算都移往緩存器執(zhí)行”的優(yōu)化能力,即使通過(guò)類對(duì)象訪問(wèn)編譯器也會(huì)像對(duì)待指針一樣(目前是,編譯器都沒(méi)能識(shí)別出對(duì)“繼承而來(lái)的data member”的存取是通過(guò)一個(gè)非多態(tài)對(duì)象,因而不需要執(zhí)行期的間接存?。?, 效率令人擔(dān)心。但間接性并不會(huì)嚴(yán)重影響非優(yōu)化程序的執(zhí)行效率,各類型繼承效率差別不大。一般來(lái)說(shuō),virtual base class最有效的運(yùn)用形式:一個(gè)抽象的virtual base class,沒(méi)有任何data members。
多態(tài)中的function member訪問(wèn)
在C++中,nonmember/static member/nonstatic member函數(shù)都被轉(zhuǎn)化為完全相同的形式(通過(guò)managling命名處理),所以它們的效率完全相同。
如果是通過(guò)引用和指針調(diào)用虛函數(shù),效率將會(huì)降低,這是由C++多態(tài)性質(zhì)決定的。而多重繼承和虛擬繼承中虛函數(shù)的調(diào)用比單一繼承的效率更低。這個(gè)從上面表格可以清楚的看出來(lái):this指針調(diào)(比如通過(guò)thunk技術(shù)調(diào)整)和多次初始化vptr。當(dāng)然,請(qǐng)記住:通過(guò)對(duì)象訪問(wèn)虛函數(shù)和訪問(wèn)非虛成員函數(shù)效率是一樣的。在調(diào)用虛函數(shù)而又不需要多態(tài)的情況下,可以明確地調(diào)用該函數(shù)實(shí)體:類名::函數(shù)名,壓制由于虛擬機(jī)制而產(chǎn)生的不必要的重復(fù)調(diào)用操作。
this指針地址調(diào)整
多重繼承和虛擬繼承中this指針調(diào)整使得這兩種繼承效率降低,實(shí)際編程時(shí)應(yīng)該有所警惕。下面列出常見的需要調(diào)整this指針的情形:
1、new 派生類給第二(后繼)個(gè)基類指針或通過(guò)第二(后繼)base class調(diào)用派生類虛析構(gòu)函數(shù)
必須調(diào)整Derived對(duì)象的地址,以使其指向Base2 subobject對(duì)象。當(dāng)刪除基類指向的對(duì)象時(shí)必須再一次調(diào)整,使其指向Derived對(duì)象的起始地址,然而這個(gè)調(diào)整只能在執(zhí)行期完成,在編譯時(shí)無(wú)法確定指針指向的對(duì)象類類型。
下次你看到這種情況不要好奇:pBase2不等于pDerived。
Derived* pDerived = new Derived; Base2* pBase2 = pDerived; // Base2為Derived的第二個(gè)基類 pBase2 != pDerived; // 兩者不等
2、通過(guò)派生類指針調(diào)用第二或后繼base class擁有的虛函數(shù)
如果想正確調(diào)用必須在編譯時(shí)調(diào)整派生類指針,以指向后繼base subobject調(diào)用正確的虛函數(shù)。由上面的模型圖可以看到:如果通過(guò)派生類指針調(diào)用mumble函數(shù),而mumble函數(shù)只存在于后繼類的虛函數(shù)表中,故必須調(diào)整之。
3、后繼base class指針調(diào)用返回derived class type的虛函數(shù)并且賦值給另一后繼base class指針時(shí)
示例如下:
Base2* pb1 = new Derived; // 調(diào)整指針指向base2 clss子對(duì)象 Base2* pb2 = pb1->clone(); // pb1被調(diào)整至Derived對(duì)象的地址,產(chǎn)生新的對(duì)象,再次調(diào)整對(duì)象指針指向base2基類子對(duì)象,賦值給pb2。
記?。築ase class指針一定得指向一個(gè)完整的與自身類型相同的對(duì)象或者子對(duì)象地址,不滿足這個(gè)條件的情形都需要this指針的調(diào)整。
詳細(xì)知識(shí)請(qǐng)參考:《Inside The C++ Object Model》。
相關(guān)文章
OpenCV實(shí)現(xiàn)智能視頻監(jiān)控
這篇文章主要為大家詳細(xì)介紹了OpenCV實(shí)現(xiàn)智能視頻監(jiān)控,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08
VSCode添加頭文件(C/C++)的實(shí)現(xiàn)示例
這篇文章主要介紹了VSCode添加頭文件(C/C++)的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
C語(yǔ)言素?cái)?shù)(質(zhì)數(shù))判斷的3種方法舉例
這篇文章主要給大家介紹了關(guān)于C語(yǔ)言素?cái)?shù)(質(zhì)數(shù))判斷的3種方法,質(zhì)數(shù)是只能被1或者自身整除的自然數(shù)(不包括1),稱為質(zhì)數(shù),文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-11-11
詳解C++類的成員函數(shù)做友元產(chǎn)生的循環(huán)依賴問(wèn)題
這篇文章主要為大家詳細(xì)介紹了C++類的成員函數(shù)做友元產(chǎn)生的循環(huán)依賴問(wèn)題,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2022-03-03
Visual?Studio?2022使用MinGW來(lái)編譯調(diào)試C/C++程序的圖文教程
這篇文章主要介紹了Visual?Studio?2022使用MinGW來(lái)編譯調(diào)試C/C++程序,以實(shí)例來(lái)簡(jiǎn)單介紹一下VS2022中如何使用MinGW來(lái)編譯、調(diào)試C/C++程序,需要的朋友可以參考下2022-08-08

