溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

探索c++對象模型(一)

發(fā)布時間:2020-07-07 00:05:50 來源:網絡 閱讀:765 作者:暮回_zz 欄目:編程語言

    C++中虛函數的誕生,就是為了多態(tài)的實現(xiàn)。當子類對父類的虛函數進行了重寫,在父類指針調用重寫的虛函數時,如果父類指針(或引用)指向了父類的對象,則調用父類的虛函數,如果父類指針(或引用)指向了子類對象,則調用子類的虛函數。

    要想了解多態(tài)的實現(xiàn),就必須要知道虛函數表的構成。

【注:文章中代碼測試環(huán)境為Win7 64位 VS2013】

首先,我們討論單個含有虛函數的類,即不存在繼承關系。

    當我們的類中含有虛函數時,類實例化出來的對象,他的成員除了自己的成員變量外,還會多出一個指針。這個指針我們稱為虛表指針,他所指向的是我們類對象所維護的虛表。虛表中保存的是類中所有虛函數的地址。代碼如下:

class B
{
public:
	B()
		:_val(1){}
	virtual void fun1()
	{
		cout << "void B::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "void B::fun2()" << endl;
	}
	void fun3()
	{
		cout << "void B::fun3()" << endl;
	}
private:
	int _val;
};

int main()
{
	B b;
	cout << sizeof(b) << endl;//輸出結果為8
	system("pause");
	return 0;
}

    代碼說明:構造函數給類成員變量賦值為1,方便內存查看,成員函數有兩個虛函數,一個非虛函數,斷點打在return 0;處。

    然后我們切換到監(jiān)視窗口,<如下>可以發(fā)現(xiàn),對象b實際上維護了兩個成員,"__vfptr"和"_val",在內存窗口中"&b",得到的是 0012cc74 ,即 __vfptr,下一個是 00000001,即我們的成員 _val。這也解釋了為什么 sizeof(b) 的輸出結果是8。不管有多少虛函數,類中只保留了一個虛表指針,添加再多的虛函數,也不會改變sizeof(b)的值

    另外,可以看到的是虛表只保存虛函數地址,非虛函數,依舊是屬于整個類而非對象。

    

探索c++對象模型(一)

我們再次查看 __vfptr 指向的空間

探索c++對象模型(一)

前兩個是我虛函數的地址,也就是說,他們之間存在關系如下

探索c++對象模型(一)

    為了驗證,這里我重新封裝一個函數指針,通過偏移,看能不能輸出cout里面的內容。代碼如下:

typedef void(*p_fun)();

void Print_fun(p_fun* ppfun)
{
	for (int i = 0;/* ppfun[i] != NULL*/i<2; i++)
	{
		ppfun[i]();
	}
}
void test()
{
	B b;
	Print_fun((p_fun*)(*(int*)&b));
}

測試是可以輸出我們函數內容的。多說一句,虛表中前面保存的都是虛函數的地址,最后結束項在不同編譯器下是不一樣的,在VS2013環(huán)境下,最后一項保存的地址是不可訪問的,VS2008環(huán)境下,最后是以0x00000000結尾,即是NULL。所以打印函數Print_fun()中for循環(huán)條件我做了修改。當然可以更加直接的這樣調用函數。

//	((p_fun*)(*(int*)&b))[0]();
//	((p_fun*)(*(int*)&b))[1]();

接下來,我們看看包含虛函數重寫的單繼承中的虛表

    這里給出單繼承的測試代碼

class A
{
public:
	A()
		:_a_val(1){}
	 virtual void fun1()
	{
		cout << "void A::fun1()" << endl;
	}
	virtual void fun2()
	{
		cout << "void A::fun2()" << endl;
	}
protected:
	int _a_val;
};

class B:public A
{
public:
	B()
		:_b_val(2){}
	virtual void fun1()
	{
		cout << "virtual B::fun1()" << endl;
	}
	virtual void fun3()
	{
		cout << "virtual B::fun3()" << endl;
	}
	virtual void fun4()
	{
		cout << "virtual B::fun4()" << endl;
	}
protected:
	int _b_val;
};

void test()
{
	B b;
}

    代碼說明:父類 A 包含兩個虛函數 fun1() 、fun2(),一個成員變量 _a_val ,構造成員變量為 1 ;子類 B 共有繼承了 A ,重寫函數 fun1() ,同時添加兩個自己的虛函數 fun3()、fun4(),成員變量 _b_val ,構造為 2 。

    接下來切換到調試窗口<如下圖>,可以看到類B實例化對象 b 實際上維護了三個成員,"__vfptr"、"_a_val"、"_b_val",在內存窗口中“&b”,得到的是0x009bcd48,對應到監(jiān)視窗口,即 __vfptr ,接下來是0x00000001,0x00000002,即成員變量_a_val,_b_val。如果在這里去sizeof(b),得到的結果應該是12。

探索c++對象模型(一)

接下來查看 __vfptr 指向的空間

探索c++對象模型(一)

    監(jiān)視中看到的是只有兩個函數地址,但內存窗口中可以看到,前四個在內存中是在一起的,或者說很近。為了確認,使用剛剛的打印函數,不過需要改變一下循環(huán)次數,受編譯器的限制,這里只能手動修改循環(huán)次數,看最多打印多少次是正常結束,而非程序崩潰。

typedef void(*p_fun)();
void Print_fun(p_fun* ppfun)
{
	for (int i = 0;/* ppfun[i] != NULL*/i<4; i++)
	{
		ppfun[i]();
	}
}
void test()
{
	B b;
//	((p_fun*)(*(int*)&b))[0]();
//	((p_fun*)(*(int*)&b))[1]();
//	((p_fun*)(*(int*)&b))[2]();
//	((p_fun*)(*(int*)&b))[3]();
	Print_fun((p_fun*)(*(int*)&b));
}

     測試得到,最多可以打印四次,打印結果如下:

