亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

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

引言

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

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

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

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

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

請注意,以下用于分析的數(shù)據(jù)皆來自于MSVC的編譯結(jié)果。C++標(biāo)準(zhǔn)定義了一些基本規(guī)范,但不同編譯器的實現(xiàn)方式可能會有所差異,所以內(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://chabaoo.cn/article/208240.htm

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

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

因此,在單繼承的類層次結(jié)構(gòu)中,每個派生類中引入的新實例數(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包含每個基類實例數(shù)據(jù)的副本,并且置于類的最前方。與單繼承不同是,多繼承不可能使每個基類數(shù)據(jù)的起始地址都位于派生類的開頭。從圖中也可以看出,在基類A占據(jù)起始位置后,基類B只能保存在偏移量為8的位置。這就使得將C*轉(zhuǎn)換為A*B*時的操作出現(xiàn)了差異。

none

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

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

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

菱形繼承

現(xiàn)在就搬出我們在文章開頭提到的菱形繼承的例子,來看看具體的布局是怎么樣的。

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ù)據(jù)副本。

當(dāng)類D同時繼承了類BC之后,也完整地保存了BC的實例數(shù)據(jù)副本,也就導(dǎo)致D中出現(xiàn)了兩份A的實例數(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ù)據(jù)副本的情況下,通過虛基類表(vbtable)來訪問共享的實例數(shù)據(jù)。聽起來有些難以理解,所以接下來我會通過分析虛繼承下的類布局來解釋虛繼承語法的實現(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

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

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

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

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

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

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

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

虛繼承——菱形繼承

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;
};

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

在為菱形繼承添加上虛繼承之后,我們可以明確地看到BC結(jié)尾的A實例數(shù)據(jù)副本,在D的結(jié)尾被合并成了一份。與此同時,編譯器根據(jù)D的布局結(jié)構(gòu)創(chuàng)建了新的vbtableBCvbptr也被修改為指向新的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;		// 編譯器自動轉(zhuǎn)換的結(jié)果
printf("%d", (void*)p_a == (void*)(16 + (char*)p_c)); // 返回1

而在D類之中,C* + 16訪問的就是D::d1的地址了,這種做法明顯是錯誤的,因此代碼的運(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*相對于C*的位置是不固定的,在運(yùn)行時多態(tài)的情況下,我們無法僅在編譯階段計算出確定的偏移量。

但有了vbptrvbtable之后,無論是C類的C*還是D類的C*,我們都可以訪問當(dāng)前vbptr所指向的vbtable獲取偏移量。而vbptrvbtable都是可以在編譯時根據(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*的起始位置(編譯時確定)
printf("%d", (void*)p_a == (void*)(*(vbptr_c + 1) + (char*)p_c)); // vbptr_c + 1是因為A*偏移量位于vbtable[1](編譯時確定)

虛表指針(vbptr)的位置

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

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

none

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

我們已經(jīng)介紹過了這個布局,C虛繼承A后,在起始位置添加了vbptr,并將A的實例數(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的同時,再普通繼承B。這次C發(fā)生了兩個變化:

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

共用虛基類表(vbtable)

介紹完“正常情況”后,我們再來看一個特殊情況。

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的同時,再普通繼承B。

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

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

參考資料

C++: Under the Hood

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

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

相關(guān)文章

  • 使用C++實現(xiàn)類似Qt的信號與槽機(jī)制功能

    使用C++實現(xiàn)類似Qt的信號與槽機(jī)制功能

    信號與槽機(jī)制是 Qt 框架中的核心設(shè)計,用于實現(xiàn)對象之間的解耦通信,在純 C++ 中,我們也可以設(shè)計出類似的機(jī)制,利用模板、函數(shù)指針和哈希表,實現(xiàn)高效且靈活的信號與槽功能,本文給大家介紹了如何使用C++實現(xiàn)類似Qt的信號與槽機(jī)制功能,需要的朋友可以參考下
    2025-01-01
  • C語言科學(xué)計算入門之矩陣乘法的相關(guān)計算

    C語言科學(xué)計算入門之矩陣乘法的相關(guān)計算

    這篇文章主要介紹了C語言科學(xué)計算入門之矩陣乘法的相關(guān)計算,文章中還介紹了矩陣相關(guān)的斯特拉森算法的實現(xiàn),需要的朋友可以參考下
    2015-12-12
  • C++二分查找(折半查找)算法實例詳解

    C++二分查找(折半查找)算法實例詳解

    這篇文章主要介紹了C++二分查找(折半查找)算法,結(jié)合實例形式詳細(xì)分析了二分查找算法的原理、思想、實現(xiàn)方法與相關(guān)操作技巧,需要的朋友可以參考下
    2017-05-05
  • C語言版掃雷游戲

    C語言版掃雷游戲

    這篇文章主要為大家詳細(xì)介紹了C語言版掃雷游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-05-05
  • 詳解C++17中if和switch語句的新特性

    詳解C++17中if和switch語句的新特性

    這篇文章主要為大家詳細(xì)介紹了C++17中if和switch語句的新特性的相關(guān)知識,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2023-12-12
  • C語言之實現(xiàn)棧的基礎(chǔ)創(chuàng)建

    C語言之實現(xiàn)棧的基礎(chǔ)創(chuàng)建

    這篇文章主要介紹了C語言之實現(xiàn)棧的基礎(chǔ)創(chuàng)建,本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-07-07
  • C語言實現(xiàn)單鏈表的基本功能詳解

    C語言實現(xiàn)單鏈表的基本功能詳解

    鏈表是一個結(jié)構(gòu)體實現(xiàn)的一種線性表,它只能從前往后,不可以從后往前,在實現(xiàn)單鏈表的操作時,需要用指針來操作。本文主要介紹了實現(xiàn)單鏈表的基本功能的代碼示例,具有一定價值,感興趣的同學(xué)可以學(xué)習(xí)一下
    2021-11-11
  • C++中的Switch 語句詳情

    C++中的Switch 語句詳情

    在日常的開發(fā)當(dāng)中,我們經(jīng)常會遇到一種情況,我們用一個變量表示狀態(tài)。比如關(guān)閉-激活-完成,當(dāng)我們需要判斷狀態(tài)的時候,就需要羅列if-else語句。今天這篇文章我們就來介紹一下C++ Switch 語句,需要的朋友可以參考一下
    2021-11-11
  • C++的運(yùn)算符你真的了解嗎

    C++的運(yùn)算符你真的了解嗎

    這篇文章主要為大家詳細(xì)介紹了C++的運(yùn)算符,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助
    2022-02-02
  • C++實現(xiàn)LeetCode(769.可排序的最大塊數(shù))

    C++實現(xiàn)LeetCode(769.可排序的最大塊數(shù))

    這篇文章主要介紹了C++實現(xiàn)LeetCode(769.可排序的最大塊數(shù)),本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-07-07

最新評論