1. 程式人生 > >C++多型呼叫實現原理(虛擬函式表詳解)

C++多型呼叫實現原理(虛擬函式表詳解)

1.帶有虛擬函式的基類物件模型

我們先看段程式碼:

#include<iostream>
using namespace std;
class B1
{
	public:
		void func1()
		{}
		int _b;
};
class B2
{
	public:
		virtual void func()
		{}
		int _b;
};
int main()
{
	cout<<"sizeof(B1) = "<<sizeof(B1)<<endl;  
	cout<<"sizeof(B2) = "<<
sizeof(B2)<<endl; system("pause"); return 0; }

執行結果:
在這裡插入圖片描述
可以看出,B2的這個類比B1多了4個位元組,而這4個位元組就是用來存放虛擬函式的地址,也就是說,這4個位元組的資料是一個指標(地址),這個指標指向的是虛擬函式地址。看個圖就很容易理解。
在這裡插入圖片描述
我們需要注意的是:
1.B2物件的前4個位元組存放的是虛表的地址,其後才是B2該物件的成員變數;(虛擬函式表我們也叫做虛表)。
2.若B2這個類中有多個虛擬函式,那麼其物件大小還是8,因為前4個位元組是存放虛擬函式表的地址,在這個虛擬函式表(函式指標陣列)裡面每個元素才是每個虛擬函式的地址。

2.派生類物件虛擬函式表如何構建?

上個例子,只是給出了帶有虛擬函式基類的物件模型,那派生類的物件虛擬函式表應該如何構建?

#include<iostream>
#include<string>
using namespace std;
class Base					//基類
{
public:
	virtual void TestFunc1()
	{
		cout << "Base::TestFunc1()" << endl;
	}
	virtual void TestFunc2()
	{
		cout << "Base::TestFunc2()"
<< endl; } virtual void TestFunc3() { cout << "Base::TestFunc3()" << endl; } int _b; }; class Derived : public Base //派生類 { public: virtual void TestFunc4() { cout << "Derived::TestFunc4()" << endl; } virtual void TestFunc1() { cout << "Derived::TestFunc1()" << endl; } virtual void TestFunc3() { cout << "Derived::TestFunc3()" << endl; } virtual void TestFunc5() { cout << "Derived::TestFunc5()" << endl; } int _d; }; typedef void(*PVFT)(); //宣告函式指標,用來呼叫虛擬函式 void PrintVFT(Base& b, const string& str) { cout << str << endl; /*這裡是先將物件的地址取出來再強制型別換,此時再解引用的話,取的值就是物件前四個位元組地址裡面存放的值, 這個值就是虛表的地址,即就是函式指標陣列的首地址,我們再將這個地址轉換成函式指標型別*/ PVFT* pVFT = (PVFT*)(*(int*)&b)while (*pVFT) //虛表中最後一個元素是空值,列印完迴圈退出 { (*pVFT)(); // 再解引用就是函式指標數組裡面的第一個元素(即就是第一個虛擬函式地址),再往後一次列印 ++pVFT; } cout << endl; } void TestVirtualFunc(Base& b) { b.TestFunc1(); b.TestFunc3(); return; } int main() { Base b; Derived d; // 列印基類與派生類的虛表 PrintVFT(b, "Base VFT:"); PrintVFT(d, "Derived VFT:"); // 傳遞Base類物件 TestVirtualFunc(b); cout << endl; // 傳遞派生類物件 TestVirtualFunc(d); system("pause"); return 0; }

執行結果:
在這裡插入圖片描述
根據結果,我們首先可以看出基類虛擬函式表是按照宣告順序依此存放,派生類的虛擬函式表則相應的發生了一些改變。
1.先將基類中的虛表內容拷貝一份到派生類虛表中
2.如果派生類重寫了基類中某個虛擬函式,用派生類自己的虛擬函式替換虛表中基類的虛擬函式
3.派生類自己新增加的虛擬函式按其在派生類中的宣告次序增加到派生類虛表的最後

其實我們沒必要刻意去記這些規則,如果用多型的思想去考慮下的話,這樣的規則很合理。對我自己而言,首先派生類繼承基類,那麼基類有的東西,派生類本來沒有的東西繼承之後也就具有;其次,若基類和派生類都具有相同的東西,那麼在派生類的虛表中,我派生類要保持我自己的特點,所以此時派生類的虛表中存放的是自己的虛擬函式,這樣做的目的很簡單,就是為了在多型呼叫時,會很靈活,根據物件本身自己來呼叫相應的虛擬函式,假如派生類中的虛表中存放的是基類的虛擬函式,那麼請問,在多型呼叫時,不管給基類還是派生類的物件,呼叫的虛擬函式都是級基類的虛擬函式,這樣做還會實現多型這個特性嗎?最後一點不言而喻,派生類獨有的虛擬函式按照宣告順序加在虛表後面即可。
討論完基類和派生類物件的虛表之後,我們來看看派生類物件完整的物件模型。

