c++多型的原理 以及虛擬函式表詳解
c++中多型的原理
要實現多型,必然在背後有自己的實現機制,如果不瞭解其背後的機制,就很難對其有更深的理解。
一個多型的例子
class Person{ public: virtual void Drink() { cout << "drink water" << endl; }; }; class Children : public Person{ public: virtual void Drink() { cout << "drinking juice " << endl; } }; class Parents : public Person{ public: virtual void Drink() override{ cout << "drinking wine" << endl; }; }; int main() { Person per; Person* per1 = new Parents; Person* per2 = new Children; per1->Drink(); //drinking wine per2->Drink();//drinking juice return 0; }
通過除錯我們以看到在一個包含虛擬函式的Person類的物件per中有著一個隱藏的成員_vfptr虛擬函式指標,通過計算Person類的大小,我們發現該類的大小為4 ,說明,該指標是存在的。
我們所看到的指標為虛擬函式表指標,指向一個虛擬函式。一個含有虛擬函式的表中都含有一個虛函表,簡稱虛表,存放的是該類中虛擬函式的地址。
以上我們所看到的是基類物件,現在我們來看派生類中的物件的虛表
我們在基類Person中增加兩個函式,一個虛擬函式fun1,一個普通函式fun2.
class Person{ public: virtual void Drink() { cout << "drink water" << endl; }; virtual void fun1() { cout << "fun1" << endl; } void fun2() { cout << "fun2" << endl; } }; class Children : public Person{ public: virtual void Drink() { cout << "drinking juice " << endl; } }; class Parents : public Person{ public: virtual void Drink() override{ cout << "drinking wine" << endl; }; virtual void fun3() { cout << "fun3" << endl; } }; typedef void(*Fun) (); void PrintVtable(Fun Vtable[]){ for (int i = 0; Vtable[i] != nullptr; i++) { printf("虛擬函式 %d : 0X%x " ,i, Vtable[i]); Fun f = Vtable[i]; f(); } } int main() { Parents pare; PrintVtable((Fun*)*(int*)&pare); //虛擬函式表指標存放在物件的前4個位元組,將其轉為int*,解引用,因為其中存放的為函式指標,所以再將其強轉為函式指標 return 0; }
通過派生類物件的記憶體模型,可以發現,在派生類的物件中也有了個虛擬函式表指標,但是在監視視窗中的虛擬函式表中並沒有派生類自己的虛擬函式的地址。
- 虛擬函式表指標一般存放在物件的開頭的位置
- 虛擬函式表本質是一個存虛擬函式指標的指標陣列,這個陣列最後面放了一個nullptr。
通過虛擬函式表指標我們可以將虛擬函式表打印出來看,通過打印出來的虛擬函式表,可以看出派生類自己的虛擬函式被放在最後,只不過在監視視窗沒有顯示出來。
- 派生類中虛表的佈局:首先存放的是拷貝自基類的虛表中的虛擬函式,如果對其中的虛擬函式進行了重寫,就用重寫的將原來基類的虛擬函式進行覆蓋,最後再按派生類中虛擬函式宣告的順序,將派生類的虛擬函式的地址增加到最後。
實際上的多型就是不同的物件,在呼叫時查詢其虛擬函式表,找到要呼叫的函式,因為在派生類的虛擬函式表中已將完成了重寫,所以儘管呼叫的是同一個函式,但完成的卻是不同的動作,一個行為有多種狀態。
動態繫結與靜態繫結
- 靜態繫結:靜態繫結又叫早繫結,是在編譯期間,編譯器已經確定好了函式的呼叫情況,在編譯階段已經確定好了程式的行為。
函式的過載實際上一種靜態的多型。 - 動態繫結:動態繫結並不是在編譯階段確定函式的呼叫的,而是在執行階段來確定的。
多型的呼叫實質上就是動態的繫結,即函式的呼叫並不是在編譯期間確定的,而是在執行期間根據函式的呼叫情況在相應的物件中查詢並進行呼叫。
通過彙編程式碼我們可以看到多型的函式呼叫是在執行時去確定的。
先在呼叫第二個虛擬函式fun1
單繼承下的虛擬函式表
- 在單繼承的關係下,派生類的虛擬函式表中先存放的是拷貝自基類的虛擬函式表,並用重寫的函式將基類的相應的函式覆蓋。在表的後面,按派生類自己的宣告順序,加入自己的虛擬函式地址。
- 在vs的編譯器下監視的視窗中在虛擬函式表中無法看到派生類自己的虛擬函式,可以通過列印虛擬函式表看到
- 虛擬函式表存放在物件的前四個位元組,以nullptr結尾,相當於一個函式指標陣列 (前面列印過單繼承關係下的虛擬函式表)
多繼承下的虛擬函式表
- 在多繼承關係下,派生類物件中有多個虛擬函式指標,指向多個虛擬函式表,每個基類都有一個,按繼承的順序依次存放虛擬函式指標。而派生類自己的虛擬函式存放在第一個虛擬函式表的後面。
class Mother{
public:
virtual void fun1(){
cout << "Mother :: fun1" << endl;
}
};
class Father{
public:
virtual void fun2() {
cout << "Father :: fun2" << endl;
}
};
class Son : public Mother ,public Father{
public:
virtual void fun1() {
cout << "Son :: fun1 " << endl;
}
virtual void fun2(){
cout << "Son :: fun2 " << endl;
}
virtual void fun3(){
cout << "Son :: fun3 " << endl;
}
};
int main()
{
Son son;
PrintVtable((Fun*)*(int*)&son);
PrintVtable((Fun*)*(int*)((char*)&son + sizeof(Mother)));
return 0;
}
虛擬函式表存放在哪裡
虛擬函式指標是存放在物件中,所以虛擬函式指標的位置是跟著物件的位置走的
- 物件在棧上被建立,虛擬函式指標存在於棧上
- 物件被建立在堆上,虛擬函式指標就存在於堆上
虛擬函式表的位置是不確定的所以我們自己手動的測試,通過手動的測試,可以發現通過打印出的虛擬函式表的地址,與程式碼區的地址極為相似,所以大致可以確定虛擬函式表是存放在程式碼區的。
int a = 10;
int main()
{
Son son;
Son* s1 = new Son;
/*PrintVtable((Fun*)*(int*)&son);
PrintVtable((Fun*)*(int*)((char*)&son + sizeof(Mother)));*/
const int b = 20;
static int c = 30;
char* pch = "hello world";
printf("虛擬函式表的地址: %x \n", (Fun*)*(int*)&son);
printf("a : %x \n", &a);
printf("b : %x \n", &b);
printf("堆區:s1: %x \n", s1);
printf("棧區:son: %x \n", &son);
printf("靜態區: c: %x \n", &c);
printf("程式碼段: pch : %x \n", pch);
return 0;
}