C++虛函數表


虛函數表

C++中虛函數是通過一張虛函數表(Virtual Table)來實現的,在這個表中,主要是一個類的虛函數表的地址表;這張表解決了繼承、覆蓋的問題。在有虛函數的類的實例中這個表被分配在了這個實例的內存中,所以當我們用父類的指針來操作一個子類的時候,這張虛函數表就像一張地圖一樣指明了實際所應該調用的函數。

C++編譯器是保證虛函數表的指針存在於對象實例中最前面的位置(是為了保證取到虛函數表的最高的性能),這樣我們就能通過已經實例化的對象的地址得到這張虛函數表,再遍歷其中的函數指針,並調用相應的函數。

下面先看一段代碼:

class Base {
public:
	virtual void f() { cout << "Base::f()" << endl; }
	virtual void g() { cout << "Base::g()" << endl; }
	virtual void h() { cout << "Base::h()" << endl; }
};

typedef void(*Fun)(void);

int main()
{
	Base b;
	Fun pFun = NULL;
	cout << "虛函數表的地址為:" << (int*)(&b) << endl;
	cout << "虛函數表的第一個函數地址為:" << (int*)*(int*)(&b) << endl;

	pFun = (Fun)*((int*)*(int*)(&b));
	pFun();
	system("pause");
	return 0;
}

運行結果如下:
TIM截圖20181030142610.png
我們再追蹤一下虛函數表的地址:

image.png

image.png

image.png

結合結果分析一下代碼:首先是創建了一個Base的類,Base類里面有三個成員函數,都為虛函數;然后typedef void(*Fun)(void)是利用類型別名聲明一個函數指針,指向的地址為NULL,可以等價成這樣:typedef decltype(void) *Fun。然后再到main函數里,利用Base實例化了對象了b;然后Fun pFun=NULL則是聲明了一個返回指向函數的指針,該指針pFun此時也是NULL,根據圖1可以知道,他的類型是void(*)(),表示的就是函數指針,而當執行完這句后就會從原來沒有分配的0xcccccccc變成0x00000000。接下來就是(int*)(&b)強行把&b轉成int *,取得虛函數表的地址;再次取址就可以得到第一個虛函數的地址了,也就是Base::f() 。接下來就是 pFun = (Fun)*((int*)*(int*)(&b)); 把函數指針指向虛函數表的第一個函數,最后再pFunc()運行。

如果想讓pFun調用其它的函數,可以是這樣:

(Fun)*((int*)*(int*)(&b)+0);  // Base::f()

(Fun)*((int*)*(int*)(&b)+1);  // Base::g()

(Fun)*((int*)*(int*)(&b)+2);  // Base::h()

通過下圖可以很好的進行理解:

image.png

(int*)*(int*)(&b)可以這樣理解,(int*)(&b)就是對象b的地址,只不過被強制轉換成了int*了,如果直接調用*(int*)(&b)則是指向對象b地址所指向的數據,但是此處是個虛函數表呀,所以指不過去,必須通過(int*)將其轉換成函數指針來進行指向就不一樣了,它的指向就變成了對象b中第一個函數的地址,所以(int*)*(int*)(&b)就是獨享b中第一個函數的地址;又因為pFun是由Fun這個函數聲明的函數指針,所以相當於是Fun的實體,必須再將這個地址轉換成pFun認識的,即加上(Fun)*進行強制轉換:簡要概括就是從b地址開始
讀取四個字節的內容,然后將這個內容解釋成一個內存地址,然后訪問這個地址,然后將這個地址中存放的值再解釋成一個函數的地址.

image.png

下面將對比說明有無虛函數覆蓋情況下的虛函數表的樣子:

一般繼承(無虛函數覆蓋)

先寫出一個繼承關系:
image.png

寫成代碼如下:

class Base {
public:
	virtual void f() { cout << "Base::f()" << endl; }
	virtual void g() { cout << "Base::g()" << endl; }
	virtual void h() { cout << "Base::h()" << endl; }
};

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

typedef void(*Fun)(void);

int main()
{
	//Base b;
	Derive d;
	Fun pFun = NULL;
	cout << "虛函數表的地址為:" << (int*)(&d) << endl;
	cout << "虛函數表的第一個函數地址為:" << (int*)*(int*)(&d)<< endl;
	pFun =(Fun)*((int*)*(int*)(&d)+1);
	pFun();
	pFun = (Fun)*((int*)*(int*)(&d) + 3);
	pFun();

	system("pause");
	return 0;
}

執行結果如下:

image.png

通過調試看一下相應的虛函數表:

image.png

image.png

這個繼承關系中,子類沒有重載任何父類的函數,我們實例化了一個對象d:Derive d,則它的虛函數表是如下的:

image.png

所以虛函數按照其聲明順序放於表中,並且父類的虛函數在子類的虛函數前面

一般繼承(有虛函數覆蓋)

這樣的一個繼承關系:

image.png

這個繼承關系中,Derive的f()重載了Base類中的f(),下面我們來逐步調試:

1540901666638

上圖是剛剛通過Derive d聲明的虛函數表的樣子,我們再直接打印出對象d的第一個函數、第三個函數和第四個函數來看看:

image.png

相應的虛函數表變成了:

