C++之虛函數表和vptr指針

一、序章

如果說指針是C語言的精髓,那C++的精髓就是多態,而多態實現的基礎是動態聯編(晚綁定),動態聯編實現的基礎是虛函數.
在C++中是這樣是這樣規定虛函數:

	1.函數前面加上關鍵字virtua 就形成了虛函數
	2,當指針或者引用在調用虛函數的時候,編輯器會根據其指向的實際對象,調用相應的虛函數,也就是說 派生類重寫基類的虛函數的時候,當基類指針指向派生類對象並在調用虛函數的時候,這時基類指針會調用派生類重寫的虛函數,

其實C++僅僅是規定了虛函數的規則,但是其具體的實現卻是由編輯器來實現的,而編輯器實現虛函數的原理就是虛函數表和vptr指針.
虛函數表的原理是:

1,當類中出現了虛函數的時,每當類生成一個對象時,編輯器會自動在該對象首部添加一個vptr指針,
指向 一個獨立的指針數組,該數組中存儲着虛函數地址,當指針(引用)調用虛函數的時候,
會通過這個vptr指針找到虛函數表中對應的虛函數地址,並調用.
2,當派生類繼承基類,會繼承他的vptr指針和虛擬表,當派生類重寫基類的虛函數的時候,
編輯器會更改虛函數表中相應的虛函數地址,將其改爲派生類重寫後的虛函數地址.

先在我們大概知道虛函數表的原理,那麼我們需要詳細的剖析和重現期原理,我們首先證明vptr的存在並且虛函數表中存儲的是虛函數地址

二、 證明vptr指針的存在&虛函數表中存儲着虛函數地址

先看下沒有虛函數,沒有vptr指針的一個代碼示例,這樣有助於我們後面對虛函數的比較

	#include"iostream";
	using namespace std;
	class Parent {
	public:
		void func1() {
			cout<<"Parent----func1--"<<endl;
		}
	private:
		int a;
		int b;
	};
	int main() {
		Parent p;
		p.func1();
		system("pause");
		return 0;
	}

上面的代碼 我們用命令行查看 類Parent的對象在內存中分部情況

1>class Parent	size(8): //說明在內存中總共佔據8個字節
1>	+---
1> 0	| a //a的地址偏移量是0,說明a是首地址和對象的地址相同
1> 4	| b //b的地址偏移量是4 
1>	+---

上面我們看出在內存中Parent的對象一共佔據8個字節 這是正確的 我們知道 類對象的大小是所有成員變量大小的總和,因爲成員函數是屬於類的所有對象共享成員函數,所以成員函數是不在類對象所佔內存中的,
對於類Parent的對象 其大小是成員變量大小的總和就是8個字節

上面的對象內存分佈 我們可以可以看到變量a的地址偏移量是0,說明:對象p的地址 和對象p中成員變量a的地址是相同的 下圖是是在vs監控中對象p的地址和其成員的地址,其中p的地址正好和a的地址相同,此外我們還可以看出對於類對象的內存塊中,成員變量的地址順序按照聲明的順序排列的
對象p的內存

接下來我們將函數func1 變成虛函數 觀察對象p的內存變化 即在成員函數 func1前面加上virtual,其他不變

	virtual void func1() {
			cout<<"Parent----func1--"<<endl;
		}

通過命令行獲取其對象內存分佈:

	1>class Parent	size(12):
	1>	+---
	1> 0	| {vfptr}//多出了變量vptr,其地址偏移量是0 說明其地址和對象的地址相同
	1> 4	| a      //偏移量是4 說明a不是首地址了 而且vptr的大小是4個字節
	1> 8	| b
	1>	+---

此處說明,當類中出現了虛函數時 編輯器會自動爲每個類對象生成一個vptr的變量 其大小是4個字節, 我們通過斷點查看其類型是什麼,如下圖所示
有虛函數的對象p的內存

注意:上面我們可以看到vptr的類型是 void ** 說明vptr是 指針的指針(二級指針) 類型 而我們注意vptr下面是個數組,共有一個元素,元素類型是 void *.
注意上面監控的地址,這是根據上圖整理來的對象p的內存

變量 內存地址 偏移量
&p 0x005cf7ec 0
&p.vptr 0x005cf7ec 0
&p.a 0x005cf7f0 4
&p.b 0x005cf7f4 8

從上圖和上標我們可以得出結論:

  1. 當類出現虛函數表的時候,編輯器會自動爲對象生成一個vptr指針,其指向一個指針數組
  2. vptr指針地址和對象的首地址相同,而且其大小是4個字節

根據上面的結論我們可以得到,虛函數表存儲的每一個指針,接下來我們來驗證虛函數表存儲的的確就是虛函數地址,.如果我們用虛函數表存儲的指針調用了虛函數,則證明虛函數表中的確存儲着虛函數地址,
還是用上面的例子不過我們需要修改主函數裏的內容 整體代碼如下:

#include"iostream";
using namespace std;
class Parent {
public:
	virtual void func1() {
		cout<<"Parent----func1--"<<endl;
	}
private:
	int a;
	int b;
};
typedef void(*Fn)();
int main() {
	Parent parent;
	Parent *p1 = &parent;//1
	void * pp1 = *(void **)p1;//2
	Fn f = (Fn)*(int *)pp1;//3
	f();
	system("pause");
	return 0;
}

打印結果是:
Parent----func1–

