天天看點

從記憶體布局看C++虛繼承的實作原理

轉載自:http://blog.csdn.NET/xiejingfa/article/details/48028491

準備工作

1、VS2012使用指令行選項檢視對象的記憶體布局

微軟的Visual Studio提供給使用者顯示C++對象在記憶體中的布局的選項:/d1reportSingleClassLayout。使用方法很簡單,直接在[工具(T)]選項下找到“Visual Studio指令提示(C)”後點選即可。切換到cpp檔案所在目錄下輸入如下的指令即可

      c1 [filename].cpp /d1reportSingleClassLayout[className]

其中[filename].cpp就是我們想要檢視的class所在的cpp檔案,[className]指我們想要檢視的class的類名。(下面舉例說明...)

2、檢視普通多繼承子類的記憶體布局

既然我們今天講的是虛基類和虛繼承,我們就先用上面介紹的指令提示工具檢視一下普通多繼承子類的記憶體布局,可以跟後文虛繼承子類的記憶體布局情況加以比較。

我們建立一個名叫NormalInheritance的cpp檔案,輸入一下内容。

[cpp] view plain copy print ?

  1. // 基類A  
  2. class A  
  3. {  
  4. public:  
  5.     int dataA;  
  6. };  
  7. class B : public A  
  8. {  
  9. public:  
  10.     int dataB;  
  11. };  
  12. class C : public A  
  13. {  
  14. public:  
  15.     int dataC;  
  16. };  
  17. class D : public B, public C  
  18. {  
  19. public:  
  20.     int dataD;  
  21. };  
/**
	普通繼承(沒有使用虛基類)
*/

// 基類A
class A
{
public:
	int dataA;
};

class B : public A
{
public:
	int dataB;
};

class C : public A
{
public:
	int dataC;
};

class D : public B, public C
{
public:
	int dataD;
};
           

上面是一個簡單的多繼承例子,我們啟動Visual Studio指令提示功能,切換到NormalInheritance.cpp檔案所在目錄,輸入一下指令:

c1  NormalInheritance.cpp /d1reportSingleClassLayoutD

我們可以看到class D的記憶體布局如下:

從記憶體布局看C++虛繼承的實作原理

從類D的記憶體布局可以看到A派生出B和C,B和C中分别包含A的成員。再由B和C派生出D,此時D包含了B和C的成員。這樣D中就總共出現了2個A成員。大家注意到左邊的幾個數字,這幾個數字表明了D中各成員在D中排列的起始位址,D中的五個成員變量(B::dataA、dataB、C::dataA、dataC、dataD)各占用4個位元組,sizeof(D) = 20。

為了跟後文加以比較,我們再來看看B和C的記憶體布局:

從記憶體布局看C++虛繼承的實作原理
從記憶體布局看C++虛繼承的實作原理

虛繼承的記憶體分布情況

上面我們看到了普通多繼承子類的記憶體分布情況,下面我們進入主題,來看看典型的菱形虛繼承子類的記憶體分布情況。

我們建立一個名叫VirtualInheritance的cpp檔案,輸入一下内容:

[cpp] view plain copy print ?

  1. #include <iostream>  
  2. // 基類A  
  3. class A  
  4. {  
  5. public:  
  6.     int dataA;  
  7. };  
  8. class B : virtual public A  
  9. {  
  10. public:  
  11.     int dataB;  
  12. };  
  13. class C : virtual public A  
  14. {  
  15. public:  
  16.     int dataC;  
  17. };  
  18. class D : public B, public C  
  19. {  
  20. public:  
  21.     int dataD;  
  22. };  
/**
	虛繼承(虛基類)
*/

#include <iostream>

// 基類A
class A
{
public:
	int dataA;
};

class B : virtual public A
{
public:
	int dataB;
};

class C : virtual public A
{
public:
	int dataC;
};

class D : public B, public C
{
public:
	int dataD;
};
           

VirtualInheritance.cpp和NormalInheritance.cpp的不同點在與C和C繼承A時使用了virtual關鍵字,也就是虛繼承。同樣,我們看看B、C、D類的記憶體布局情況:

從記憶體布局看C++虛繼承的實作原理
從記憶體布局看C++虛繼承的實作原理
從記憶體布局看C++虛繼承的實作原理

我們可以看到,菱形繼承體系中的子類在記憶體布局上和普通多繼承體系中的子類類有很大的不一樣。對于類B和C,sizeof的值變成了12,除了包含類A的成員變量dataA外還多了一個指針vbptr,類D除了繼承B、C各自的成員變量dataB、dataA和自己的成員變量外,還有兩個分别屬于B、C的指針。

那麼類D對象的記憶體布局就變成如下的樣子:

