【C++】淺析C++中的對象模型

以下代碼運行環境:windows8.1 32位 VS2015

(一)不含有虛函數的單一繼承模型:
測試代碼:
//單一繼承,無虛函數
class A
{
public:
       A(int a = 0, char c = 0)
              :_a(a)
              , _c(c)
       {}
       int GetA()
       {
              return _a;
       }
       static int staticFun()
       {
              return _val;
       }
       static int _val;
private:
       int _a;
       char _c;
};
class B :public A
{
public:
       B(char b = 0)
              :_b(b)
       {}
       char GetB()
       {
              return _b;
       }
private:
       char _b;
};
int A::_val = 100;
void test1()
{
       A a(10,'a');
       B b('b');
}

現象以及分析:
關於類的靜態成員,我們需要知道以下幾點:
(1)類的靜態成員是屬於類而不屬於對象,所以他不是類的單個對象所有。
(2)靜態成員只存在一個,不像普通的成員,每創建一個對象,就會創建一組普通的成員。
(3)靜態成員的初始化不能在類中,肯定是不能在構造函數中,只能在類外並且在main函數之前,按照這樣的格式進行初始化:int A::_val = 100。並且是先初始化再使用。
(4)在靜態成員函數中不可以使用非靜態成員。因爲非靜態成員是屬於對象,而類的靜態成員函數是屬於類的,在類的對象實例化之前就已經完成了初始化。如果在靜態成員函數用引用非靜態成員,就好比是使用一個並沒有完成初始化的變量。
(5)類的非靜態成員函數中可以引用類的靜態成員。反之不可以。
(6)靜態成員函數沒有this指針。

瞭解了靜態成員的這些特性(當然本文主要是來分析對象模型的),我們在分析對象模型的時候,可以順便來看看靜態成員到底存儲在哪裏?
首先看一下上邊代碼的內存分佈:

在程序調試中,是這樣表現出來的:
這裏,我們只是看到的A類的普通數據成員繼承給子類(上述圖中,子類自稱自父類的成員默認是0,是因爲我們的父類的默認構造函數給出的默認值是0),下邊我們來看一下A類的靜態成員是存儲在哪裏?是否會繼承給子類?

從這裏我們可以看出,父類的靜態成員繼承給子類,並且兩者是存儲在一個地址上的。這裏就驗證了這樣的一句話:父類中定義了靜態成員,則整個繼承體系中只有一個這樣的成員,無論派生出多少個子類。靜態成員是存儲在全局區的。

(二)含有虛函數的單一繼承模型:
測試代碼:
class A
{
public:
       virtual void foo()
       {
              cout << "A::foo()" << endl;
       }
       virtual void funA()
       {
              cout << "A::funA()" << endl;
       }
private:
       int _a;
       char _c;
};
class B :public A
{
public:
       virtual void foo()
       {
              cout << "B::foo()" << endl;
       }
       virtual void funB()
       {
              cout << "B::funB()" << endl;
       }
private:
       char _b;
};
void test2()
{
       A a;
       B b;
}
內存分佈:

結合程序的調試進行分析:

結合程序的調試信息進行分析:

下邊我們來寫一個函數來打印出我們的虛表,看看與我們分析的是否一樣~~
void PrintTable(int* vTable)
{
       typedef void(*pFun)();
       cout << "虛表地址爲:" << vTable << endl;
       for (int i = 0; vTable[i] != NULL; ++i)
       {
              printf("第%d個虛表地址爲:%p->", i, vTable[i]);
              pFun f = (pFun)vTable[i];
              f();
       }
       cout << endl;
}
void test2()
{
       A a;
       B b;
       int* vTable1 = (int*)(*(int*)&a);
       int* vTable2 = (int*)(*(int*)&b);
       PrintTable(vTable1);
       PrintTable(vTable2);
}
運行結果:

【總結】:
(1)一個類只要有一個虛函數,它就會被編譯器分配一個虛表指針,也就是__vfptr,用來存儲虛函數的地址;
(2)子類的虛函數表是在父類的虛函數表上進行修改的,就像上邊的對象模型所示,B類的虛函數就是在A類的虛函數之後;
(3)父類中的虛函數被子類改寫,也就是說,子類中含有與父類的虛函數 函數名相同,參數列表相同,返回值相同的函數(協變除外),這樣就構成了重寫。下邊再次區分幾個容易混淆的概念--重載、重寫(覆蓋)、重定義(隱藏)。

