C++虛函數表原理淺析


C++中的虛函數實現了多態的機制,也就是用父類型指針指向其子類的實例,然后通過父類的指針調用實際子類的成員函數,這種技術可以讓父類的指針有“多種形態”,這也是一種泛型技術,也就是使用不變的代碼來實現可變的算法

本文不再闡述虛函數的使用方法,而是從虛函數的實現機制上做一個清晰的剖析

參考博文:https://blog.csdn.net/u012879957/article/details/81027287

想了解實現機制,就必須先了解對象的存儲方式

對象數據和函數的存儲方式

我們知道,用類去定義對象,系統會為每一個對象分配存儲空間

在你的印象中類的存儲可能是下圖這樣的:

上圖表示要分別為對象的數據和函數的代碼分配存儲空間,這樣肯定是不行的,內存利用率太低了,所以C++編譯系統采用的是以下的方法:

每個對象占用存儲空間的只是該對象的數據部分(虛函數指針和虛基類指針也屬於數據部分),函數代碼屬於公用部分

我們常說的“A對象的成員函數”,是從邏輯的角度而言的,而成員函數的物理存儲方式其實不是如此

C++內存分區

C++的內存分區大概分成五個部分:

  1. 棧(stack):是由編譯器在需要時自動分配,不需要時自動清除的變量存儲區,通常存放局部變量、函數參數等。
  2. 堆(heap):是由new分配的內存塊,由程序員釋放(編譯器不管),一般一個new與一個delete對應,一個new[]與一個delete[]對應,如果程序員沒有釋放掉,資源將由操作系統在程序結束后自動回收
  3. 自由存儲區:是由malloc等分配的內存塊,和堆十分相似,用free來釋放
  4. 全局/靜態存儲區:全局變量靜態變量被分配到同一塊內存中
  5. 常量存儲區:這是一塊特殊存儲區,里邊存放常量,不允許修改

(堆和自由存儲區其實不過是同一塊區域,new底層實現代碼中調用了malloc,new可以看成是malloc智能化的高級版本)

你可能會問:靜態成員函數和非靜態成員函數都是在類的定義時放在內存的代碼區的,因而可以說它們都是屬於類的,但是類為什么只能直接調用靜態類成員函數,而非靜態類成員函數(即使函數沒有參數)只有類對象才能調用呢

原因是:類的非靜態類成員函數其實都內含了一個指向類對象的指針型參數(即this指針),因此只有類對象才能調用(此時this指針有實值)

虛函數表

C++通過繼承和虛函數來實現多態性,虛函數是通過一張虛函數表實現的,虛函數表解決了繼承、覆蓋、添加虛函數的問題,保證其真實反應實際的函數

不太熟悉的朋友,以下內容可能看的很懵,個人建議上下來回看

虛函數表原理簡述

C++實現虛函數的方法是:為每個類對象添加一個隱藏成員,隱藏成員保存了一個指針,這個指針叫虛表指針(vptr),它指向一個虛函數表(virtual function table, vtbl)

虛函數表就像一個數組,表中有許多的槽(slot),每個槽中存放的是一個虛函數的地址(可以理解為數組里存放着指向每個虛函數的指針)

即:每個類使用一個虛函數表,每個類對象用一個虛表指針

在有虛函數的類的實例對象中,這個表被分配在了這個實例對象的內存中(就和上面說的一樣),當我們用父類的指針來操作一個子類的時候,這張表就像一個地圖一樣,指明了實際所應該調用的函數

大概結構如下:

在上面這個圖中,虛函數表的最后多加了一個結點,這是虛函數表的結束結點,就像字符串的結束符/0一樣,其標志了虛函數表的結束,這個結束標志的值在不同的編譯器下可能是不同的

舉個例子:
基類對象包含一個虛表指針,指向基類的虛函數表
派生類對象也將包含一個虛表指針,指向派生類虛函數表

  • 如果派生類重寫了基類的虛方法,該派生類虛函數表將保存重寫的虛函數的地址,而不是基類的虛函數地址
  • 如果基類中的虛方法沒有在派生類中重寫,那么派生類將繼承基類中的虛方法,而且派生類中虛函數表將保存基類中未被重寫的虛函數的地址,但如果派生類中定義了新的虛方法,則該虛函數的地址也將被添加到派生類虛函數表中

你可能已經暈了,沒有關系,接下來我們用實例代碼演示一下

找到虛函數表

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; }
};

按照上面的說法,我們可以通過Base的實例來得到虛函數表,這個表(數組)存了指向f,g,h這三個函數的指針

typedef void(*Fun)(void);

int main()
{
	Base bObj;
	Fun pFun = NULL;	
    //指向void* pf(void)類的函數的指針pFun

	cout << "虛函數表的地址:" << (int*)(&bObj) << endl;
	cout << "虛函數表的第一個函數地址:" << (int*) * (int*)(&bObj) << endl;
	//再次取址得到第一個虛函數的地址

	//第一個虛函數
	pFun = (Fun) * ((int*) * (int*)(&bObj));
	pFun();
}