vbptr:繼承自父類B中的指針

int dataB:繼承自父類B的成員變量

vbptr:繼承自父類C的指針

int dataC:繼承自父類C的成員變量

int dataD:D自己的成員變量

int A:繼承自父類A的成員變量

顯然,虛繼承之是以能夠實作在多重派生子類中隻儲存一份共有基類的拷貝,關鍵在于vbptr指針。那vbptr到底指的是什麼?又是如何實作虛繼承的呢?其實上面的類D記憶體布局圖中已經給出答案:

從記憶體布局看C++虛繼承的實作原理

實際上,vbptr指的是虛基類表指針(virtual base table pointer),該指針指向了一個虛表(virtual table),虛表中記錄了vbptr與本類的偏移位址;第二項是vbptr到共有基類元素之間的偏移量。在這個例子中,類B中的vbptr指向了虛表D::[email protected]@,虛表表明公共基類A的成員變量dataA距離類B開始處的位移為20,這樣就找到了成員變量dataA,而虛繼承也不用像普通多繼承那樣維持着公共基類的兩份同樣的拷貝,節省了存儲空間。

為了進一步确定上面的想法是否正确,我們可以寫一個簡單的程式加以驗證:

[cpp] view plain copy print ?

  1. int main()  
  2. {  
  3.     D* d = new D;  
  4.     d->dataA = 10;  
  5.     d->dataB = 100;  
  6.     d->dataC = 1000;  
  7.     d->dataD = 10000;  
  8.     B* b = d; // 轉化為基類B  
  9.     C* c = d; // 轉化為基類C  
  10.     A* fromB = (B*) d;  
  11.     A* fromC = (C*) d;  
  12.     std::cout << "d address    : " << d << std::endl;  
  13.     std::cout << "b address    : " << b << std::endl;  
  14.     std::cout << "c address    : " << c << std::endl;  
  15.     std::cout << "fromB address: " << fromB << std::endl;  
  16.     std::cout << "fromC address: " << fromC << std::endl;  
  17.     std::cout << std::endl;  
  18.     std::cout << "vbptr address: " << (int*)d << std::endl;  
  19.     std::cout << "    [0] => " << *(int*)(*(int*)d) << std::endl;  
  20.     std::cout << "    [1] => " << *(((int*)(*(int*)d)) + 1)<< std::endl; // 偏移量20  
  21.     std::cout << "dataB value  : " << *((int*)d + 1) << std::endl;  
  22.     std::cout << "vbptr address: " << ((int*)d + 2) << std::endl;  
  23.     std::cout << "    [0] => " << *(int*)(*((int*)d + 2)) << std::endl;  
  24.     std::cout << "    [1] => " << *((int*)(*((int*)d + 2)) + 1) << std::endl; // 偏移量12  
  25.     std::cout << "dataC value  : " << *((int*)d + 3) << std::endl;  
  26.     std::cout << "dataD value  : " << *((int*)d + 4) << std::endl;  
  27.     std::cout << "dataA value  : " << *((int*)d + 5) << std::endl;  
  28. }  
int main()
{
	D* d = new D;
	d->dataA = 10;
	d->dataB = 100;
	d->dataC = 1000;
	d->dataD = 10000;

	B* b = d; // 轉化為基類B
	C* c = d; // 轉化為基類C
	A* fromB = (B*) d;
	A* fromC = (C*) d;

	std::cout << "d address    : " << d << std::endl;
	std::cout << "b address    : " << b << std::endl;
	std::cout << "c address    : " << c << std::endl;
	std::cout << "fromB address: " << fromB << std::endl;
	std::cout << "fromC address: " << fromC << std::endl;
	std::cout << std::endl;

	std::cout << "vbptr address: " << (int*)d << std::endl;
	std::cout << "    [0] => " << *(int*)(*(int*)d) << std::endl;
	std::cout << "    [1] => " << *(((int*)(*(int*)d)) + 1)<< std::endl; // 偏移量20
	std::cout << "dataB value  : " << *((int*)d + 1) << std::endl;
	std::cout << "vbptr address: " << ((int*)d + 2) << std::endl;
	std::cout << "    [0] => " << *(int*)(*((int*)d + 2)) << std::endl;
	std::cout << "    [1] => " << *((int*)(*((int*)d + 2)) + 1) << std::endl; // 偏移量12
	std::cout << "dataC value  : " << *((int*)d + 3) << std::endl;
	std::cout << "dataD value  : " << *((int*)d + 4) << std::endl;
	std::cout << "dataA value  : " << *((int*)d + 5) << std::endl;
}
           

得到結果為:

從記憶體布局看C++虛繼承的實作原理