C++虛函數與動態聯編

虛調用的幾種具體情形

虛調用是相對於實調用而言的,它的本質是動態聯編(後面我們會講到)。

實調用:在發生函數調用的時候,如果函數的地址是在編譯階段確定的,就是實調用。反之,函數的入口地址要在運行時通過查

詢虛函數表的方式獲得,就是虛調用。

虛調用不能簡單理解爲"對虛函數的調用", 因爲對虛函數的調用很可能是實調用。

下面這個程序,對虛函數的調用就是實調用

#include <iostream>
using namespace std;

class A
{
public:
	virtual void show()
	{
		cout<<"in A::show()\n";
	}
};

class B:public A
{
public:
	void show()
	{
		cout<<"in B::show()\n";
	}
};

void func(A a)
{
	a.show();	//a是A的一個實例,並不是指向類A對象的指針或引用,所以爲實調用。
}

int main()
{
	B b;
	func(b);	//調用類A的拷貝構造函數,產生一個A類對象作爲a進入函數func()的函數體
			//在函數體內,a是一個純粹的類A 對象,與類型B 毫無關係
	return 0;
}


在構造函數中調用虛函數,對虛函數的調用實際上是實調用(一般情況下,因避免在構造函數中調用虛函數)。這是虛函數被實調用的另一個例子。怎麼理解呢?從概念上說,在一個對象的構造函數運行完畢之前,這個對象還沒有完全誕生,所以在構造函數中調用虛函數,實際上都是實調用。請看下面的一個例子。

在構造函數中調用虛函數:

#include <iostream>
using namespace std;

class A
{
public:
	virtual void show()
	{
		cout<<"in A::show()\n";
	}
	A()
	{
		show();	//調用虛函數
	}
};

class B:public A
{
public:
	void show()
	{
		cout<<"in B::show()\n";
	}
	//B()
	//{
	//	show();	//
	//}
};

int main()
{
	A a;
	B b;
	return 0;
}


現在,我們來看一下虛函數到底是幹什麼用的?

設立虛函數的初衷,就是想在設計基類的時候,對該基類的派生類實施一定程度的控制。可以理解爲“通過基類訪問派生類成

員”。因此,虛調用最常用的形式是:通過指向基類對象的指針訪問派生類對象的虛函數,或通過基類對象的引用調用派生類

對象的虛函數。虛調用是通過查詢虛函數表來實現的,而擁有虛函數的對象都可以訪問到所屬類的虛函數表

派生類對象怎麼訪問到基類對象的虛函數?

通過指向派生類對象的指針或引用調用基類對象的虛函數,下面就是一個具體例子:

#include <iostream>
using namespace std;

class A
{
public:
	virtual void show()
	{
		cout<<"in A::show()\n";
	}
};

class B:public A
{
public:
	void show()
	{
		cout<<"in B::show()\n";
	}
};

int main()
{
	A a;
	//通過派生類對象的引用pb 實現了調用基類中虛函數show(),,
	//如果把 A中show() 前面的virtual去掉, 則調用的就是B 中的show()
	B &pb = static_cast<B&>(a);
	pb.show();	//調用的是基類 A的 show();
	return 0;
}


是不是實現虛調用一定要顯式藉助於指針或引用才能實現呢?

當然不是,請看下面的例子:

#include <iostream>
using namespace std;

class A
{
public:
	virtual void show()
	{
		cout<<"in A::show()\n";
	}
	void callfunc()
	{
		show();
	}
};

class B:public A
{
public:
	void show()
	{
		cout<<"in B::show()\n";
	}
};

int main()
{
	B b;
	b.callfunc();	//調用的是A::callfunc(),,但在A::callfunc()調用的是B::show()
			//這就是一個虛調用
	A a;
	a.callfunc();	//這裏調用的是A::show()
	return 0;
}

 

虛函數可以是私有的嗎?

虛函數一般被聲明爲公有的,這樣實現虛函數的調用會比較方便。但C++並沒有要求虛函數必須是公有的,將虛函數設置成私

有的和受保護並不妨礙虛函數之間的覆蓋和虛函數的調用。

動態聯編怎麼實現?

動態聯編:是指被調函數的入口地址是在運行時、而不是在編譯時決定的。C++利用動態聯編來完成虛函數的調用,C++標準

並沒有規定如何實現動態聯編,但大多數的C++編譯器都是通過虛指針(vptr)和虛函數表(vtable)來實現動態聯編的。

基本思路:

1.爲每一個包含虛函數的類建立一個虛函數表,虛函數表的各個表項存放的是各虛函數在內存中的入口地址;