image.png

我們可以知道覆蓋的f()函數被放到了虛函數表中原來父類虛函數的位置,而沒有被覆蓋的函數依次往后排列

多重繼承(無虛函數覆蓋)

現在用這樣一個繼承關系:

image.png

調試程序發現:

image.png

如果我們訪問第一個函數地址之后的第6個函數位置會發生什么呢?

image.png

是找不到的,說明虛函數表已經不是按原來的方式通過一個地址找到所有的函數,或者說所有的子函數實現是按照順序排列來存放的了。

虛函數表是這樣的:

image.png

但是我們通過我們目前實現訪問虛函數表的方式是訪問不到下面兩張虛函數表的,卻可以通過這樣來實現:

Derive d;
Base2 *b2=&d;
b2->f();
b2->g();

image.png

image.png

在聲明了b2並綁定d的時候,已經指向了Base2的虛函數表的地址,再通過b2->f()就可以訪問Base2的虛函數表中第一個函數的位置。

多重繼承(有虛函數覆蓋)

繼承關系:

image.png

在這里就不放出代碼和調試內容了,直接給出虛函數表的樣子:

image.png

幾點注意

1.不能通過父類型的指針訪問子類自己的虛函數,是非法的

Base *b=new Derive();
b->f1();     //編譯會出錯

P.S:我們可以通過指針的方式訪問虛函數表來達到違反C++語義的行為

class Base1 {
public:
	virtual void f() { cout << "Base1::f" << endl; }
	virtual void g() { cout << "Base1::g" << endl; }
	virtual void h() { cout << "Base1::h" << endl; }
};

class Base2 {
public:
	virtual void f() { cout << "Base2::f" << endl; }
	virtual void g() { cout << "Base2::g" << endl; }
	virtual void h() { cout << "Base2::h" << endl; }
};

class Base3 {
public:
	virtual void f() { cout << "Base3::f" << endl; }
	virtual void g() { cout << "Base3::g" << endl; }
	virtual void h() { cout << "Base3::h" << endl; }
};

class Derive : public Base1, public Base2, public Base3 {
public:
	virtual void f() { cout << "Derive::f" << endl; }
	virtual void g1() { cout << "Derive::g1" << endl; }
};

typedef void(*Fun)(void);

int main()
{
	Fun pFun = NULL;
	Derive d;
	int** pVtab = (int**)&d;
	//Base1's vtable
	//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+0);
	pFun = (Fun)pVtab[0][0];
	pFun();
	//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+1);
	pFun = (Fun)pVtab[0][1];
	pFun();
	//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+2);
	pFun = (Fun)pVtab[0][2];
	pFun();
	//Derive's vtable
	//pFun = (Fun)*((int*)*(int*)((int*)&d+0)+3);
	pFun = (Fun)pVtab[0][3];
	pFun();
	//The tail of the vtable
	pFun = (Fun)pVtab[0][4];
	cout << pFun << endl;
	//Base2's vtable
	//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
	pFun = (Fun)pVtab[1][0];
	pFun();
	//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
	pFun = (Fun)pVtab[1][1];
	pFun();
	pFun = (Fun)pVtab[1][2];
	pFun();
	//The tail of the vtable
	pFun = (Fun)pVtab[1][3];
	cout << pFun << endl;

	//Base3's vtable
	//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+0);
	pFun = (Fun)pVtab[2][0];
	pFun();
	//pFun = (Fun)*((int*)*(int*)((int*)&d+1)+1);
	pFun = (Fun)pVtab[2][1];
	pFun();
	pFun = (Fun)pVtab[2][2];
	pFun();
	//The tail of the vtable
	pFun = (Fun)pVtab[2][3];
	cout << pFun << endl;
	return 0;

}

在這里延伸一下關於int **int *,這里引用了知乎上的一片答案:

C語言里面的定義的指針,它除了表示一個地址,它還帶有類型信息。這個類型信息,用來告訴你,在這個地址空間上,存放着什么類型的變量。打個比如,有如下的代碼片段:int a;
int *p = &a;
假設p的指針值為0x08004000,並且int類型長度為4字節。那么p將告訴你,[0x08004000, 0x08004004) 內存空間上存放着一個int類型。如果你只從0x08004000地址中,只讀取兩個字節,那是錯誤的。同樣道理,以下代碼片段:int a;
int *b = &a;
int **c = &b;
是告訴你,c的指針值是另一個指針(b),而該指針則指向一個int變量。如果你刻意要較真,認為指針就是一個地址,並且舉出下面的例子:int a;
int *b = &a;
int *c = &b;  // 請留意這里
最后一句賦值,實際上是將類型信息丟棄了。因為在編譯器看來,C 就表示一個指針,它指向的是一個整數,只是你自己將它解釋成指針而已。printf("%d\n",*(int *)(*c)); // 開發人員,將*c解釋成指針,編譯器是不認帳的喔
為什么將一個整數解釋成一個指針沒有問題呢?那是因為湊巧,你把它放到64位架構上試試看看。

2.如果父類的虛函數是private或者是protected的,但這些非public的虛函數同樣會存在於虛函數表中!
3.虛函數表不一定是存在最開頭,但是目前各個編譯器大多是這樣設置的。
image.png


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM