内存首地址为1000h_C++虚继承,菱形继承,内存分布
前言
在敘述C++虛繼承之前,我先給大家拋出一個問題。例如現在有4個類,分別是class A, class B, class C, class D。它們的關系如下圖。
如上如所示,class B和class C都繼承class A;class D又繼承class B和C。
我們用代碼展示如下。
class A {public: void fun() { std::cout << "class A fun" << std::endl; } int data; };class B : public A { };class C : public A { };class D : public B, public C {};如上面代碼所示,當class B和class C都繼承了class A。由單繼承的原理我們可知,類B和類C中都繼承了類A的成員變量data。所以類B的對象大小和類c的對象大小都為4。那么當類D
繼承類B和類C,按照單繼承的原理,派生類是要繼承基類的public成員變量,因此類D的對象中會有來自類B的成員變量data,和類C的成員變量data。
因此,當實例化一個class D的時候,其對象的大小應該為8。其內存分布圖為
但是呢,如果我們需要對data進行操作,我們可以直接寫成下面這樣嗎?
D d; //實例化對象d.data = 100; //?可以這樣嗎?//我們用編譯器編譯后,報出了如下的錯誤。//error: request for member ‘data’ is ambiguous//表明我們使用的data是不明確的,模棱兩可的因為對象d中有2個data,如果我們只調用data,編譯器是不知道我們到底要使用哪一個data。所以我們可以使用::(域限定符號)來表明我們要使用哪一個data。比如
D d;d.B::data = 100;d.C::data = 200;貌似,我們好像已經解決了這個“二義性”的問題,但是深究這個二義性,其實更深層次的是因為公共基類A發生了兩次實例化問題。
虛繼承
一
虛繼承的實現是在繼承的時候加上關鍵字virtual。它的作用是只會在最后的派生類中將虛基類的構造函數調用一次,忽略虛基類的中間派生類對虛繼承的構造函數的調用,從而保證虛基類的數據成員不會被初始化多次。
所以,通過虛繼承,我們也能很好的解決多重繼承下公共基類的多份拷貝問題.
例如
class A {public: void fun() { std::cout << "class A fun" << std::endl; } int data; };class B : virtual public A { };class C : virtual public A { };class D : public B, public C { };如上面代碼所示,class B和class C對class A使用了虛繼承。那么當class D繼承class B和C的時候,只能實例化一份class A的對象。所以D的對象中只有一份data數據。其內存分布如下
所以可以直接對類D的對象進行如下操作
D d;d.data = 100;std::cout << "data = " << data << std::endl; //100注意
1. 雖然是虛基類,但是依然可以使用基類的指針或者引用來指向派生類的對象。
2. 虛繼承只是解決了多重繼承中公共基類被實例化多次的問題
二
虛繼承下派生類的對象內存分布分析。
這一節是延續我之前寫的《C++虛函數繼承之對象內存分布》這篇文章,其實也是本篇文章的重點。
虛繼承的派生類的內存布局與普通繼承有很多不同,主要體現如下:
1. 虛繼承的子類,如果本身定義了虛函數,則編譯器會為其生成一個虛函數指針(vptr)及虛函數表。該vptr在對象內存的最前面。這里跟普通的繼承就有區別了:普通的繼承如果基類有虛函數,那么派生類會在基類的虛表之后繼續擴展基類的虛表,與基類共用一個虛函數指針。
2. 虛繼承的子類會單獨保留基類的虛函數指針vptr和虛表。
總而言之,通過虛繼承的子類會生成一個虛基類指針(vbptr)。虛基類表指針總在虛函數表指針之后。所以,如果一個類它是虛繼承的子類并且它有自身的虛函數,那么虛基類指針便在虛函數指針之后,有一個指針大小的偏移量。如果該類沒有虛函數,那么它的虛基類表指針在對象內存最前面。
虛基類表:虛基類指針指向的是虛基類表,虛基類表中也是存儲有多條數據,不過與虛函數表不同的是,虛函數表中每個元素存儲的是虛函數的地址,虛基類表中存儲的是偏移值。第一個元素存儲的是虛基類表指針(vbptr)所在地址到該類內存首地址的偏移值,由上面的分析我們可知,這個值要么是0(該類本身沒有虛函數),要么是-4(該類有虛函數)。虛基類表的第二個,第三個元素記錄依次為該類的最左虛繼承父類,次左虛繼承父類...的內存地址相對于虛基類表指針的偏移值。我們可以通過下圖加深理解。
接下來我們通過幾個案例,來闡述一下虛繼承后,派生類中的內存分布情況。
2.1 單虛擬繼承
接下來我們通過幾個案例,來闡述一下虛繼承后,派生類中的內存分布情況。2.1 單虛擬繼承如上述代碼所示,Child虛繼承類Base。Base類中有2個虛函數,child類中也有自身的虛函數,并且重寫了Base的fun1函數。
在64位系統下,sizeof(Child)=32。具體的內存分布圖如下。
由圖可知。
1. Child類的首地址存放的是Child類自身的虛表指針,指向的是存放自身虛函數地址的虛函數表(VTable)。
2. 接著存放的是虛基類指針,虛基類指針指向的是虛基類表,虛基類表中存放的是偏移值。第一個元素為虛基類指針相對于內存首地址的偏移值,第二個元素為虛繼承的基類的內存與虛基類指針的偏移值。
3. 接著存放Child類成員變量data
4. 接著存放基類Base的虛表指針。虛表指針指向的虛表中,由于Child類重寫了fun1,所以虛函數表的信息也有所改變。
5. 最后存放基類Base的成員變量data
2.2 多虛繼承
class Base1 {public: virtual void Base1_fun1() { std::cout << "Base1::Base1_fun1" << std::endl; } virtual void Base1_fun2() { std::cout << "Base1::Base1_fun2" << std::endl; } int Base1_data;};class Base2 {public: virtual void Base2_fun1() { std::cout << "Base2::Base2_fun1" << std::endl; } virtual void Base2_fun2() { std::cout << "Base2::Base2_fun2" << std::endl; } int Base2_data; };class Child : virtual public Base1, virtual public Base2 {public: void Base1_fun1() override { std::cout << "Child::Base1_fun1" << std::endl; } void Base2_fun1() override { std::cout << "Child::Base2_fun1" << std::endl; } virtual Child_fun3() { std::cout << "Child::Child_fun3" << std::endl; } int child_data;};如上述代碼所示,類child虛繼承類Base1和類Base2。并且類child重寫了類Base1和類Base2的部分虛函數。
具體的內存分布圖如下。不考慮內存對齊,只闡述各個元素的位置。
由上圖可知,
1. Child類實例化一個對象后,其對象內存首地址存放的是指向child類自身虛函數表的虛表指針
2. 接著放入虛基類指針,指向的是虛基類表。虛基類表中存放的是偏移值。第一個元素是虛基類指針與對象內存首地址之間的偏移值;第二個元素存放的是對象中Base1(child先繼承的基類)的首地址與虛基類指針的偏移值;第三個元素存放的是對象內存中Base2(child第二繼承的基類)的首地址與虛基類指針的偏移值
3. 接著存放的是子類的成員變量
4. 接著放入先繼承的Base1的虛表指針
5. 接著放入Base1的成員變量
6. 接著放入后繼承的Base2的虛表指針
7. 最后放入Base2的成員變量
2.3 菱形繼承
菱形繼承其實就是本篇文章最前面提的案例。
class A {public: virtual void fun1() { std::cout << "A::fun1" << std::endl; } int A_data;};class B : virtual public A {public: virtual void fun2() { std::cout << "B::fun2" << std::endl; } int B_data;};class C : virtual public A {public: virtual void fun3() { std::cout << "C::fun3" << std::endl; } int C_data;};class D : public B, public C {public: virtual void fun4() { std::cout << "D::fun4" << std::endl; } int D_data;};如代碼所示,類B和類C虛繼承類A,類D公有實繼承類B和類C,那么類D的對象內存分布圖如下
由上圖可知,因為類B和類C都虛繼承了類A,由上面的案例可知,類B和類C中都有一個vbptr(虛基類指針),然后由于類D繼承了類B和C,所以推斷類D中有2個vbptr,分別來自類B和C。
并且由于類D是實繼承,不同于虛繼承,所以根據之前學習的繼承原理,類D中的虛函數地址應該放在第一個繼承的基類的虛函數表之后(擴展)。并且由于類A是被虛繼承的,類A的數據在類D中只有一份,由類B和類C的虛基類指針,通過偏移量獲取類A的數據。
總結
以上主要從菱形繼承會產生的問題,到引出虛繼承,以及簡單從單虛繼承,多虛繼承,菱形虛繼承這幾個方面簡單闡述了虛繼承的實現,功能,以及虛繼承后的類對象內存分布情況。
當然,本文只是簡單分析了內存分布情況,并沒有實際考慮內存對齊等情況,因此具體問題需要具體分析。
總之,C++是一門高深的語言,我們都不斷的在學習的過程中,望共勉之。
上面如有概念錯誤之處,煩請指正,謝謝!
最后,喜歡的小伙伴麻煩點個贊和關注,以后定期會發一些更多的文章,謝謝!
總結
以上是生活随笔為你收集整理的内存首地址为1000h_C++虚继承,菱形继承,内存分布的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: telnet本机端口不通原因_【Acad
- 下一篇: python使用matplotlib绘图