2.在該類的每一個對象中設置一個指向虛函數表的指針(這就是爲什麼含有虛函數的對象會多出4個字節的大小);

3.在調用虛函數的時候,先利用虛指針找到虛函數表,確定虛函數的入口地址在表中的位置,獲取入口地址完成調用。

下面來詳細瞭解一下虛指針和虛函數表:

(1) 虛指針(vptr)放在對象的哪個位置?

虛指針是作爲對象的一部分存放在對象的空間中的,一個類只有一個虛函數表,因此該類的所有對象的虛指針都指向同一個地

方。在不同的編譯器中,虛指針在對象中的位置是不同的,在Vistual C++中,虛指針位於對象的其實位置,在GUN C++中,

虛指針位於對象的尾部而不是頭部。那麼怎麼確定虛指針到底存放在哪呢,看下面的程序:

#include <iostream>
using namespace std;

class HaveVirtual
{
	int i;
public:
	HaveVirtual()
	{
		i = 1;
	}
	virtual void show()
	{
		cout<<"you are hear\n";
	}
};

int main()
{
	HaveVirtual hv;
	unsigned long *p;
	p = reinterpret_cast<unsigned long*>(&hv);
	cout<<p[0]<<endl;
	cout<<p[1]<<endl;
	return 0;
}

通過觀察p[0] 和 p[1]的值,就可以判斷虛指針放在哪了。

(2)虛函數表的內部結構

 一個類只有一個虛函數表,所有的類都不會和其它的類共享同一張虛函數表。

怎麼創建虛函數表呢?

1.確定當前類包含的虛函數的個數。一個類的虛函數有兩個來源:一是繼承自父類(可能在當前類中改寫),其它的是在當前類

中新聲明的虛函數;

2.爲所有虛函數排序。繼承自父類的所有虛函數,排在當前類新聲明的虛函數之前,新聲明的虛函數按照在當前類中聲明的順

序排列;

3.確定虛函數的入口地址。繼承自父類的虛函數,如果在當前類中被改寫,則虛函數的入口地址是改寫之後的函數的地址,否

則保留父類中的虛函數的入口地址。新聲明的虛函數的入口地址就是在當前類中的函數的入口地址。

(3)虛函數表放在哪裏

虛函數表放在應用程序的常量區。虛函數的每一項代表了一個函數的入口地址,類型是Double Word

(4)通過訪問虛函數表手動調用虛函數

既然知道了虛函數表的位置和結構,那麼就可以通過訪問虛函數表,手動調用虛函數。

下面是一個手動調用虛函數的例子:

#include <iostream>
using namespace std;

typedef void (*funptr)();	//定義一個函數指針funptr

void ExecuteVirtualFunc(void * pObj, int index)
{
	funptr p;
	unsigned long * pAddr;
	pAddr = reinterpret_cast<unsigned long*>(pObj);	//取得對象的虛指針
				//visual C++中虛指針放在對象的頭部
	pAddr = (unsigned long *)*pAddr;	//通過虛指針得到虛函數表的首地址
	p = (funptr)pAddr[index];		//通過索引獲得虛函數入口地址
	_asm
	{
		mov ecx, pObj	//將對象的首地址放入寄存器 ecx
	}
	p();	//調用函數
}

class Base
{
	int i;
public:
	Base()
	{
		i = 0;
	}
	virtual void f1()
	{
		cout<<"Base's f1()\n";
	}
	virtual void f2()
	{
		cout<<"Base's f2()\n";
	}
	virtual void f3()
	{
		cout<<"Base's f3()\n";
	}
};

class Derived:public Base
{
	int j;
public:
	Derived()
	{
		j = 2;
	}
	virtual void f4()
	{
		cout<<"Derived's f4()\n";
	}
	void f3()
	{
		cout<<"Derived's f3()\n";
	}
	void f1()
	{
		cout<<"Derived's f1()\n";
	}
};

int main()
{
	Base b;
	Derived d;
	ExecuteVirtualFunc(&b, 1);	//調用對象b 的第2個虛函數 f2()
	ExecuteVirtualFunc(&d, 3);	//調用對象d 的第4個虛函數 f4()
	return 0;
}


調用類的非靜態成員函數是,必須同時給出對象的首地址,所以在程序中使用內聯彙編代碼_asm {  mov ecx, pObj ecx }來達

到這個目的。在Visual C++中,在調用類的非靜態成員函數之前,對象的首地址都是送往寄存器 ecx 的。

 

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