在上篇博文我們知道虛函數存在虛表,虛表存了一個虛函數的指針,是以sizeof()這個類中包含虛函數則會包含指針的大小,本篇文章我們将通過原理了解多态。
[ C++ ] 抽象類 虛函數 虛函數表 -- C++多态(1)
1、多态原理
下面這段代碼中,這個Buy函數傳Person的調用的Person::BuyTicket(),傳Student調用的是Student::BuyTicket.這樣構成了多态,是以多态調用實作,是依靠運作時,去指向對象的虛表中查調用的函數位址。
class Person
{
public:
Person(const char* name = "張三")
:_name(name)
{}
virtual void BuyTicket(){
cout << _name << "購票,需要排隊,每人 100 ¥" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(const char* name)
:_name(name)
{}
virtual void BuyTicket(){
cout << _name << "購票,需要排隊,每人 50 ¥" << endl;
}
private:
string _name;
};
void Buy(Person* p){
p->BuyTicket();
}
int main(){
Person p("張三");
Buy(&p);
Student st("張同學");
Buy(&st);
return 0;
}

我們也可以通過監視視窗進行檢視
1.觀察監視視窗時我們看到,Person指向p對象時,p->BuyTicket在p的虛表中找到虛函數是Person::Ticket。
2.觀察監視視窗時我們看到,Student指向對象st時,st->BugTicket在st的虛表中找到虛函數是Student::Ticket。
3.這樣就實作除了不同對象去完成同一行為時,展現出不同的形态。
4.反過來思考我們要達到多态,有兩個條件:1、一個是虛函數覆寫,一個是對象的指針或引用調用虛函數。
5.在通過彙編代碼分析,看出滿足多态以後的函數調用,不是在編譯時确定的,而是運作起來以後到對象中去取的。不滿足多态的函數調用時編譯時是确認好的。
多态調用:運作時決議--運作時确定調用函數的位址
普通函數:編譯時決議--編譯時确認調用函數的位址
2、動态綁定與靜态綁定
1.靜态綁定又稱為前期綁定(早綁定),在程式編譯期間确定了程式的行為,也成為了靜态多态,比如:函數重載。
2.動态綁定又稱為後期綁定(晚綁定),是在程式運作期間,根據具體拿到的類型确定程式的具體行為,調用具體的和拿書,也成為了動态多态。
3.上圖買票的彙編代碼很好的解釋了什麼是靜态綁定(編譯時)和動态(運作時)綁定。
3、單繼承和多繼承關系的虛函數表
3.1單繼承中的虛函數表
class Base
{
public:
virtual void Func1(){
cout << "Base::Func1()" << endl;
}
virtual void Func2(){
cout << "Base::Func2()" << endl;
}
private:
int _b = 1;
};
class Derive :public Base
{
public:
virtual void Func1(){
cout << "Derive::Func1()" << endl;
}
virtual void Func3(){
cout << "Derive::Func3()" << endl;
}
virtual void Func4(){
cout << "Derive::Func4()" << endl;
}
private:
int _d = 2;
};
int main(){
Base b;
Derive d;
return 0;
}
在監視視窗我們發現Derive中看不見Fun3和Fun4,這裡是編譯器的監視視窗故意隐藏了這兩個函數,也可以認為是VS的一個Bug。那麼我們如何檢視d的虛表呢?我們将使用代碼列印出虛表中的函數。
列印的思路:取出b,d對象的頭4bytes,就是虛表的指針,前面我們說了虛函數表本質是一個存虛函數指針的指針數組,這個數組最後面放了一個nullptr
1.先取b的位址,強轉成一個int*的指針。
2.再解引用取值,就取到了b對象頭4bytes的值,這個值就是指向虛表的指針。
3.再強轉成VFPTR*,因為虛表就是一個存VFPTR類型(虛函數指針類型)的數組。
4.虛表指針傳遞給PrintVTable進行列印虛表。
5.需要說明的是這個列印虛表的代碼經常會崩潰,因為編譯器有時對虛表的處理不幹淨,虛表最後面沒有放nullptr,導緻越界,這是編譯器的問題。我們隻需要點目錄欄的-生成-清了解決方案,再編譯就好了。
typedef void(*V_FUNC)();
//
//// 列印虛表
////void PrintVFTable(V_FUNC a[])
void PrintVFTable(V_FUNC a[]){
printf("vfptr:%p\n", a);
for (size_t i = 0; a[i] != nullptr; ++i)
{
printf("[%d]:%p->", i, a[i]);
V_FUNC f = a[i];
f();
}
}
int main(){
Base b;
Derive d;
PrintVFTable((V_FUNC*)(*((int*)&b)));
PrintVFTable((V_FUNC*)(*((int*)&d)));
return 0;
}
3.2 多繼承中的虛函數表
以下有一段多繼承中的虛函數表代碼,我們通過列印出虛表的函數觀察其中的奧妙
class Base1
{
public:
virtual void Func1(){
cout << "Base1::Func1()" << endl;
}
virtual void Func2(){
cout << "Base1::Func2()" << endl;
}
private:
int _b1 = 1;
};
class Base2
{
public:
virtual void Func1(){
cout << "Base2::Func1()" << endl;
}
virtual void Func2(){
cout << "Base2::Func2()" << endl;
}
private:
int _b2 = 1;
};
class Derive :public Base1,public Base2
{
public:
virtual void Func1(){
cout << "Derive::Func1()" << endl;
}
virtual void Func3(){
cout << "Derive::Func3()" << endl;
}
private:
int _d1 = 2;
};
typedef void(*V_FUNC)();
//
//// 列印虛表
////void PrintVFTable(V_FUNC a[])
void PrintVFTable(V_FUNC a[]){
printf("vfptr:%p\n", a);
for (size_t i = 0; a[i] != nullptr; ++i)
{
printf("[%d]:%p->", i, a[i]);
V_FUNC f = a[i];
f();
}
cout << endl;
}
int main(){
Derive d;
PrintVFTable((V_FUNC*)(*((int*)&d)));
//PrintVFTable((V_FUNC*)(*((int*)&d+sizeof(Base1))));
return 0;
}
通過列印虛表我們可以看到多繼承派生類的為重寫的虛函數放在第一個繼承基類部分的虛函數表中