最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

C++虛繼承原理與類布局深度分析

 更新時(shí)間:2024年04月03日 08:56:35   作者:千松  
這篇文章主要介紹了C++虛繼承原理與類布局分析,本文的目標(biāo)是探究虛繼承的實(shí)現(xiàn)方式和類布局(Class Layout)的具體規(guī)則,需要的朋友可以參考下

引言

在開始深入了解虛繼承之前,我們先要明白C++引入虛繼承的目的。C++有別于其他OOP語言最明顯的特性就是類的多繼承,而菱形繼承結(jié)構(gòu)則是多繼承中最令人頭疼的情況。

我們都知道,當(dāng)派生類繼承基類時(shí),派生類內(nèi)部會(huì)保存一份基類數(shù)據(jù)的副本。在D->B|CB|C->A的菱形繼承結(jié)構(gòu)中,BC各自存有一份A成員變量的副本,這導(dǎo)致D繼承BC后同時(shí)保存了兩份A成員變量,這就導(dǎo)致了空間浪費(fèi)和語法二義性的問題。

所以C++引入了虛繼承,用于解決菱形繼承導(dǎo)致的數(shù)據(jù)冗余。

本文的目標(biāo)是探究虛繼承的實(shí)現(xiàn)方式和類布局(Class Layout)的具體規(guī)則,主要內(nèi)容源自于本人對(duì)C++: Under the Hood的解讀和提煉。

不過在開始之前,我們需要先熟悉一下普通繼承下的類布局,方便與之后的虛繼承進(jìn)行對(duì)比。

請(qǐng)注意,以下用于分析的數(shù)據(jù)皆來自于MSVC的編譯結(jié)果。C++標(biāo)準(zhǔn)定義了一些基本規(guī)范,但不同編譯器的實(shí)現(xiàn)方式可能會(huì)有所差異,所以內(nèi)容僅具有一定的參考性。

單繼承

以下是由A類派生B類的單繼承例子:

none

class A
{
public:
    int a1;
    int a2;
};

none

class B : public A
{
public:
    int b1;
    int b2;
};

通過在VS中啟用Class Layout的輸出,我們可以得到以下內(nèi)容:

none

class A	size(8):
	+---
 0	| a1
 4	| a2
	+---
class B	size(16):
	+---
 0	| +--- (base class A)
 0	| | a1
 4	| | a2
	| +---
 8	| b1
12	| b2
	+---

Visual Studio中查看類布局的方法可以參考http://m.fzitv.net/article/208240.htm。

看起來可能有點(diǎn)抽象,它其實(shí)是等價(jià)于下圖中的內(nèi)容:

由于派生類繼承了其基類的所有屬性和行為,因此派生類的每個(gè)實(shí)例都將包含基類實(shí)例數(shù)據(jù)的完整副本。在B中,A的成員數(shù)據(jù)擺放在B的成員數(shù)據(jù)之前。雖然標(biāo)準(zhǔn)并沒有如此規(guī)定,但是當(dāng)我們需要將B類的地址嵌入A類的指針時(shí)(例如:A *p = new B();),這種布局不需要再添加額外的位移,就可以使指針指向A數(shù)據(jù)段的開頭(在接下來的多繼承中更能體現(xiàn)這么做的好處)。圖中A*、B*指針指向的位置也體現(xiàn)了這一點(diǎn)。

因此,在單繼承的類層次結(jié)構(gòu)中,每個(gè)派生類中引入的新實(shí)例數(shù)據(jù)只是簡單地附加到基類的布局末尾。

多繼承

none

class A
{
public:
    int a1;
    int a2;
};

none

class B
{
public:
    int b1;
    int b2;
};

none

class C : public A, public B
{
public:
    int c1;
    int c2;
};

C多重繼承自AB,與單繼承一樣,C包含每個(gè)基類實(shí)例數(shù)據(jù)的副本,并且置于類的最前方。與單繼承不同是,多繼承不可能使每個(gè)基類數(shù)據(jù)的起始地址都位于派生類的開頭。從圖中也可以看出,在基類A占據(jù)起始位置后,基類B只能保存在偏移量為8的位置。這就使得將C*轉(zhuǎn)換為A*B*時(shí)的操作出現(xiàn)了差異。

none

C c;
(void *)(A *)&c == (void *)&c
(void *)(B *)&c > (void *)&c
(void *)(B *)&c == (void*)(sizeof (A) + (char *)&c)