說明正確的調用了 func1函數,證明了虛函數表存儲的的確是虛函數地址.

我們從上面圖中得到結果是:vptr 存儲的值是 void ** 類型 而其指向的指針數組存儲的值是 void* 類型 所以下面解析下 從對象地址得到虛函數指針的過程

  1. 第一步,因爲對象p地址和vptr指針的值都是對象p的首地址,所以這裏得到vptr的首地址
  2. 第二步,因爲vptr是 void ** 類型所以將其轉換爲 vptr 指針類型 再解引用,取到vptr存儲的值
  3. 我們知道vptr存儲的是虛函數表,指針數組的地址,而數組地址和數組首元素的地址是相同,所以這裏我們將vptr存儲的值轉換爲 void * 類型再解引用就可以得到虛函數表中的第一個虛函數的地址,但是因爲VS中不允許將指針轉換爲void *類型,所以這裏我們將vptr的值轉換爲int * 類型(注:所以指針大小在環境下都是相同的,我的環境是4個字節),所以有 *(int *)pp1,這時已經得到虛函數表存儲的第一個元素的值,即是:虛函數的首地址,這裏我們將其轉換爲虛函數相應的指針類型,這裏我們便得到虛函數指針.3.我們知道vptr存儲的是虛函數表,指針數組的地址,而數組地址和數組首元素的地址是相同,所以這裏我們將vptr存儲的值轉換爲 void * 類型再解引用就可以得到虛函數表中的第一個虛函數的地址,但是因爲VS中不允許將指針轉換爲void *類型,所以這裏我們將vptr的值轉換爲int * 類型(注:所以指針大小在環境下都是相同的,我的環境是4個字節),所以有 *(int *)pp1,這時已經得到虛函數表存儲的第一個元素的值,即是:虛函數的首地址,這裏我們將其轉換爲虛函數相應的指針類型,這裏我們便得到虛函數指針.

三、 同一類的不用對象的虛函數表&基類和派生類的虛函數表

  • 該小節我們問題是,同一類的不同對象的虛函數表和vptr是否相同
  • 基類和派生類的虛函數表和vptr是否相同
  • vptr初始化的時間,或者說是生成的時間

(1)同一類的不同對象的虛函數表是否相同

我們生成多個對象觀察其vptr的值是否相同 相同說明指向同一個虛函數表

#include"iostream";
using namespace std;
class Parent {
public:
	Parent() {
		a = 0;
		b = 0;
	}
	Parent(int a,int b) {
		this->a = a;
		this->b = b;
	}
	virtual void func1() {
		cout<<"Parent----func1--"<<endl;
	}
private:
	int a;
	int b;
};
typedef void(*Fn)();
int main() {
	Parent p1;
	Parent p2(1,2);
	Parent p3(3,4);
	Parent p4(5,6);
	p4.func1();
	system("pause");
	return 0;
}

我們打斷點觀察4個對象的vptr指針,結果如下:
同一類的不同對象的vptr指針指向的值

上圖是生成4的不同對象的vptr其指向的對象的值是相同的說明其指向同一個虛函數表,這也說明了
同一類的所有對象共享同一個虛函數表,

(2)vptr指針初始化的時間(沒有基類)