我們拆分開來慢慢看這段代碼

typedef void(*Fun)(void);

typedef void(*Fun)(void)是利用類型別名聲明一個函數指針,指向的地址為NULL,等價於typedef decltype(void) *Fun

現在插入幾個斷點,以觀察指針pFun的變化:

Base實例化了對象了bObj,然后Fun pFun=NULL則是聲明了一個返回指向函數的指針

這里斷點斷在Fun pFun=NULL之前,可以看到pFun還未被初始化

初始化pFun=NULL后值變成了0x00000000

實例出對象bObj后,我們用(int*)(&bObj)強行把&bObj轉成int*,取得虛函數表的地址,也就是一個指向虛函數表這個數組的首元素的地址的指針,對這個指針再次取址就可以得到第一個虛函數(數組首元素)的地址了,也就是第一個虛函數Base::f()的地址

cout << "虛函數表的地址:" << (int*)(&bObj) << endl;
cout << "虛函數表的第一個函數地址:" << (int*) * (int*)(&bObj) << endl;
//再次取址得到第一個虛函數的地址

//第一個虛函數
pFun = (Fun) * ((int*) * (int*)(&bObj));
pFun();

你可能看不太懂這個操作,對(int*) * (int*)(&bObj)可以這樣理解,(int*)(&bObj)就是對象bObj被強制轉換成了int*了的地址,如果直接調用*(int*)(&bObj)則是指向對象bObj地址所指向的數據,但是此處是個虛函數表,所以指不過去,必須通過(int*)將其轉換成函數指針來進行指向,(int*) * (int*)(&bObj)的指向就變成了對象bObj中第一個函數的地址

又因為pFun是由Fun這個函數聲明的函數指針,所以相當於是Fun的實體,必須再將這個地址轉換成pFun認識的類型,即加上(Fun)*進行強制轉換

整個過程簡單來說,就是從bObj地址開始讀取四個字節的內容(&bObj),然后將這個內容解釋成一個內存地址((int*)(&bObj)),再訪問這個地址((int*) * (int*)(&bObj)),最后將這個地址中存放的值再解釋成一個函數的地址((Fun) * ((int*) * (int*)(&bObj)))

可以看到pFun的值已經等於虛函數表首元素(_vfptr[0])的值0x00b41168了,也就是說pFun這個指向函數的指針已經指向了函數f()(記住虛函數表存的是指向虛函數的指針,所以值就是這些虛函數的地址)

控制台的輸出:

和數組一樣,如果要調用Base::g()Base::h(),我們可以:

pFun = (Fun) * ((int*) * (int*)(&bObj));
    // (Fun) * ((int*) * (int*)(&bObj) + 1);	// Base::g()
    // (Fun) * ((int*) * (int*)(&bObj) + 2);	// Base::h()

再看一次這張圖,是不是更清晰了一點?

現在我們來看看出現繼承時的情況

單繼承(無覆蓋)

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 << "Derive::f1()" << endl; }
	virtual void g1() { cout << "Derive::g1()" << endl; }
	virtual void h1() { cout << "Derive::h1()" << endl; }
};

typedef void(*Fun)(void);

int main()
{
	//Base bObj;
	Derive dObj;
	Fun pFun = NULL;
	cout << "虛函數表的地址:" << (int*)(&dObj) << endl;
	cout << "虛函數表的第一個函數地址:" << (int*) * (int*)(&dObj) << endl;
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 0);
	pFun();
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 1);
	pFun();
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 2);
	pFun();
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 3);
	pFun();
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 4);
	pFun();
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 5);
	pFun();
	return 0;
}

通過vs斷點,我們發現到+3時,pFun的值變成了虛函數f1的地址:


運行結果:

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

也就是說

  1. 虛函數按照其聲明順序放於表中
  2. 父類的虛函數在子類的虛函數前

單繼承(有覆蓋)

現在我們修改下Derive類

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 f() { cout << "Derive::f()" << endl; }
	virtual void g1() { cout << "Derive::g1()" << endl; }
	virtual void h1() { cout << "Derive::h1()" << endl; }
};

這個繼承關系中,Derive的f()重載了Base類中的f(),下面我們用同樣的方法調試,main函數基本不變

int main()
{
	//Base bObj;
	Derive dObj;
	Fun pFun = NULL;
	cout << "虛函數表的地址:" << (int*)(&dObj) << endl;
	cout << "虛函數表的第一個函數地址:" << (int*) * (int*)(&dObj) << endl;
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 0);
	pFun();
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 1);
	pFun();
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 2);
	pFun();
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 3);
	pFun();
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 4);
	pFun();
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 5);
	pFun();
	return 0;
}