這幾個(gè)判斷語句的結(jié)果都為true,因此可以看出當(dāng)C*轉(zhuǎn)為B*時(shí),會(huì)在原地址的基礎(chǔ)上進(jìn)行偏移。這也是多繼承帶來的開銷之一。

編譯器實(shí)現(xiàn)可以采用任何順序布置基類實(shí)例和派生類實(shí)例數(shù)據(jù)。MSVC通常的做法是先按聲明順序布局基類實(shí)例,然后按聲明順序布置派生類的新數(shù)據(jù)成員。 不過在后續(xù)的例子中我們將會(huì)看到,當(dāng)部分基類具有虛基類表(或虛函數(shù)表)而其他基類沒有時(shí),情況就不一定如此了。

菱形繼承

現(xiàn)在就搬出我們?cè)谖恼麻_頭提到的菱形繼承的例子,來看看具體的布局是怎么樣的。

none

class A
{
public:
    int a1;
    int a2;
};

none

class B : public A
{
public:
    int b1;
    int b2;
};

none

class C : public A
{
public:
    int c1;
    int c2;
};

none

class D : public B, public C
{
public:
    int d1;
    int d2;
};

BC都繼承了A,因此也都保存了一份基類A的實(shí)例數(shù)據(jù)副本。

當(dāng)類D同時(shí)繼承了類BC之后,也完整地保存了BC的實(shí)例數(shù)據(jù)副本,也就導(dǎo)致D中出現(xiàn)了兩份A的實(shí)例數(shù)據(jù)副本。

編譯器不能確定我們究竟是要訪問從B繼承來的A成員,還是從C繼承來的A成員,從D*轉(zhuǎn)換到A*的偏移量也無法確定。因此,下面這些操作都是具有二義性的,不能成功編譯:

none

D d;
d.a1 = 1; 			// E0266	"D::a1" 不明確
A *p_a = (A *)&d; 	// C2594	“類型強(qiáng)制轉(zhuǎn)換”: 從“D *”到“A *”的轉(zhuǎn)換不明確

想要成功執(zhí)行的話,就必須顯式地聲明訪問路徑,以消除二義性:

none

D d;
d.B::a1 = 1; 			// 或者d.C::a1
A *p_a = (A *)(B *)&d; 	// 或者(A *)(C *)&d

虛繼承

為了解決這一問題,C++引入了虛繼承的概念。在僅保留一份重復(fù)的實(shí)例數(shù)據(jù)副本的情況下,通過虛基類表(vbtable)來訪問共享的實(shí)例數(shù)據(jù)。聽起來有些難以理解,所以接下來我會(huì)通過分析虛繼承下的類布局來解釋虛繼承語法的實(shí)現(xiàn)。

我們先來分析單繼承情況下,虛繼承與普通繼承之間的類布局差異。

none

class A
{
public:
    int a1;
    int a2;
};

none

class B : public A
{
public:
    int b1;
    int b2;
};

none

class C : virtual public A
{
public:
    int c1;
    int c2;
};

A為基類,B繼承于A,C虛繼承于A。

通過對(duì)比BC的類布局我們可以發(fā)現(xiàn)兩個(gè)明顯的差異:

  • 虛繼承中,派生類布局的起始位置增加了vbptr指針,該指針指向vbtable
  • 虛繼承中,基類的實(shí)例數(shù)據(jù)副本被放置在了派生類的末尾

vbtable中的兩個(gè)條目也很好理解,我們首先要知道XdYvbptrZ表示的是在X類中,YvbptrZ類入口的偏移量。因此:

  • 第一條記錄CdCvbptrC = 0表示,C類中,CvbptrC類入口的偏移量為0。
  • 第二條記錄CdCvbptrA = 16表示,C類中,CvbptrA類入口的偏移量為16。從圖中也可以看出C類中,C::vbptr的保存位置為0,A類的入口位于16,因此偏移量為16

在數(shù)據(jù)訪問的過程中,需要用到vbtable中的偏移量來計(jì)算訪問地址,這就涉及到了查表+偏移的操作。因此,虛繼承的訪問開銷會(huì)比前面在多繼承中提到的固定偏移計(jì)算來得更大,與此同時(shí)vbptrvbtable也造成了額外的內(nèi)存開銷。

從單繼承的例子來看,虛繼承帶來了更大的時(shí)間和內(nèi)存開銷,但卻沒有體現(xiàn)出任何的額外優(yōu)勢(shì)。并且也看不出vbptrvbtable存在的必要性,畢竟為什么我們不直接讓A* = C* + 16?

而接下來通過菱形繼承的例子,我們就會(huì)明白這種做法的必要性。

虛繼承——菱形繼承

none

class A
{
public:
    int a1;
    int a2;
};

none

class B : virtual public A
{
public:
    int b1;
    int b2;
};

none

class C : virtual public A
{
public:
    int c1;
    int c2;
};

none

class D : public B, public C
{
public:
    int d1;
    int d2;
};

需要注意,在這個(gè)例子中BC虛繼承于A,而D則是普通繼承于BC。

在為菱形繼承添加上虛繼承之后,我們可以明確地看到BC結(jié)尾的A實(shí)例數(shù)據(jù)副本,在D的結(jié)尾被合并成了一份。與此同時(shí),編譯器根據(jù)D的布局結(jié)構(gòu)創(chuàng)建了新的vbtable,BCvbptr也被修改為指向新的vbtable

現(xiàn)在我們就可以解答前面提出的問題:“為什么不直接讓`A* = C* + 16呢?”

從圖中就可以看出,在C類的布局中,C* + 16 == A*是成立的,因此以下代碼的運(yùn)行結(jié)果是1

none

C* p_c = new C();
A* p_a = p_c;		// 編譯器自動(dòng)轉(zhuǎn)換的結(jié)果
printf("%d", (void*)p_a == (void*)(16 + (char*)p_c)); // 返回1

而在D類之中,C* + 16訪問的就是D::d1的地址了,這種做法明顯是錯(cuò)誤的,因此代碼的運(yùn)行結(jié)果是0

none

C* p_c = new D(); // 注意:這里的C*來源于類型D
A* p_a = p_c;
printf("%d", (void*)p_a == (void*)(16 + (char*)p_c)); // 返回0

所以根本的問題在于,不同類中的A*相對(duì)于C*的位置是不固定的,在運(yùn)行時(shí)多態(tài)的情況下,我們無法僅在編譯階段計(jì)算出確定的偏移量。

但有了vbptrvbtable之后,無論是C類的C*還是D類的C*,我們都可以訪問當(dāng)前vbptr所指向的vbtable獲取偏移量。而vbptrvbtable都是可以在編譯時(shí)根據(jù)類布局來確定的。所以下面的代碼中,無論C*的來源是C類還是D類,運(yùn)行的結(jié)果始終為1

none

C* p_c = new D();
A* p_a = p_c;
int* vbptr_c = *(int**)p_c; // 這里根據(jù)C類的布局知道vbptr位于C*的起始位置(編譯時(shí)確定)
printf("%d", (void*)p_a == (void*)(*(vbptr_c + 1) + (char*)p_c)); // vbptr_c + 1是因?yàn)锳*偏移量位于vbtable[1](編譯時(shí)確定)

虛表指針(vbptr)的位置

關(guān)于虛繼承的實(shí)現(xiàn)方式已經(jīng)解釋的差不多了,接下來我們?cè)俳榻B幾種類布局的情況,以幫助你更好地理解這些概念。

讓我們先復(fù)習(xí)一下上一個(gè)章節(jié)中的例子來說明:

none

class A
{
public:
    int a1;
    int a2;
};
class C : virtual public A
{
public:
    int c1;
    int c2;
};

我們已經(jīng)介紹過了這個(gè)布局,C虛繼承A后,在起始位置添加了vbptr,并將A的實(shí)例數(shù)據(jù)副本布置在了末尾。

讓我們把情況弄得稍微復(fù)雜一些:

none

class A
{
public:
    int a1;
    int a2;
};
class B // 注意,這次B沒有繼承A
{
public:
    int b1;
    int b2;
};
class C : virtual public A, public B
{
public:
    int c1;
    int c2;
};

我們讓C虛繼承A的同時(shí),再普通繼承B。這次C發(fā)生了兩個(gè)變化:

  • vbptr的位置從0變?yōu)榱?code>8,也就是說vbptr的行為似乎和普通成員變量一樣,被布置在基類的成員之后。注意我這里說的是"似乎",因?yàn)橄乱徽鹿?jié)我們就會(huì)找到特例。
  • 第二個(gè)變化則是vbtable中的CdCvbptrC的值從0變?yōu)榱?code>-8,這其實(shí)就是受到vbptr位置變化的影響。

共用虛基類表(vbtable)

介紹完“正常情況”后,我們?cè)賮砜匆粋€(gè)特殊情況。

none

class A
{
public:
    int a1;
    int a2;
};

none

class B : virtual public A
{
public:
    int b1;
    int b2;
};

none

class C : virtual public A, public B
{
public:
    int c1;
    int c2;
};

這次我們讓B虛繼承于A,然后和上一章一樣,讓C虛繼承A的同時(shí),再普通繼承B。

可以看到,由于BC都有vbptr,并且具有公共的虛基類A,導(dǎo)致二者的vbptr合并到了起始位置,并且共用一個(gè)vbtable。

后續(xù)我經(jīng)過幾次測試后發(fā)現(xiàn)一個(gè)規(guī)律,當(dāng)派生類同時(shí)進(jìn)行虛繼承和非虛繼承的情況下,只要非虛繼承的基類中存在vbptr指針,那么派生類的虛繼承就會(huì)與之共用一個(gè)vbptrvbtable。

參考資料

C++: Under the Hood

How virtual inheritance is implemented in memory by c++ compiler?

到此這篇關(guān)于C++虛繼承原理與類布局分析的文章就介紹到這了,更多相關(guān)C++虛繼承原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

  • C++中的explicit關(guān)鍵字實(shí)例淺析

    C++中的explicit關(guān)鍵字實(shí)例淺析

    在C++程序中很少有人去使用explicit關(guān)鍵字,不可否認(rèn),在平時(shí)的實(shí)踐中確實(shí)很少能用的上,再說C++的功能強(qiáng)大,往往一個(gè)問題可以利用好幾種C++特性去解決。接下來給大家介紹 C++中的explicit關(guān)鍵字,需要的朋友可以參考下
    2017-03-03
  • OPENCV批量讀取圖片實(shí)現(xiàn)方法

    OPENCV批量讀取圖片實(shí)現(xiàn)方法

    下面小編就為大家?guī)硪黄狾PENCV批量讀取圖片實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的?,F(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-06-06
  • C語言深入細(xì)致講解動(dòng)態(tài)內(nèi)存管理

    C語言深入細(xì)致講解動(dòng)態(tài)內(nèi)存管理

    動(dòng)態(tài)內(nèi)存是相對(duì)靜態(tài)內(nèi)存而言的。所謂動(dòng)態(tài)和靜態(tài)就是指內(nèi)存的分配方式。動(dòng)態(tài)內(nèi)存是指在堆上分配的內(nèi)存,而靜態(tài)內(nèi)存是指在棧上分配的內(nèi)存,本文帶你深入探究C語言中動(dòng)態(tài)內(nèi)存的管理
    2022-05-05
  • opencv實(shí)現(xiàn)讀取視頻保存視頻

    opencv實(shí)現(xiàn)讀取視頻保存視頻

    這篇文章主要為大家詳細(xì)介紹了opencv實(shí)現(xiàn)讀取視頻保存視頻,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-01-01
  • vs2019創(chuàng)建dll以及使用的圖文教程

    vs2019創(chuàng)建dll以及使用的圖文教程

    本文主要介紹了vs2019創(chuàng)建dll以及使用的圖文教程,文中通過圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-07-07
  • OpenGL掃描線填充算法詳解

    OpenGL掃描線填充算法詳解

    這篇文章主要為大家詳細(xì)介紹了OpenGL實(shí)現(xiàn)掃描線填充算法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2020-02-02
  • C++實(shí)現(xiàn)二維圖形的傅里葉變換

    C++實(shí)現(xiàn)二維圖形的傅里葉變換

    這篇文章主要介紹了C++實(shí)現(xiàn)二維圖形的傅里葉變換的方法,是C++程序設(shè)計(jì)里一個(gè)重要的應(yīng)用,需要的朋友可以參考下
    2014-08-08
  • 16種C語言編譯警告(Warning)類型的解決方法

    16種C語言編譯警告(Warning)類型的解決方法

    由于編譯的警告各種各樣,根本不可以一一羅列出來,下面只是列舉出比較典型的十六種警告,還有一些警告,大家只要根據(jù)字面意思,就可以很快的查找出來,并解決之。希望對(duì)大家有所幫助。
    2014-08-08
  • 最新評(píng)論

    永新县| 宜川县| 彭山县| 丁青县| 金溪县| 内黄县| 汪清县| 滨州市| 图片| 平和县| 三原县| 黔西县| 永登县| 深水埗区| 吉木萨尔县| 驻马店市| 杭州市| 湖南省| 碌曲县| 天水市| 克什克腾旗| 大厂| 封开县| 六盘水市| 贞丰县| 台州市| 石泉县| 芦溪县| 庄浪县| 平乐县| 汉寿县| 常熟市| 达州市| 黔西| 新宁县| 昔阳县| 尉氏县| 教育| 客服| 容城县| 沙河市|