注意我們這裏分析的是沒有繼承的時候vptr初始化的時間,當有繼承的時候,派生類vptr初始話要複雜的多,這裏先不做分析
我們都知道當類生成對象的時候,首先是從構造函數開始的,所以這裏我們先對構造函數進行反編譯看看是否有vptr的聲明如下
這是parent的無參構造函數

	Parent() {//構造函數開始
01031F40  push        ebp  
01031F41  mov         ebp,esp  
01031F43  sub         esp,0CCh  
01031F49  push        ebx  
01031F4A  push        esi  
01031F4B  push        edi  
01031F4C  push        ecx  
01031F4D  lea         edi,[ebp-0CCh]  
01031F53  mov         ecx,33h  
01031F58  mov         eax,0CCCCCCCCh  
01031F5D  rep stos    dword ptr es:[edi]  
01031F5F  pop         ecx  
01031F60  mov         dword ptr [this],ecx  
01031F63  mov         ecx,offset _5926205C_虛函數\虛函數\test.cpp (0103F027h)  
01031F68  call        @__CheckForDebuggerJustMyCode@4 (01031299h)  
01031F6D  mov         eax,dword ptr [this]  //傳入this指針
01031F70  mov         dword ptr [eax],offset Parent::`vftable' (01039B34h)  //這裏定義vptr(對象首地址)指向vftable
		a = 0;//定義a
01031F76  mov         eax,dword ptr [this]  
01031F79  mov         dword ptr [eax+4],0  
		b = 0;//定義b
01031F80  mov         eax,dword ptr [this]  
01031F83  mov         dword ptr [eax+8],0  
	}

01031F6D mov eax,dword ptr [this] //傳入this指針
01031F70 mov dword ptr [eax],offset Parent::`vftable’ (01039B34h) //這裏定義vptr(對象首地址)指向vftable

注意上面兩句彙編語言,首先傳入this指針,然後讓首地址指向虛函數表Parent::`vftable’
然後才定義變量a和變量b

在有參構造函數Parent(int a,int b) {},反編譯的結果類似

結論:在類構造函數中編輯器會首先自動添加vptr指針並指向虛函數表的地址,然後再編輯用戶的代碼,換句話說在類的構造函數中會首先執行vptr的初始化然後再執行其他的

(3)基類和派生類的虛函數表是否相同

爲了查看基類和派生類的虛函數表是否相同,我們需要新定義一個派生類用來繼承基類,代碼如下

#include"iostream";
using namespace std;
class Parent {
public:
	Parent() {
		a = 0;
		b = 0;
	}
	Parent(int a,int b) {
		this->a = a;
		this->b = b;
	}
	virtual void func1() {
		cout<<"Parent----func1--"<<endl;
	}
private:
	int a;
	int b;
};
class Child:public Parent {
private:
	int c;

};
typedef void(*Fn)();
int main() {
	Parent p1;
	Child  c1;
	p1.func1();
	c1.func1();
	system("pause");
	return 0;
}

這裏我們打斷點觀察 p1和c1的vptr指針的值是否相同 相同說明派生類和基類指向同一個虛函數表,否則基類和派生類的虛函數表不相同.斷點結果如下:
基類和派生類的虛函數表
上圖中
c1 vptr指向的值是:0x010f9b58
p1 vptr指向的值是:0x010f9b34
c1 和p1的vptr指向不同 說明兩個類的虛函數表示不同的

總結,不同類的虛函數表都是獨立的,不同,(這裏說的是整個虛函數表對象而不是其內容)

前面我們分析了,vptr的產生以及初始化,驗證了虛函數表的虛函數指針,以及比較同一類的不同虛函數表,以及不同類的虛函數表, 接下來我們來分析

  1. 虛函數表的核心–虛函數地址的替換,
  2. vptr的分部初始化
  3. 各種不同情況下,派生類虛函數表的生成以及和基類虛函數表的關係

四、 虛函數表的核心–虛函數地址的替換

原理:虛函數表實現基類指針指向派生類對象的時,調用派生類重載函數時,會調用派生類的函數而不是基類的函數的原因是就是:派生類的虛函數首先會拷貝基類的虛函數內容,然後讓派生類重寫了基類的虛函數時,派生類的虛函數表就會將重寫的函數地址改爲派生類重載的函數地址,而不再是父類的虛函數地址了,

接下來我們驗證下結果是否如此,我們首先定義一個基類,並且定義2個虛函數,然後在定義一個派生類來繼承該類 並且重寫其中的一個虛函數,再觀察虛函數表

#include"iostream";
using namespace std;
class Parent {
public:
	Parent() {
		a = 0;
		b = 0;
	}
	Parent(int a,int b) {
		this->a = a;
		this->b = b;
	}
	virtual void func1() {
		cout<<"Parent----func1--"<<endl;
	}
	virtual void func2() {
		cout << "Parent----func2--" << endl;
	}
private:
	int a;
	int b;
};
class Child:public Parent {
public:
	virtual void func1() {
		cout << "Child----func1--" << endl;
	}
private:
	int c;
};
typedef void(*Fn)();
int main() {
	Parent p1;
	Child  c1;
	p1.func1();
	c1.func1();
	system("pause");
	return 0;
}

打斷點對比基類和派生類的虛函數表
基類和虛函數的虛函數表對比
上圖顯示:

  1. c1和p1的vptr指向的值不相同,說明不用類的虛函數表不同
  2. c1和p1的虛函數表的內存都是含有2個虛函數指針(因爲派生類沒有新增虛函數)
  3. c1和p1的虛函數表的第二個元素相同,指向的都是 Parent::func2(void);
  4. c1虛函數表的第一個元素指向的是:Child::func1(void);而p1虛函數的第一個元素指向的是Parent::func1(void);

所以得出結論:
派生類聲明後編輯器在編譯期間會新創建一個獨立的虛函數表,然後複製基類虛函數表的內容,
然後查看派生類是否重寫了基類的虛函數,如果重寫了,則將虛函數表中的該虛函數地址改爲派生類重寫的函數地址,如果沒重寫則不操作.

五、vptr的分步初始化

我們在上面反彙編知道了,在類的構造函數中編輯器會添加初始化vptr的操作,
在繼承中派生類會繼承基類的一切,構造方法也會繼承下來,派生類在調用構造函數的時候,必須先調用基類的構造函數,執行結束後纔會執行派生類的構造函數,而我們知道基類的構造函數中肯定是有vptr初始化的,而且指向的是基類的虛函數表,而我們在上面的結論中又知道基類和派生類的vptr指向是不同的所以派生類的構造函數中肯定有什麼執行,
所以我們接下來反彙編基類和派生類的構造函數,

代碼和"虛函數表的核心"中代碼一樣 我們接下來反彙編 Parent和Child的無參構造函數

Parent的無參構造函數的反彙編

	Parent() {
010D1F40  push        ebp  
010D1F41  mov         ebp,esp  
010D1F43  sub         esp,0CCh  
010D1F49  push        ebx  
010D1F4A  push        esi  
010D1F4B  push        edi  
010D1F4C  push        ecx  
010D1F4D  lea         edi,[ebp-0CCh]  
010D1F53  mov         ecx,33h  
010D1F58  mov         eax,0CCCCCCCCh  
010D1F5D  rep stos    dword ptr es:[edi]  
010D1F5F  pop         ecx  
010D1F60  mov         dword ptr [this],ecx  
010D1F63  mov         ecx,offset _5926205C_虛函數\虛函數\test.cpp (010DF027h)  
010D1F68  call        @__CheckForDebuggerJustMyCode@4 (010D1299h)  
010D1F6D  mov         eax,dword ptr [this]  
010D1F70  mov         dword ptr [eax],offset Parent::`vftable' (010D9B34h)  //(1) 初始化首地址指向類Parent的虛函數表
		a = 0;
010D1F76  mov         eax,dword ptr [this]  
010D1F79  mov         dword ptr [eax+4],0   //(2)初始化 a
		b = 0;
010D1F80  mov         eax,dword ptr [this]  
010D1F83  mov         dword ptr [eax+8],0  //(3) 初始化b
	}

