關(guān)于C++虛繼承的內(nèi)存模型問(wèn)題
1、前言
C++虛繼承的內(nèi)存模型是一個(gè)經(jīng)典的問(wèn)題,其具體實(shí)現(xiàn)依賴(lài)于編譯器,可能會(huì)出現(xiàn)較大差異,但原理和最終的目的是大體相同的。本文將對(duì)g++中虛繼承的內(nèi)存模型進(jìn)行詳細(xì)解析。
2、多繼承存在的問(wèn)題
C++的多繼承是指從多個(gè)直接基類(lèi)中產(chǎn)生派生類(lèi)的能力,多繼承的派生類(lèi)繼承了所有父類(lèi)的成員。從概念上來(lái)講這是非常簡(jiǎn)單的,但是多個(gè)基類(lèi)的相互交織可能會(huì)帶來(lái)錯(cuò)綜復(fù)雜的設(shè)計(jì)問(wèn)題,命名沖突就是不可回避的一個(gè),比如典型的是菱形繼承,如圖2-1所示:
在圖2-1中,類(lèi)A
派生出類(lèi)B
和類(lèi)C
,類(lèi)D
繼承自類(lèi)B
和類(lèi)C
,這個(gè)時(shí)候類(lèi)A
中的成員變量和成員函數(shù)繼承到類(lèi)D
中變成了兩份,一份來(lái)自A–>B–>D
這條路徑,另一份來(lái)自A–>C–>D
這條路徑。
在一個(gè)派生類(lèi)中保留間接基類(lèi)的多份同名成員,雖然可以在不同的成員變量中分別存放不同的數(shù)據(jù),但大多數(shù)情況下這是多余的,因?yàn)楸A舳喾莩蓡T變量不僅占用較多的存儲(chǔ)空間,還容易產(chǎn)生命名沖突。假如類(lèi)A
有一個(gè)成員變量a
,那么在類(lèi)D
中直接訪問(wèn)a
就會(huì)產(chǎn)生歧義,編譯器不知道它究竟來(lái)自A -->B–>D
這條路徑,還是來(lái)自A–>C–>D
這條路徑。下面是菱形繼承的代碼實(shí)現(xiàn):
#include <iostream> #include <stdint.h> class A { public: long a; }; class B: public A { public: long b; }; class C: public A { public: long c; }; class D: public B, public C { public: void seta(long v) { a = v; } // 命名沖突 void setb(long v) { b = v; } // 正確 void setc(long v) { c = v; } // 正確 void setd(long v) { d = v; } // 正確 private: long d; }; int main(int argc, char* argv[]) { D d; }
這段代碼就是圖2-1所示的菱形繼承的具體實(shí)現(xiàn),可以看到在類(lèi)D
的seta()
方法中,代碼試圖直接訪問(wèn)間接基類(lèi)的成員變量a
,結(jié)果發(fā)生了錯(cuò)誤,因?yàn)轭?lèi)B
和類(lèi)C
中都有成員變量a
(都是從類(lèi)A
繼承的),編譯器不知道選用哪一個(gè),所以產(chǎn)生了歧義。
為了消除歧義,我們可以在使用a
時(shí)指明它具體來(lái)自哪個(gè)類(lèi),代碼如下:
void seta(long v) { B::a = v; } /* 或 */ void seta(long v) { C::a = v; }
使用GDB查看變量d的內(nèi)存布局,如圖2-2所示:
于是我們可以畫(huà)出變量d的內(nèi)存布局,如圖2-3所示:
3、虛繼承簡(jiǎn)介
為了解決多繼承時(shí)命名沖突和冗余數(shù)據(jù)的問(wèn)題,C++提出了虛繼承這個(gè)概念,虛繼承可以使得在派生類(lèi)中只保留一份間接基類(lèi)的成員。使用方式就是在繼承方式前面加上virtual
關(guān)鍵字修飾,示例代碼如下(基于前面的例子修改):
#include <iostream> #include <stdint.h> class A { public: long a; }; class B: virtual public A { public: long b; }; class C: virtual public A { public: long c; }; class D: public B, public C { public: void seta(long v) { a = v; } // 現(xiàn)在不會(huì)沖突了 void setb(long v) { b = v; } // 正確 void setc(long v) { c = v; } // 正確 void setd(long v) { d = v; } // 正確 private: long d; }; int main(int argc, char* argv[]) { D d; }
可以看到這段代碼使用虛繼承重新實(shí)現(xiàn)了前面提到的那個(gè)菱形繼承,這樣在派生類(lèi)D
中就只保留了一份間接基類(lèi)A
的成員變量a
了,后續(xù)再直接訪問(wèn)a
就不會(huì)出現(xiàn)歧義了。虛繼承的目的是讓某個(gè)類(lèi)做出聲明,承諾愿意共享它的基類(lèi),這個(gè)被共享的基類(lèi)就稱(chēng)為虛基類(lèi)(Virtual Base Class),本例中的類(lèi)A
就是一個(gè)虛基類(lèi)。在這種機(jī)制下,不論虛基類(lèi)在繼承體系中出現(xiàn)了多少次,在派生類(lèi)中都只包含一份虛基類(lèi)的成員。本例的繼承關(guān)系如圖3-1所示:
從這個(gè)新的繼承體系中我們可以發(fā)現(xiàn)虛繼承的一個(gè)特征:必須在虛派生的真實(shí)需求出現(xiàn)前就已經(jīng)完成虛派生的操作。在圖3-1中,我們是當(dāng)定義類(lèi)D
時(shí)才出現(xiàn)了對(duì)虛派生的需求,但是如果類(lèi)B
和類(lèi)C
不是從類(lèi)A
虛派生得到的,那么類(lèi)D
還是會(huì)保留間接基類(lèi)A
的兩份成員,示例代碼如下:
#include <iostream> #include <stdint.h> class A { public: long a; }; class B: public A { public: long b; }; class C: public A { public: long c; }; class D: virtual public B, virtual public C { public: void seta(long v) { a = v; } // 錯(cuò)誤,不能等到定義類(lèi)D時(shí)再來(lái)做虛繼承的工作 void setb(long v) { b = v; } // 正確 void setc(long v) { c = v; } // 正確 void setd(long v) { d = v; } // 正確 private: long d; }; int main(int argc, char* argv[]) { D d; }
換個(gè)角度講,虛派生只影響從指定了虛基類(lèi)的派生類(lèi)中進(jìn)一步派生出來(lái)的類(lèi),它不會(huì)影響派生類(lèi)本身。在實(shí)際開(kāi)發(fā)中,位于中間層次的基類(lèi)將其繼承聲明為虛繼承一般不會(huì)帶來(lái)什么問(wèn)題。通常情況下,使用虛繼承的類(lèi)層次是由一個(gè)人或者一個(gè)項(xiàng)目組一次性設(shè)計(jì)完成的。對(duì)于一個(gè)獨(dú)立開(kāi)發(fā)的類(lèi)來(lái)說(shuō),很少需要基類(lèi)中的某一個(gè)類(lèi)是虛基類(lèi),況且新類(lèi)的開(kāi)發(fā)者也無(wú)法改變已經(jīng)存在的類(lèi)體系。
4、虛繼承在標(biāo)準(zhǔn)庫(kù)中的使用
C++標(biāo)準(zhǔn)庫(kù)中的iostream
就是一個(gè)虛繼承的典型案例。iostream
是從istream
和ostream
直接繼承而來(lái)的,而istream
和ostream
又都繼承自一個(gè)名為ios
的類(lèi),這個(gè)就是一個(gè)典型的菱形繼承。此時(shí)istream
和ostream
必須采用虛繼承,否則將導(dǎo)致iostream
中保留兩份ios
的成員。
iostream
相關(guān)的源代碼如下(從gcc-2.95.3
版本中摘錄出來(lái)的,內(nèi)容有所省略):
struct _ios_fields { // The data members of an ios. streambuf *_strbuf; ostream* _tie; int _width; __fmtflags _flags; _IO_wchar_t _fill; __iostate _state; __iostate _exceptions; int _precision; void *_arrays; /* Support for ios::iword and ios::pword. */ }; class ios : public _ios_fields {...}; class istream : virtual public ios {...}; class ostream : virtual public ios {...}; class iostream : public istream, public ostream { public: iostream() { } iostream(streambuf* sb, ostream*tied=NULL); };
5、虛繼承下派生類(lèi)的內(nèi)存布局解析
g++中是沒(méi)有所謂的虛基類(lèi)表的(據(jù)說(shuō)vs是有單獨(dú)一個(gè)虛基類(lèi)表的),只有一個(gè)虛表,由于平時(shí)用的比較多的是虛函數(shù),所以一般情況下都直接管它叫做虛函數(shù)表,在g++編譯環(huán)境下這種叫法其實(shí)是不嚴(yán)謹(jǐn)?shù)?。測(cè)試程序如下:
#include <iostream> #include <stdint.h> class A { public: long a; }; class B: virtual public A { public: long b; }; class C: virtual public A { public: long c; }; class D: public B, public C { public: void seta(long v) { a = v; } void setb(long v) { b = v; } void setc(long v) { c = v; } void setd(long v) { d = v; } private: long d; }; int main(int argc, char* argv[]) { D d; d.seta(1); d.setb(2); d.setc(3); d.setd(4); }
類(lèi)D
在當(dāng)前編譯器(GCC 4.8.5
)下的內(nèi)存布局如圖5-1所示:
從圖5-1中可以看出這個(gè)表和之前這篇文章《一文讀懂C++虛函數(shù)的內(nèi)存模型》講的虛函數(shù)表是差不多的,就多了一個(gè)vbase_offset
而已。因?yàn)檫@里的類(lèi)設(shè)計(jì)比較簡(jiǎn)單,沒(méi)有把虛函數(shù)加進(jìn)來(lái),有虛函數(shù)的話(huà)_vptr.B
或者_vptr.C
下面的內(nèi)存空間存儲(chǔ)的就是指向?qū)?yīng)虛函數(shù)的指針了(以下只講_vptr.B
的相關(guān)內(nèi)容,_vptr.C
同理就不贅述了)。
這里可以看到_vptr.B
指向的是虛函數(shù)的起始地址(因?yàn)檫@里沒(méi)有虛函數(shù),所以下面緊接著就是_vptr.C
的內(nèi)容),而不是與它相關(guān)聯(lián)的全部信息的起始地址,事實(shí)上從圖5-1中可以看出_vptr.B - 3
~ _vptr.B
這個(gè)范圍內(nèi)的數(shù)據(jù)都是類(lèi)B
虛表的內(nèi)容(不知道編譯器為什么這么設(shè)計(jì),這里也進(jìn)行揣測(cè)了),這三個(gè)特殊的內(nèi)存地址存儲(chǔ)的內(nèi)容解析如下:
_vptr.B - 1
:這里存儲(chǔ)的是typeinfo for D
,里面的內(nèi)容其實(shí)也是一個(gè)指針,指向的是類(lèi)D
的運(yùn)行時(shí)信息,這些玩意都是為了支持RTTI的。RTTI的相關(guān)內(nèi)容以后會(huì)講,這里就先不多分析了。_vptr.B - 2
:這里存儲(chǔ)的是offset_to_top
,這個(gè)表示的是當(dāng)前的虛表指針距離類(lèi)開(kāi)頭的距離,可以看到對(duì)于_vptr.B
來(lái)說(shuō)這個(gè)值就是0,因?yàn)?code>_vptr.B就存在于類(lèi)D
的起始位置,而對(duì)于_vptr.C
來(lái)說(shuō)這個(gè)值是-16,大家可以算一下_vptr.C
與類(lèi)D
的起始位置確實(shí)是差兩個(gè)地址也就是16個(gè)字節(jié)(64位系統(tǒng)),至于為什么是負(fù)數(shù),這是因?yàn)槎褍?nèi)存是向下增長(zhǎng)的,越往下地址數(shù)值越大。
offset_to_top深度解析:在多繼承中,由于不同基類(lèi)的起點(diǎn)可能處于不同的位置,因此當(dāng)需要將它們轉(zhuǎn)化為實(shí)際類(lèi)型時(shí),this指針的偏移量也不相同。由于實(shí)際類(lèi)型在編譯時(shí)是未知的,這要求偏移量必須能夠在運(yùn)行時(shí)獲取。實(shí)體offset_to_top表示的就是實(shí)際類(lèi)型起始地址到當(dāng)前這個(gè)形式類(lèi)型起始地址的偏移量。在向上動(dòng)態(tài)轉(zhuǎn)換到實(shí)際類(lèi)型時(shí)(即基類(lèi)轉(zhuǎn)派生類(lèi)),讓this指針加上這個(gè)偏移量即可得到實(shí)際類(lèi)型的地址。需要注意的是,由于一個(gè)類(lèi)型即可以被單繼承,也可以被多繼承,因此即使只有單繼承,實(shí)體offset_to_top也會(huì)存在于每一個(gè)多態(tài)類(lèi)型之中。
(這里要注意一點(diǎn)就是offset_to_top只存在于多態(tài)類(lèi)型中,所以我們可以看到在第二小節(jié)那個(gè)例子中,根本就沒(méi)有什么所謂的虛表之類(lèi)的東西,它也就不支持RTTI,最簡(jiǎn)單的大家可以使用dynamic_cast
去試試,會(huì)報(bào)錯(cuò)說(shuō)該類(lèi)型不具備多態(tài)性質(zhì)的。那么問(wèn)題來(lái)了,怎樣才能以最簡(jiǎn)短的方式讓它具備多態(tài)的性質(zhì)呢?很簡(jiǎn)單,定義一個(gè)析構(gòu)函數(shù),用virtual修飾即可)
_vptr.B - 3
:這里存儲(chǔ)的是vbase_offset
,這個(gè)表示的是當(dāng)前虛表指針與其對(duì)應(yīng)的虛基類(lèi)的距離。從圖中可以看出對(duì)于_vptr.B
來(lái)說(shuō)這個(gè)值是40,算一下剛好是_vptr.B
與a
的差距,_vptr.C
同理。
vbase_offset深度解析:以測(cè)試程序?yàn)槔瑢?duì)于類(lèi)型為B的引用,在編譯時(shí),無(wú)法確定它的虛基類(lèi)A它在內(nèi)存中的偏移量。因此,需要在虛表中額外再提供一個(gè)實(shí)體,表明運(yùn)行時(shí)它的基類(lèi)所在的位置,這個(gè)實(shí)體稱(chēng)為vbase_offset,位于offset_to_top上方。
接下來(lái)我們通過(guò)GDB來(lái)驗(yàn)證一下前面講的內(nèi)容,先打印出變量d
的內(nèi)存信息,如圖5-2所示:
從圖5-2中可以看到變量d
的內(nèi)容與前面分析的差不多,接下來(lái)我們來(lái)看一下這兩個(gè)虛表的內(nèi)容,如圖5-3所示:
從圖5-3中可以看出前面的內(nèi)存圖是正確的,接下來(lái)就再看一下變量d
自身的內(nèi)存布局,如圖5-4所示:
圖5-4顯示出的結(jié)果和前面圖5-1的完全一致,到這里調(diào)試就結(jié)束了,由調(diào)試結(jié)果可以知道圖5-1的內(nèi)存模型是正確的。
這里要補(bǔ)充一點(diǎn),就是對(duì)于虛繼承下的類(lèi)
D
,和第二節(jié)那個(gè)沒(méi)有虛繼承的相比,基類(lèi)A
的位置被移動(dòng)到了類(lèi)D
的最末尾,不過(guò)不用擔(dān)心,運(yùn)行時(shí)可以靠vbase_offset
找到它。
6、總結(jié)
本文先是對(duì)虛繼承的概念以及使用場(chǎng)景進(jìn)行了說(shuō)明,然后通過(guò)一個(gè)內(nèi)存模型圖向大家展示了g++下虛繼承的內(nèi)存形態(tài),最后使用GDB查看實(shí)際的內(nèi)存情況來(lái)驗(yàn)證內(nèi)存模型圖的正確性。本文為了更直觀地展示虛繼承的內(nèi)存模型,示例設(shè)計(jì)得很簡(jiǎn)單,類(lèi)的設(shè)計(jì)中只有一個(gè)成員變量而沒(méi)有成員函數(shù)、虛函數(shù)等其它內(nèi)容。本文與前文《一文讀懂C++虛函數(shù)的內(nèi)存模型》相當(dāng)于拋磚引玉,為下文作鋪墊,在下一篇文章中我將對(duì)一些稍微復(fù)雜一點(diǎn)的情景進(jìn)行分析,看看完整形態(tài)的虛表究竟是什么樣的。
到此這篇關(guān)于關(guān)于C++虛繼承的內(nèi)存模型問(wèn)題的文章就介紹到這了,更多相關(guān)C++虛繼承的內(nèi)存模型內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
VisualStudio2022編寫(xiě)C語(yǔ)言的實(shí)現(xiàn)步驟
VisualStudio2022是一款強(qiáng)大的集成開(kāi)發(fā)環(huán)境,可以用來(lái)編寫(xiě)C語(yǔ)言程序,本文主要介紹了VisualStudio2022編寫(xiě)C語(yǔ)言的實(shí)現(xiàn)步驟,具有一定的參考價(jià)值,感興趣的可以了解一下2024-06-06C++使用opencv調(diào)用級(jí)聯(lián)分類(lèi)器來(lái)識(shí)別目標(biāo)物體的詳細(xì)流程
所謂級(jí)聯(lián)分類(lèi)器其實(shí)就是把分類(lèi)器按照一定的順序聯(lián)合到一起,下面這篇文章主要給大家介紹了關(guān)于C++使用opencv調(diào)用級(jí)聯(lián)分類(lèi)器來(lái)識(shí)別目標(biāo)物體的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-05-05VS2010+Opencv+MFC讀取圖像和視頻顯示在Picture控件
這篇文章主要為大家詳細(xì)介紹了VS2010+Opencv+MFC讀取圖像和視頻顯示在Picture控件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08詳細(xì)分析Android中實(shí)現(xiàn)Zygote的源碼
這篇文章主要介紹了詳細(xì)分析Android中實(shí)現(xiàn)Zygote的源碼,包括底層的C/C++代碼以及Java代碼部分入口,需要的朋友可以參考下2015-07-07C語(yǔ)言中無(wú)符號(hào)數(shù)和有符號(hào)數(shù)之間的運(yùn)算
C語(yǔ)言中有符號(hào)數(shù)和無(wú)符號(hào)數(shù)進(jìn)行運(yùn)算默認(rèn)會(huì)將有符號(hào)數(shù)看成無(wú)符號(hào)數(shù)進(jìn)行運(yùn)算,其中算術(shù)運(yùn)算默認(rèn)返回?zé)o符號(hào)數(shù),邏輯運(yùn)算當(dāng)然是返回0或1了。下面通過(guò)一個(gè)例子給大家分享C語(yǔ)言中無(wú)符號(hào)數(shù)和有符號(hào)數(shù)之間的運(yùn)算,一起看看吧2017-09-09淺談C語(yǔ)言共用體和與結(jié)構(gòu)體的區(qū)別
下面小編就為大家?guī)?lái)一篇淺談C語(yǔ)言共用體和與結(jié)構(gòu)體的區(qū)別。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02Opencv2.4.9函數(shù)HoughLinesP分析
這篇文章主要為大家詳細(xì)介紹了Opencv2.4.9函數(shù)HoughLinesP,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01C++實(shí)現(xiàn)LeetCode(20.驗(yàn)證括號(hào))
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(20.驗(yàn)證括號(hào)),本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07