重載--在同一個作用域內,函數名相同,參數列表不同,返回值可以相同可以不同的兩個函數可以構成重載,需要聲明的是,c++語言中支持函數的重載,而c語言中不支持函數的重載。原因是c語言和c++對函數的處理是不同的。具體可以點擊文末的鏈接。
重寫(覆蓋)--在不同的作用域中(分別在父類和子類中),函數名相同,參數列表,返回值都相同(協變除外),並且父類的函數是虛函數,訪問限定符可同可不同的兩個函數就構成了重寫。
重定義(隱藏)--在不同的作用域中(分別在父類和子類),函數名相同,只要不是構成重寫就是重定義。
     協變:協變也是一種重寫,只是父類和子類中的函數的返回值不同,父類的函數返回父類的指針或者引用,子類函數返回子類的指針或者引用。

(4)只有類的成員函數纔可以被定義爲虛函數,靜態成員函數不可以被定義爲虛函數。

(三)多繼承的對象模型
測試代碼:
class A
{
public:
       virtual void foo()
       {
              cout << "A::foo()" << endl;
       }
       virtual void funA()
       {
              cout << "A::funA()" << endl;
       }
private:
       int _a;
};
class B
{
public:
       virtual void foo()
       {
              cout << "B::foo()" << endl;
       }
       virtual void funB()
       {
              cout << "B::funB()" << endl;
       }
private:
       int _b;
};
class C :public A, public B
{
public:
       virtual void foo()
       {
              cout << "C::foo()" << endl;
       }
       virtual void funC()
       {
              cout << "C::funC()" << endl;
       }
private:
       int _c;
};
void test3()
{
       C c;
}
下邊我們先通過調試信息,看看對象的內存分佈:

通過這個圖,我們就可以畫出多繼承的對象的內存分佈:

下邊我們仍然編寫一個函數打印出虛函數表:
void PrintTable(int* vTable)
{
       typedef void(*pFun)();
       cout << "虛表地址爲:" << vTable << endl;
       for (int i = 0; vTable[i] != NULL; ++i)
       {
              printf("第%d個虛函數地址爲:0x%p->", i, vTable[i]);
              pFun f = (pFun)vTable[i];
              f();
       }
       cout << endl;
}
void test3()
{
       C c;
       int* vTable = (int*)(*(int*)&c);
       PrintTable(vTable);
       vTable = (int *)(*((int*)&c + sizeof(A) / 4));
       PrintTable(vTable);
}
程序運行結果:

【總結】
在多重繼承體系下,有n個含有虛函數的父類,派生類中就有n個虛函數表,最終子類的虛函數是在第一個父類的虛函數表中;

(四)含有虛繼承的多重繼承模型(含有虛函數)
測試代碼:
class A
{
public:
	A()
		:_a(1)
	{}
	virtual void foo()
	{
		cout << "A::foo()" << endl;
	}
	virtual void funA()
	{
		cout << "A::funA()" << endl;
	}
private:
	int _a;
};
class B : public virtual A
{
public:
	B()
		:_b(2)
	{}
	virtual void foo()
	{
		cout << "B::foo()" << endl;
	}
	virtual void funB()
	{
		cout << "B::funB()" << endl;
	}
private:
	int _b;
};
class C : public virtual A
{
public:
	C()
		:_c(3)
	{}
	virtual void foo()
	{
		cout << "C::foo()" << endl;
	}
	virtual void funC()
	{
		cout << "C::funC()" << endl;
	}
private:
	int _c;
};
class D :public B, public C
{
public:
	D()
		:_d(4)
	{}
	virtual void foo()
	{
		cout << "D::foo()" << endl;
	}
	virtual void FunD()
	{
		cout << "D::funD()" << endl;
	}
private:
	int _d;
};


通過調試信息看對象模型:
B類對象的調試信息:

C類對象的調試信息:

D類對象的調試信息:



下邊,根據調試信息畫出內存分佈:



文中涉及到的內容鏈接:
參考文檔:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章