Child的無參構造函數的反彙編

	Child() {
010D1D10  push        ebp  
010D1D11  mov         ebp,esp  
010D1D13  sub         esp,0CCh  
010D1D19  push        ebx  
010D1D1A  push        esi  
010D1D1B  push        edi  
010D1D1C  push        ecx  
010D1D1D  lea         edi,[ebp-0CCh]  
010D1D23  mov         ecx,33h  
010D1D28  mov         eax,0CCCCCCCCh  
010D1D2D  rep stos    dword ptr es:[edi]  
010D1D2F  pop         ecx  
010D1D30  mov         dword ptr [this],ecx  
010D1D33  mov         ecx,offset _5926205C_虛函數\虛函數\test.cpp (010DF027h)  
010D1D38  call        @__CheckForDebuggerJustMyCode@4 (010D1299h)  
010D1D3D  mov         ecx,dword ptr [this]  
010D1D40  call        Parent::Parent (010D10AAh)  
010D1D45  mov         eax,dword ptr [this]  
010D1D48  mov         dword ptr [eax],offset Child::`vftable' (010D9B54h)  //(4) 再次初始化vptr將對象首地址指向 新建的派生類的虛函數表Child::`vftable'
		c = 0;
010D1D4E  mov         eax,dword ptr [this]  
010D1D51  mov         dword ptr [eax+0Ch],0  //(5) 初始化變量C
	}