探索c++對象模型(一)

    *可以看到fun1()函數被子類 B 重寫,fun2()函數繼承自父類。得到結果監(jiān)視窗口未顯示虛函數fun3()和fun4()地址,但實際上子類新創(chuàng)建的虛函數地址也會保存到虛表當中,而且在單繼承過程中,子類的虛函數和父類的虛函數是保存在同一虛當中,并未對子類的虛函數創(chuàng)建獨立的虛表。

    即有下圖關系:

探索c++對象模型(一)

接下來的多繼承中的對象模型

    首先給出測試代碼,如下

class A
{
public:
	A()
		:_a_val(1){}
	virtual void test1()
	{
		cout << "A::test1()" << endl;
	}
	virtual void test2()
	{
		cout << "A::test2()" << endl;
	}
protected:
	int _a_val;
};

class B
{
public:
	B()
		:_b_val(2){}
	virtual void test1()
	{
		cout << "B::test1()" << endl;
	}
	virtual void test3()
	{
		cout << "B::test3()" << endl;
	}
protected:
	int _b_val;
};

class C
{
public:
	C()
		:_c_val(3){}
	virtual void test1()
	{
		cout << "C::test1()" << endl;
	}
	virtual void test4()
	{
		cout << "C::test4()" << endl;
	}
protected:
	int _c_val;
};

class D:public A,public B,public C
{
public:
	D()
		:_d_val(4){}
	virtual void test1()
	{
		cout << "D::test1()" << endl;
	}
	virtual void test5()
	{
		cout << "D::test5()" << endl;
	}
protected:
	int _d_val;
};

void test()
{
	D d;
}

    代碼說明:首先創(chuàng)造三個基類,類 A 包含兩個虛函數fun1()、fun2(),類包含成員變量 _a_val ,構造為1;類 B 包含兩個虛函數 fun1()、fun3(),類包含成員變量 _b_val,構造為2;類 C 包含兩個虛函數 fun1()、fun4(),類包含成員變量 _c_val,構造為3;創(chuàng)建第四個類,作為派生類 D ,同時共有繼承類A、類B、類C,包含虛函數fun1(),fun2(),fun1()函數對子類中的fun1()進行重寫,同時包含成員變量_d_val。

        接下來切換到調試窗口<如下圖>,可以看到類 D 實例化對象 d 這里維護了七個成員,由于繼承了三個類,因此這里有三個虛表指針"__vfptr"、同時包含繼承自三個類的成員變量"_a_val"、"_b_val"、"_c_val"和自己本身的成員變量"_d_val"。在內存窗口中“&d”,得到的是0x013bdd04,對應到監(jiān)視窗口,即繼承的第一個類的虛表指針 __vfptr ,接下來是0x00000001,即成員變量_a_val,接下來依次類推,得到第二個類的虛表指針,和繼承自第二個類的成員變量,第三個類的虛表指針,和繼承自第三個類的成員變量,最后一項是子類 D 的成員變量。如果在這里去 sizeof(b),得到的結果應該是28。

探索c++對象模型(一)

    不過這里有個問題,是子類 D 的虛函數地址在哪里。。這里我們打開多個內存窗口,同時把各個虛表指針指向的內容列出來。<如圖>

    探索c++對象模型(一)    可以看到,盡管監(jiān)視窗口中,A的虛表指針下只有兩項,但對應到內存中卻有三項,可以推測,子類單獨的虛函數地址是保存在了第一繼承子類的虛函數表中,未覆蓋的虛函數不會單獨創(chuàng)建一塊虛函數表。

    除此之外,還應該可以看到,子類每繼承一個含有虛函數的父類,就會多一個虛表指針,可能會同時維護多個虛表。

    換句話說,存在如下圖對應關系。


探索c++對象模型(一)

    多提一點,子類繼承了多個父類,父類虛表的地址不一定是連續(xù)的

    這里依舊使用函數指針的方式去調用我的成員函數來加以驗證。代碼如下

typedef void(*p_fun)();
void Print_fun(p_fun* ppfun)
{
	for (int i = 0; ppfun[i] != NULL; i++)
	{
		ppfun[i]();
	}
}

void test()
{
	D d;
	Print_fun((p_fun*)(*(int*)&d));
	cout << "-----------------------------------------" <<endl;
	Print_fun((p_fun*)(*((int*)&d+2)));
	cout << "-----------------------------------------" << endl;	
	Print_fun((p_fun*)(*((int*)&d+4)));
	cout << "-----------------------------------------" << endl;
}

打印結果如下:

探索c++對象模型(一)

    由打印結果可見,子類專有的虛函數test5()的函數地址放在了第一個繼承的虛表中,test1()函數均被子類 D 重寫。







    我們通過虛函數表理解C++ 中的對象模型,了解多態(tài)實際上是用虛函數實現(xiàn)覆蓋,但通過上面的測試,可以發(fā)現(xiàn),實現(xiàn)多態(tài)的同時,無疑會帶來效率的下降(通過兩次指針解引用才可以訪問)。

    除此之外應該看到的一點是,多態(tài)實現(xiàn)的過程是不安全的,盡管虛函數表的內容我們不能夠隨意修改,但永遠可以被直接訪問,這是不安全的一種直接表現(xiàn)。

    關于菱形繼承的對象模型和菱形虛擬繼承的對象模型,會在下一篇中提到。


------------------------------------------muhuizz------------------------------------------

                                                                http://11331490.blog.51cto.com 

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI