討論靜態綁定與動態綁定,首先須要理解的是綁定,何爲綁定?函數調用與函數自己的關聯,以及成員訪問與變量內存地址間的關係,稱爲綁定。 理解了綁定後再理解靜態與動態。ios
在C++中動態綁定是經過虛函數實現的,是多態實現的具體形式。而虛函數是經過虛函數表實現的。這個表中記錄了虛函數的地址,解決繼承、覆蓋的問題,保證動態綁定時可以根據對象的實際類型調用正確的函數。這個虛函數表在什麼地方呢?C++標準規格說明書中說到,編譯器必需要保證虛函數表的指針存在於對象實例中最前面的位置(這是爲了保證正確取到虛函數的偏移量)。也就是說,咱們能夠經過對象實例的地址獲得這張虛函數表,而後能夠遍歷其中的函數指針,並調用相應的函數。c++
要想弄明白動態綁定,就必須弄懂虛函數的工做原理。C++中虛函數的實現通常是經過虛函數表實現的(C++規範中沒有規定具體用哪一種方法,但大部分的編譯器廠商都選擇此方法)。類的虛函數表是一塊連續的內存,每一個內存單元中記錄一個JMP指令的地址。編譯器會爲每一個有虛函數的類建立一個虛函數表,該虛函數表將被該類的全部對象共享。 類的每一個虛成員佔據虛函數表中的一行。若是類中有N個虛函數,那麼其虛函數表將有N*4字節的大小。後端
虛函數(virtual)是經過虛函數表來實現的,在這個表中,主要是一個類的虛函數的地址表,這張表解決了繼承、覆蓋的問題,保證其真實反映實際的函數。這樣,在有虛函數的類的實例中分配了指向這個表的指針的內存(位於對象實例的最前面),因此,當用父類的指針來操做一個子類的時候,這張虛函數表就顯得尤其重要,指明瞭實際所應調用的函數。它是如何指明的呢?後面會講到。bash
JMP指令是彙編語言中的無條件跳轉指令,無條件跳轉指令可轉到內存中任何程序段。轉移地址可在指令中給出,也能夠在寄存器中給出,或在儲存器中指出。微信
首先咱們定義一個帶有虛函數的基類函數
class Base {
public:
virtual void fun1(){
cout<<"base fun1!\n";
}
virtual void fun2(){
cout<<"base fun2!\n";
}
virtual void fun3(){
cout<<"base fun3!\n";
}
int a;
};
複製代碼
查看其內存佈局 佈局
既然虛函數表指針一般放在對象實例的最前面的位置,那麼咱們應該能夠經過代碼來訪問虛函數表,經過下面這段代碼加深對虛函數表的理解:區塊鏈
#include "stdafx.h"
#include<iostream>
using namespace std;
class Base {
public:
virtual void fun1(){
cout<<"base fun1!\n";
}
virtual void fun2(){
cout<<"base fun2!\n";
}
virtual void fun3(){
cout<<"base fun3!\n";
}
int a;
};
int _tmain(int argc, _TCHAR* argv[])
{
typedef void(*pFunc)(void);
Base b;
cout<<"虛函數表指針地址:"<<(int*)(&b)<<endl;
//對象最前面是指向虛函數表的指針,虛函數表中存放的是虛函數的地址
pFunc pfun;
pfun=(pFunc)*((int*)(*(int*)(&b))); //這裏存放的都是地址,因此才一層又一層的指針
pfun();
pfun=(pFunc)*((int*)(*(int*)(&b))+1);
pfun();
pfun=(pFunc)*((int*)(*(int*)(&b))+2);
pfun();
system("pause");
return 0;
}
複製代碼
運行結果: ui
經過這個例子,對虛函數表指針,虛函數表這些有了足夠的理解。下面再深刻一些。C++又是如何利用基類指針和虛函數來實現多態的呢?這裏,咱們就須要弄明白在繼承環境下虛函數表是如何工做的。目前只理解單繼承,至於虛繼承,多重繼承待之後再理解。 單繼承代碼以下:this
class Base {
public:
virtual void fun1(){
cout<<"base fun1!\n";
}
virtual void fun2(){
cout<<"base fun2!\n";
}
virtual void fun3(){
cout<<"base fun3!\n";
}
int a;
};
class Child:public Base
{
public:
void fun1(){
cout<<"Child fun1\n";
}
void fun2(){
cout<<"Child fun2\n";
}
virtual void fun4(){
cout<<"Child fun4\n";
}
};
複製代碼
內存佈局對比:
另外,咱們注意到,類Child和類Base中都只有一個vfptr指針,前面咱們說過,該指針指向虛函數表,咱們分別輸出類Child和類Base的vfptr:
int _tmain(int argc, _TCHAR* argv[])
{
typedef void(*pFunc)(void);
Base b;
Child c;
cout<<"Base類的虛函數表指針地址:"<<(int*)(&b)<<endl;
cout<<"Child類的虛函數表指針地址:"<<(int*)(&c)<<endl;
system("pause");
return 0;
}
複製代碼
運行結果:
能夠看到,類Child和類Base分別擁有本身的虛函數表指針vfptr和虛函數表vftable。
下面這段代碼,說明了父類和基類擁有不一樣的虛函數表,同一個類擁有相同的虛函數表,同一個類的不一樣對象的地址(存放虛函數表指針的地址)不一樣。
int _tmain(int argc, _TCHAR* argv[])
{
Base b;
Child c1,c2;
cout<<"Base類的虛函數表的地址:"<<(int*)(*(int*)(&b))<<endl;
cout<<"Child類c1的虛函數表的地址:"<<(int*)(*(int*)(&c1))<<endl; //虛函數表指針指向的地址值
cout<<"Child類c2的虛函數表的地址:"<<(int*)(*(int*)(&c2))<<endl;
system("pause");
return 0;
}
複製代碼
運行結果:
在定義該派生類對象時,先調用其基類的構造函數,而後再初始化vfptr,最後再調用派生類的構造函數( 從二進制的視野來看,所謂基類子類是一個大結構體,其中this指針開頭的四個字節存放虛函數表頭指針。執行子類的構造函數的時候,首先調用基類構造函數,this指針做爲參數,在基類構造函數中填入基類的vfptr,而後回到子類的構造函數,填入子類的vfptr,覆蓋基類填入的vfptr。如此以來完成vfptr的初始化)。也就是說,vfptr指向vftable發生在構造函數期間完成的。
動態綁定例子:
#include "stdafx.h"
#include<iostream>
using namespace std;
class Base {
public:
virtual void fun1(){
cout<<"base fun1!\n";
}
virtual void fun2(){
cout<<"base fun2!\n";
}
virtual void fun3(){
cout<<"base fun3!\n";
}
int a;
};
class Child:public Base
{
public:
void fun1(){
cout<<"Child fun1\n";
}
void fun2(){
cout<<"Child fun2\n";
}
virtual void fun4(){
cout<<"Child fun4\n";
}
};
int _tmain(int argc, _TCHAR* argv[])
{
Base* p=new Child;
p->fun1();
p->fun2();
p->fun3();
system("pause");
return 0;
}
複製代碼
運行結果:
其實,在new Child時構造了一個子類的對象,子類對象按上面所講,在構造函數期間完成虛函數表指針vfptr指向Child類的虛函數表,將這個對象的地址賦值給了Base類型的指針p,當調用p->fun1()時,發現是虛函數,調用虛函數指針查找虛函數表中對應虛函數的地址,這裏就是&Child::fun1。調用p->fun2()狀況相同。調用p->fun3()時,子類並無重寫父類虛函數,但依舊經過調用虛函數指針查找虛函數表,發現對應函數地址是&Base::fun3。因此上面的運行結果如上圖所示。
到這裏,你是否已經明白爲何指向子類實例的基類指針能夠調用子類(虛)函數?每個實例對象中都存在一個vfptr指針,編譯器會先取出vfptr的值,這個值就是虛函數表vftable的地址,再根據這個值來到vftable中調用目標函數。因此,只要vfptr不一樣,指向的虛函數表vftable就不一樣,而不一樣的虛函數表中存放着對應類的虛函數地址,這樣就實現了多態的」效果「。
關注微信公衆號,推送後端開發、區塊鏈等技術分享!
![]()