可以看到第一個函數變成了Derive::f(),並且運行到pFun = (Fun) * ((int*) * (int*)(&dObj) + 5)時,pFun的值變成了空

也就是說現在虛函數表的結構是這樣的:

也就是說

  1. 覆蓋的f()函數被放到了虛表中原來父類虛函數的位置
  2. 沒有被覆蓋的函數依舊

因為這個特性,我們就可以看到對於下面這樣的程序:

Base *b = new Derive();
b->f();

由b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代,於是在實際調用發生時,是Derive::f()被調用了,這就實現了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 f1() { cout << "Derive::f1()" << endl; }
	virtual void g1() { cout << "Derive::g1()" << endl; }
};
typedef void(*Fun)(void);

int main()
{
	//Base bObj;
	Derive dObj;
	Fun pFun = NULL;
	cout << "虛函數表的地址:" << (int*)(&dObj) << endl;
	cout << "虛函數表的第一個函數地址:" << (int*) * (int*)(&dObj) << endl;
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 0);
	pFun();
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 1);
	pFun();
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 2);
	pFun();
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 3);
	pFun();
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 4);
	pFun();
	pFun = (Fun) * ((int*) * (int*)(&dObj) + 5);
	pFun();
	return 0;
}

經過斷點可以看到,當運行到這里

pFun變成了空指針

控制台結果

為什么+5之后找不到了呢?因為在多繼承下,虛函數表存儲方式發生了點變化,我們之前說到C++編譯器在對象內加入了一個隱藏成員,現在你可以理解為,在多繼承時加入了多個隱藏成員,也就是說我們現在有多個虛函數表,具體排列方式如下圖:

那我們有沒有辦法訪問呢?強大的C++當然是有的,細心的你應該發現了,這個表(數組)其實只是變成了一個二維數組

int main()
{
	Fun pFun = NULL;
	Derive dObj;
	int** pVtab = (int**)& dObj;
	//Base1's vtable
	pFun = (Fun)pVtab[0][0];
	//等價於:pFun = (Fun) * ((int*) * (int*)((int*)& dObj + 0) + 0);
	pFun();
	pFun = (Fun)pVtab[0][1];
	pFun();
	pFun = (Fun)pVtab[0][2];
	pFun();

	//Derive's vtable
	pFun = (Fun)pVtab[0][3];
	pFun();
	//The tail of the vtable
	pFun = (Fun)pVtab[0][4];
	cout << pFun << endl;

	//Base2's vtable
	pFun = (Fun)pVtab[1][0];
	pFun();
	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)pVtab[2][0];
	pFun();
	pFun = (Fun)pVtab[2][1];
	pFun();
	pFun = (Fun)pVtab[2][2];
	pFun();
	pFun = (Fun)pVtab[2][3];
	cout << pFun << endl;
	return 0;
}

也就是說

多重繼承(有覆蓋)

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; }
};

main函數不再贅述,最終你會發現現在的虛函數表是這樣的:

安全性問題

水能載舟,亦可賽艇亦能覆舟,接下來讓我們看看虛函數表可以用來干點什么壞事吧

通過父類型的指針訪問子類自己的虛函數

雖然在上面的圖中我們可以看到Base1的虛表中有Derive的虛函數,但我們根本不可能使用下面的語句來調用子類的自有虛函數:

Base1 *b1 = new Derive();
b1->f1();  //編譯出錯

任何妄圖使用父類指針調用子類中的未覆蓋父類的成員函數的行為都會被編譯器視為非法,所以,這樣的程序根本無法編譯通過

但通過多繼承部分的代碼你應該已經發現了

在運行時,我們可以通過指針的方式訪問虛函數表來達到違反C++語義的行為(也就是我們在多重繼承中使用的代碼)

Fun pFun = NULL;
Derive dObj;
int** pVtab = (int**)& dObj;
//Base1's vtable
pFun = (Fun)pVtab[0][0];
//等價於:pFun = (Fun) * ((int*) * (int*)((int*)& dObj + 0) + 0);
//Derive's vtable
pFun = (Fun)pVtab[0][3];
pFun();
//The tail of the vtable
pFun = (Fun)pVtab[0][4];
cout << pFun << endl;

訪問非public的虛函數

父類非public的虛函數同樣會存在於虛函數表中,所以,我們同樣可以使用訪問虛函數表的方式來訪問這些non-public的虛函數,這是很容易做到的

class Base {
private:
	virtual void f() { cout << "Base::f" << endl; }
};

class Derive : public Base {
};

typedef void(*Fun)(void);

void main() {
	Derive d;
	Fun  pFun = (Fun) * ((int*) * (int*)(&d) + 0);
	pFun();	//挖藕?
}

最后注意

虛函數表不一定是存在最開頭,但是目前各個編譯器大多是這樣設置的


免責聲明!

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



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