3.單繼承的派生類物件模型

對於單繼承的派生類的物件模型,其和我們上面介紹的第一個例子一樣,前四個位元組是虛表的地址,後面就是按順序存放
接著看一段程式碼:

#include<iostream>
using namespace std;

class Base
{
public:
	virtual void func1()
	{
		cout << "Base::func1" << endl;
	}

	virtual void func2()
	{
		cout << "Base::func2" << endl;
	}
private:
	int a;
};

class Derive :public Base
{
public:
	virtual void func1()
	{
		cout << "Derive::func1" << endl;
	}

	virtual void func3()
	{
		cout << "Derive::func3" << endl;
	}

	virtual void func4()
	{
		cout << "Derive::func4" << endl;
	}
private:
	int b;
};

typedef void(*VFUNC)();

void PrintVTbale(int* table)
{
	printf("vtable:%p\n", table);
	int i = 0;
	
	for (int i = 0; table[i]!= NULL; ++i) //虛表最後一個元素為空值
	{	
		printf("table[%d]:%p->", i, table[i]);
		VFUNC f = (VFUNC)table[i];	//轉為函式指標
		f();	//呼叫對應的虛擬函式
	}
	return;
}

執行結果:
在這裡插入圖片描述
根據結果:基類的物件模型和上面第一個例子一樣,不再過多討論。對於單繼承的派生類物件模型,在我們清楚了其虛表的構建之後,再其虛擬函式表地址後面按順序先存放基類的物件,其次再存放派生類的物件即可。
派生類的物件模型那就是在虛擬函式指標後面,按順序先存放基類的成員變數,接著再存放派生類自己成員變數。如下圖:
在這裡插入圖片描述
下面我們來看多繼承的派生類的物件模型

4.多繼承的派生類物件模型

看程式碼:

#include<iostream>
using namespace std;

class Base1				//基類Base1
{
public:
	virtual void func1()
	{
		cout << "Base1::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base1::func2" << endl;
	}
private:
	int b1;
};

class Base2				//基類Base2
{	
public:
	virtual void func1()
	{
		cout << "Base2::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base2::func2" << endl;
	}
private:
	int b2;
};

class Derive : public Base1, public Base2		//派生類
{
public:
	virtual void func1() {
		cout << "Derive::func1" << endl;
	}
	virtual void func3()
	{
		cout << "Derive::func3" << endl;
	}
private:
	int d1;
};

typedef void(*VFUNC)();

void PrintVTbale(int* table)
{
	printf("vtable:%p\n", table);
	for (int i = 0; table[i] != 0; ++i)
	{
		printf("table[%d]:%p->", i, table[i]);
		VFUNC f = (VFUNC)table[i];
		f();
	}
	cout << endl;
}

int main()
{
	Base1 b1;
	Base2 b2;

	Derive d;
	cout << sizeof(d) << endl;		//計算派生類物件的大小

	PrintVTbale((int*)(*(int*)&b1));
	PrintVTbale((int*)(*(int*)&b2));

	PrintVTbale((int*)(*(int*)&d));		//第一個虛表
	PrintVTbale((int*)(*(int*)((char*)&d + sizeof(Base1))));	//第二個虛表
	system("pause");
	return 0;
}

執行結果:

在這裡插入圖片描述
我們注意到;派生類物件的大小是20,對於多繼承的派生類物件,如果只有一個虛表,那麼它的大小應該是4(虛表指標) + 4(b1)+ 4(b2) + 4(d1) = 16,可是結果不是16,所以派生類物件的虛表應該不止一個。
所以他的物件模型應該如下圖這樣:
在這裡插入圖片描述
總結以下就是:
1.對於多繼承的派生類的物件,其不但繼承了基類的物件,也繼承了基類的虛擬函式表指標;
2.派生類繼承多個基類,派生類的物件模型其實就相當於將基類的物件模型繼承下來了,只不過對於派生類獨有的虛擬函式,那麼他的虛擬函式指標將會按順序存放在第一個虛表中最後的位置。
3.最後再加上派生類自己成員

至此,對於常見的普通型別多型呼叫就這些,還有其他繼承型別的多型呼叫,如菱形繼承等。
後面會再做總結。

注:文中如有不正之處,歡迎指出,希望和大家共同學習,進步。