創建派生類對象,將會依次執行parent和child的構造函數,
我們注意首先執行(1) 初始化vptr(對象首地址),將其指向基類Parent的虛函數表Parent::`vftable’
再執行(2)初始化變量a
再執行(3)初始化變量b
--------------基類構造函數---------
再執行(4)造次初始化vptr(對象首地址),將其指向派生類的虛函數表Child::‘vftable’
再執行(5)初始化變量c 注意初始化變量C的時候 ptr [eax+0Ch] 也就是說c的地址是對象首地址加上 0Ch而這個0Ch就是基類對象的大小

利用命名行打印
基類Parent的內存分佈
1>class Parent size(12):
1> ±–
1> 0 | {vfptr}
1> 4 | a
1> 8 | b
1> ±–
1>
派生類 Child的內存分佈
1>class Child size(16):
1> ±–
1> 0 | ±-- (base class Parent)
1> 0 | | {vfptr}
1> 4 | | a
1> 8 | | b
1> | ±–
1>12 | c //正好印證 dword ptr [eax+0Ch],0 而och是12 eax是對象首地址0
1> ±–

結論:對於單繼承而言,vptr的初始化是分步進行的,首先執行基類構造函數,在執行基類構造函數中最先執行vptr初始化指向基類的虛函數表.當基類的構造函數的執行完畢後 再執行派生類的構造函數 在派生類的構造函數中 最先執行vptr的二次初始化,指向派生類虛函數表.
引申:
1. 每執行一次構造函數就會初始化一次vptr執行 而其指向的是構造函數所屬對象的虛函數表
2. 所以在基類的構造函數中調用虛函數,執行的也是基類的虛函數,而不是派生類重載後的虛函數(針對所有具有繼承關係的類都適用

六、各種不同情況下,派生類虛函數表的生成以及和基類虛函數表的關係

注:此處內容較多可跳過,直接看結論
我們主要觀察基類和派生類的

(1)單繼承 --基類有虛函數 派生類沒有新增的虛函數並且不重寫基類虛函數

代碼:

#include"iostream";
using namespace std;
class Parent {
public:
	Parent() {
		a = 0;
		b = 0;
	}
	Parent(int a,int b) {
		this->a = a;
		this->b = b;
	}
	virtual void func1() {
		cout<<"Parent----func1--"<<endl;
	}
	virtual void func2() {
		cout << "Parent----func2--" << endl;
	}
private:
	int a;
	int b;

};
class Child:public Parent {
public:
	Child() {
		c = 0;
	}
private:
	int c;
};
int main() {
	Parent p1;
	Child  c1;
	p1.func1();
	c1.func1();
	system("pause");
	return 0;
}

打斷點觀察基類和派生類vptr和虛函數表
在這裏插入圖片描述
這裏我們看到

  • c1和p1的vptr分別指向兩個不同的地址,說明基類和派生類的虛函數表不同(前面已經驗證過了)
  • c1和p1的虛函數表完全相同,都是包含兩個地址,第一個指向Parent::func1(void) 第二個指向 Parent::func2(void),說明當派生類沒有新增虛函數並且沒有重寫基類的虛函數的時候,派生類創建一個新的虛函數表,並且拷貝基類虛函數表的內容不做增改

通過命令行打印派生類 Child的內存佈局(和我們打斷點得到的圖印證)

1>class Child	size(16)://
1>	+---
1> 0	| +--- (base class Parent)
1> 0	| | {vfptr}
1> 4	| | a
1> 8	| | b
1>	| +---
1>12	| c
1>	+---
1>

(2)單繼承 --基類有虛函數 派生類沒有新增的虛函數 ,但是重寫基類虛函數

代碼:

#include"iostream";
using namespace std;
class Parent {
public:
	Parent() {
		a = 0;
		b = 0;
	}
	Parent(int a,int b) {
		this->a = a;
		this->b = b;
	}
	virtual void func1() {
		cout<<"Parent----func1--"<<endl;
	}
	virtual void func2() {
		cout << "Parent----func2--" << endl;
	}
private:
	int a;
	int b;

};
class Child:public Parent {
public:
	Child() {
		c = 0;
	}
	virtual void func1() {
		cout << "Child----func1--" << endl;
	}
private:
	int c;
};
int main() {
	Parent p1;
	Child  c1;
	p1.func1();
	c1.func1();
	system("pause");
	return 0;
}

打斷點觀察基類和派生類vptr和虛函數表
在這裏插入圖片描述

這裏我們注意相對於(1)中的內容:此時c1的虛函數表原本指向基類func1虛函數地址 ,此時改爲指向派生類的重寫後的Child::func1(void)虛函數的地址;

結論:當派生類重寫基類的虛函數的時候,編輯器會將派生類的虛函數表中原本指向基類的虛函數地址改爲指向派生類重寫後的虛函數地址

(3)單繼承 --基類有虛函數 派生類有新增虛函數

代碼:

#include"iostream";
using namespace std;
class Parent {
public:
	Parent() {
		a = 0;
		b = 0;
	}
	Parent(int a,int b) {
		this->a = a;
		this->b = b;
	}
	virtual void parent_func1() {
		cout<<"Parent----parent_func1--"<<endl;
	}
	virtual void parent_func2() {
		cout << "Parent----parent_func2--" << endl;
	}
private:
	int a;
	int b;

};
class Child:public Parent {
public:
	Child() {
		c = 0;
	}
	virtual void parent_func1() {
		cout << "Child----parent_func1--" << endl;
	}

	virtual void child_func1() {
		cout << "Child----child_func1--" << endl;
	}
private:
	int c;
};
int main() {
	Parent p1;
	Child  c1;
	c1.child_func1();
	system("pause");
	return 0;
}

打斷點觀察基類和派生類的虛函數表:
在這裏插入圖片描述

c1的虛函數表中這時居然只有2個值,並沒有值指向類Child新增的虛函數,居然和(2)中的圖一樣,why 難道新增的虛函數沒有加上,
我們知道虛函數表是指針數組.現在已知這個數組中有兩個值,那我們看看這個第三個值是否是否,不爲空,那是否是新增的虛函數的指針,
修改main函數代碼

typedef void(*Fn)(void);
int main() {
	Parent p1;
	Child  c1;
	Fn fn = (Fn)*((int *)*(void **)&c1+2);//上面內容已經說過了如何取得虛函數表中的虛函數地址 
	cout << "fn====" << fn << endl;
	fn();

	system("pause");
	return 0;
}

打印結果是:
fn====002D1451
Child----child_func1–
說明虛函數表中的第三個元素不是空,而且就是Child類新增的虛函數地址,而且我們也可以通過反彙編進一步驗證
代碼

Child  c1;
	Child & c2 = c1;
	c2.child_func1();

反彙編c2.child_func1();

	c2.child_func1();
01365ED8  mov         eax,dword ptr [c2]  //取指針c2的值(c1的指針)賦值給eax
01365EDB  mov         edx,dword ptr [eax]  //將c1指針指向的值(對象c1的首地址)賦值給edx
01365EDD  mov         esi,esp  
01365EDF  mov         ecx,dword ptr [c2]  //取指針c2的值(c1的指針)賦值給ecx
01365EE2  mov         eax,dword ptr [edx+8]  //將將指針(首地址+8)指向的值賦值給eax
01365EE5  call        eax      //執行eax
01365EE7  cmp         esi,esp  
01365EE9  call        __RTC_CheckEsp (013612A3h)

我們知道對象的首地址也是vptr的值,所以講vptr+8也就是執行虛函數表的第三個元素,就是說,派生類新增的虛函數地址是加載虛函數表的後面,
關於爲什麼虛函數表沒有顯示,個人理解是因爲派生類的虛函數表的建立首先要看基類是否有虛函數表,如果有就進行拷貝,而這時因爲拷貝是基類的虛函數表,基類並不知道派生類是否有新增的虛函數,所以有索引的只有基類的虛函數,然後再看派生類是否有新增的虛函數,如果有的話則將其地址依次加載虛函數表的後面

如果基類沒有虛函數,我們就會發現派生類的虛函數表所有的虛函數都有索引,嗯 是不是有點感覺了 下面給出代碼驗證下

class Parent {
public:
	Parent() {
		a = 0;
		b = 0;
	}
	Parent(int a,int b) {
		this->a = a;
		this->b = b;
	}
	 void parent_func1() {
		cout<<"Parent----parent_func1--"<<endl;
	}
	 void parent_func2() {
		cout << "Parent----parent_func2--" << endl;
	}
private:
	int a;
	int b;
};
class Child:public Parent {
public:
	Child() {
		c = 0;
	}
	virtual void child_func1() {
		cout << "Child----child_func1--" << endl;
	}
	virtual void child_func2() {
		cout << "Child----child_func2--" << endl;
	}
private:
	int c;
};

在這裏插入圖片描述

vptr顯示的的確如我們所說

(4)多繼承

我現在看下多繼承,並且基類都有虛函數,派生類有新增的虛函數
代碼

#include"iostream";
using namespace std;
class Parent {
public:
	Parent() {
		a = 0;
		b = 0;
	}
	Parent(int a,int b) {
		this->a = a;
		this->b = b;
	}
	 virtual void parent_func1() {
		cout<<"Parent----parent_func1--"<<endl;
	}
	 virtual void parent_func2() {
		cout << "Parent----parent_func2--" << endl;
	}
private:
	int a;
	int b;

};
class Parent1 {
public:
	virtual void parent1_func1() {
		cout<<"Parent1----parent1_func1"<<endl;
	}
	virtual void parent1_func2() {
		cout << "Parent1----parent1_func2" << endl;
	}
private:
	int p1_a;

};
class Parent2 {
public:
	virtual void parent2_func1() {
		cout << "Parent2----parent1_func1" << endl;
	}
	virtual void parent2_func2() {
		cout << "Parent2----parent1_func2" << endl;
	}
private:
	int p1_a;
};
class Child:public Parent,public Parent1 {
public:
	Child() {
		c = 0;
	}
	virtual void child_func1() {
		cout << "Child----child_func1--" << endl;
	}
	virtual void child_func2() {
		cout << "Child----child_func2--" << endl;
	}
private:
	int c;
};
int main() {
	Child  c1;
	c1.child_func1();
	system("pause");
	return 0;
}

我們通過VS看下對象c1的變量:
在這裏插入圖片描述
追高亮的地方 ,對象c1包含兩個繼承類對象,每個繼承類對象都有一個vptr指針,而且其指向的值還不相同,說明指向2個不同的虛函數表, 嗯? 我們再通過命令行觀看下類Child的內存分佈

1>class Child	size(24):
1>	+---
1> 0	| +--- (base class Parent) //偏移量0 包含的基類對象 Parent的開始
1> 0	| | {vfptr}  //偏移量0 包含的基類Parent對象的vptr 指向由基類Parent拷貝而來的派生類的虛函數表
1> 4	| | a
1> 8	| | b
1>	| +---
1>12	| +--- (base class Parent1) //偏移量12(正好是類Parent的大小)  包含的Parent1對象的開始
1>12	| | {vfptr} //偏移量12 包含的基類Parent1對象的vptr 指向由基類Parent1拷貝而來的派生類的虛函數表
1>16	| | p1_a
1>	| +---
1>20	| c
1>	+---
1>
1>Child::$vftable@Parent@://派生類拷貝Parent的虛函數表
1>	| &Child_meta//共有5個成員
1>	|  0  
1> 0	| &Parent::parent_func1 //第一個元素 指向\Parent類的parent_func1 
1> 1	| &Parent::parent_func2 /第二個元素 指向\Parent類的parent_func2 
1> 2	| &Child::child_func1  /第三個元素 指向派生類新增的child_func1  
1> 3	| &Child::child_func2  /第四個元素 指向派生類新增的child_func2
1>                                        //空成員 標誌着虛函數表的結束
1>Child::$vftable@Parent1@: //派生類拷貝Parent1的虛函數表
1>	| -12    
1> 0	| &Parent1::parent1_func1 //第一個元素 指向\Parent1類的parent1_func1 
1> 1	| &Parent1::parent1_func2 //第二個元素 指向\Parent1類的parent1_func2 
1>                                            //空成員 標誌着虛函數表的結束
1>Child::child_func1 this adjustor: 0
1>Child::child_func2 this adjustor: 0

通過上面我們 得到了什麼?

  1. 多繼承,繼承來每個基類對象都包含一個vptr指針,分別指向一個由基類虛函數表拷貝而來的獨立虛函數表,
  2. 派生類新增的虛函數地址會添加在第一個vptr指針指向的虛函數表(這個可能還有疑問我們等下驗證)

我們驗證下派生類新增的虛函數地址是添加在哪一個虛函數表的後面 ,嗯 我們將Child的繼承方式修改下

class Child:public Parent1,public Parent 

通過命令行打印Child類的內存分佈

1>class Child	size(24):
1>	+---
1> 0	| +--- (base class Parent1)//首地址 變成了 嵌套的Parent1對象的首地址 
1> 0	| | {vfptr}   //嵌套的Parent1對象的vptr指針 我們看下面Parent1的虛函數表是否包含 派生類新增的虛函數地址
1> 4	| | p1_a
1>	| +---
1> 8	| +--- (base class Parent)
1> 8	| | {vfptr}
1>12	| | a
1>16	| | b
1>	| +---
1>20	| c
1>	+---
1>
1>Child::$vftable@Parent1@://嵌套的Parent1對象指向的虛函數表
1>	| &Child_meta
1>	|  0
1> 0	| &Parent1::parent1_func1
1> 1	| &Parent1::parent1_func2
1> 2	| &Child::child_func1   //果然這裏包含派生類新增的虛函數地址
1> 3	| &Child::child_func2
1>
1>Child::$vftable@Parent@:
1>	| -8
1> 0	| &Parent::parent_func1
1> 1	| &Parent::parent_func2
1>

這裏我們得到: 真的有虛函數的基類,派生類內存中嵌套的基類對象順序和聲明的繼承順序相同,而且派生類新增的虛函數地址會添加在第一個虛函數地址的後面
上面我爲什麼說是針對有虛函數的基類呢,下面我們在繼承的第一順序新添加一個無虛函數的基類
代碼:

class Parent {
public:
	Parent() {
		a = 0;
		b = 0;
	}
	 virtual void parent_func1() {
		cout<<"Parent----parent_func1--"<<endl;
	}
	 virtual void parent_func2() {
		cout << "Parent----parent_func2--" << endl;
	}
private:
	int a;
	int b;

};
class Parent1 {
public:
	virtual void parent1_func1() {
		cout<<"Parent1----parent1_func1"<<endl;
	}
	virtual void parent1_func2() {
		cout << "Parent1----parent1_func2" << endl;
	}
private:
	int p1_a;

};
class Parent2 {
public:
	virtual void parent2_func1() {
		cout << "Parent2----parent1_func1" << endl;
	}
	virtual void parent2_func2() {
		cout << "Parent2----parent1_func2" << endl;
	}
private:
	int p1_a;

};
class Parent3 {
public:
	 void parent3_func1() {
		cout << "Parent3----parent1_func1" << endl;
	}
	 void parent3_func2() {
		cout << "Parent3----parent1_func2" << endl;
	}
private:
	int p1_a;

};

class Child:public Parent3, public Parent1,public Parent, public Parent2 {
public:
	Child() {
		c = 0;
	}	
	virtual void child_func1() {
		cout << "Child----child_func1--" << endl;
	}
	virtual void child_func2() {
		cout << "Child----child_func2--" << endl;
	}
private:
	int c;
};

好了 我們現在通過命令行看下 派生類的內存分佈

1>class Child	size(36):
1>	+---
1> 0	| +--- (base class Parent1)
1> 0	| | {vfptr}
1> 4	| | p1_a
1>	| +---
1> 8	| +--- (base class Parent)
1> 8	| | {vfptr}
1>12	| | a
1>16	| | b
1>	| +---
1>20	| +--- (base class Parent2)
1>20	| | {vfptr}
1>24	| | p1_a
1>	| +---
1>28	| +--- (base class Parent3)
1>28	| | p1_a
1>	| +---
1>32	| c
1>	+---
1>
1>Child::$vftable@Parent1@://派生類新增的虛函數地址,在該虛函數地址的後面
1>	| &Child_meta
1>	|  0
1> 0	| &Parent1::parent1_func1
1> 1	| &Parent1::parent1_func2
1> 2	| &Child::child_func1
1> 3	| &Child::child_func2
1>
1>Child::$vftable@Parent@:
1>	| -8
1> 0	| &Parent::parent_func1
1> 1	| &Parent::parent_func2
1>
1>Child::$vftable@Parent2@:
1>	| -20
1> 0	| &Parent2::parent2_func1
1> 1	| &Parent2::parent2_func2
1>
1>Child::child_func1 this adjustor: 0
1>Child::child_func2 this adjustor: 0

注意我們的代碼:

class Child:public Parent3, public Parent1,public Parent, public Parent2

而Parent3是無虛函數,其他的都是有虛函數的
注意上面通過命令行打印出來的類Child的內存分佈,
child的內存一共嵌套4個基類對象,其順序是 Parent1 Parent Parent2 Parent3 +派生類自己的成員變量,而且每個有虛函數的嵌套對象都有一個vptr指針指向一個由基類拷貝出來的獨立虛函數表,而派生類新增的虛函數地址添加在第一個虛函數表的後面,

結論:在多繼承的派生類內存分佈中有如下規則

1. 無虛函數的嵌套基類對象一定排在有虛函數的嵌套基類對象的後面
2. 有虛函數的嵌套基類對象的排列順序和派生類聲明時的繼承順序相同
3. 無虛函數的嵌套基類對象的排列順序和派生類聲明時的繼承順序相同(這點我就不在寫出驗證了)
4. 派生類新增的虛函數地址添加在派生類對象首地址指向的虛函數表的後面

七、總結

一、概論
實現多態的核心是動態聯編,而動態聯編的原理就是虛函數表和vptr指針
二、本質
虛函數表的本質是指針數組,其元素是指向虛函數的指針,而vptr是指向指針數組的指針,是void ** 類型,

三、生成
(1)對於無繼承的類(基類)
沒有繼承的類(基類),生成虛函數表和vptr指針,必須要有虛函數(用virtual修飾的函數),在編譯期間,編輯器會查看類是否有虛函數,如果有便會生成一個指針數組,用來存儲指向虛函數地址的指針,該指針數組的大小是 虛函數數量+1 最後一個元素是0 用來表示數組結束,
在類的所有構造函數的最前面,編輯器都會默認加上一些代碼用來生成vptr指針指向在編譯期間生成的指針數組,然後再指向用戶的代碼
因爲vptr指針是最先聲明對應的變量,然後再聲明定義用戶的成員變量,所以vptr的地址和類對象的首地址相同
注意:每個對象的同一個vptr指向同一個虛函數表
(2)單繼承的派生類
–如果基類有虛函數,
派生類繼承基類的一切,這時無論基類是否有虛函數,在編譯期間都會生成一個虛函數表,這個虛函數表是完全拷貝基類的虛函數表, 派生類的vptr的初始化是分步初始化的,因爲派生類對象的生成首先要調用派生類的構造函數,而在繼承中派生類構造函數調用前必須先調用基類的構造函數,所以在基類中會先初始化一次vptr指針指向基類的虛函數表,等到基類構造函數執行完畢,再執行派生類的構造函數時,會再一次初始化vptr指針將其指向派生類的虛函數表.
–如果基類沒有虛函數
如果派生類沒有虛函數,則不會創建虛函數表和vptr指針
如果派生類有虛函數,則編譯期間,會創建一個虛函數表,並在構造函數中初始化一個vptr指針指向虛函數表,不過我們需要注意的是其內存分佈:如下

1>class Child	size(12):
1>	+---
1> 0	| {vfptr}  //vptr指針
1> 4	| +--- (base class Parent3)//基類沒有虛函數  注意其偏移量
1> 4	| | p1_a    //基類的成員變量
1>	| +---
1> 8	| c    //派生類新增的成員變量
1>	+---
1>
 (3)多繼承的派生類

多繼承的虛函數表的生成和和單繼承虛函數表的生成類似,不過就是在基類的構造函數執行前多執行幾次了不同基類的構造函數, 其有以下特點(詳情見六、(4) )

在多繼承的派生類內存分佈中有如下規則
1. 無虛函數的嵌套基類對象一定排在有虛函數的嵌套基類對象的後面
2. 有虛函數的嵌套基類對象的排列順序和派生類聲明時的繼承順序相同
3. 無虛函數的嵌套基類對象的排列順序和派生類聲明時的繼承順序相同(這點我就不在寫出驗證了)
4. 派生類新增的虛函數地址添加在派生類對象首地址指向的虛函數表的後面
5. 有幾個有虛函數的基類就有幾個vptr指針

四、修正虛函數表
前面說完了虛函數表和vptr指針的生成,這裏說下虛函數地址的修改,
當派生類的虛函數 表創建完成後,編輯器會查看派生類是否有重寫基類虛函數的方法,如果重寫了,含有被重寫的虛函數的虛函數表的相應地址修改爲派生類重寫後的虛函數地址,(多繼承可能有多個vptr指針,和虛函數表)
因爲虛函數表的修正才真正實現了多態,但是我們需要注意的是,因爲vptr指針是分步進行的,所以當基類在構造函數中調用被派生類重載的函數時,無論基類指針指向的是什麼對象,最終調用的都是基類的虛函數,因爲基類的構造函數中初始化的vptr指針指向的永遠都是基類的虛函數表,(所以不會出現虛函數地址被修改的現象了)
五、使用
前面我們知道了 多繼承可能會有多個vptr指針和虛函數表,如

//派生類聲明
class Child:public Parent3, public Parent1,public Parent,public Parent2 {}
//派生類Child的內存分佈
1>class Child	size(36):
1>	+---
1> 0	| +--- (base class Parent1)
1> 0	| | {vfptr}
1> 4	| | p1_a
1>	| +---
1> 8	| +--- (base class Parent)
1> 8	| | {vfptr}
1>12	| | a
1>16	| | b
1>	| +---
1>20	| +--- (base class Parent2)
1>20	| | {vfptr}
1>24	| | p1_a
1>	| +---
1>28	| +--- (base class Parent3)
1>28	| | p1_a
1>	| +---
1>32	| c
1>	+---
1>
1>Child::$vftable@Parent1@:
1>	| &Child_meta
1>	|  0
1> 0	| &Parent1::parent1_func1
1> 1	| &Parent1::parent1_func2
1> 2	| &Child::child_func1
1> 3	| &Child::child_func2
1>
1>Child::$vftable@Parent@:
1>	| -8
1> 0	| &Parent::parent_func1
1> 1	| &Parent::parent_func2
1>
1>Child::$vftable@Parent2@:
1>	| -20
1> 0	| &Parent2::parent2_func1
1> 1	| &Parent2::parent2_func2
1>
1>Child::child_func1 this adjustor: 0
1>Child::child_func2 this adjustor: 0
1>
//主函數
	Child  c1;
	Child * c = &c1;
	Parent *p = &c1;
	Parent1 *p1 = &c1;
	Parent2  * p2 = &c1;
	Parent3  * p2 = &c1;

Vs斷點
在這裏插入圖片描述
注:Parent3是無虛函數基類,其他的都是有虛函數基類
我們注意到都是&c 但是其值是不同的 爲什麼呢 因爲在進行賦值的時候會發生地址偏移,其偏移量和上面我們通過命令行打印出列Child的內存分佈相同
比如:
p = &c 得到 p = 0x0137fc9c
p的值減去c的值爲8 正好是爲內存分佈中嵌套基類Parent對象的首地址,即是:

1> 8	| +--- (base class Parent)//偏移量是8
1> 8	| | {vfptr}

所以 此時對p的操作和對指向真正的Parent對象的指針的操作是完全相同的,(此時p指向的就是派生類中嵌套的基類對象,而且虛函數表也是完整拷貝過來後進行修改的)
這時候我們是不是更能理解多態了呢

備註:
調試VS 輸出對象內存 指令:
/d1 reportSingleClassLayout[classname] 注意:沒有空格也沒有[] [classname]代表的就是一個類名稱 輸出單個對象的內存空間
/d1 reportAllClassLayout 輸出所有對象的內存空間

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