C++中多态(二)
C++中多態(二)
文章目錄
- C++中多態(二)
- 一、多態的虛函數相關概念
- 二、多態的原理
- 三、多繼承中的虛表
- 四、常見問題
一、多態的虛函數相關概念
- 1.虛函數表(和繼承的虛基表要注意區分)
答案是8bytes
- 通過觀察測試我們發現b對象是8bytes,除了_b成員,還多一個__vfptr放在對象的前面(注意有些平臺可能會放到對象的最后面,這個跟平臺有關)
- 對象中的這個指針我們叫做虛函數表指針(v代表virtual,f代表function)。
- 一個含有虛函數的類中都至少都有一個虛函數表指針,因為虛函數的地址要被放到虛函數表中,虛函數表也簡稱虛表
- 即如果某類中包含虛函數,那么編譯器會給該類的對象多增加4個字節,其內容是在該對象的構造函數中完成填充的,如果類中沒有顯示定義構造函數,那么編譯器會默認生成一個構造函數,完成給該類對象前4個字節的內容填充。
- 2.派生類中虛表的生成:
- a.同一個類的不同對象共用同一張虛表
- b.派生類有自己的虛表,不再和父類公用同一張虛表,如果派生類沒有重寫父類的虛函數,那么此時派生類虛表的內容和基類虛表的內容是一致的,即派生類的虛表是將父類的虛表進行了一份拷貝,放到派生類的虛表中。
- c.如果派生類重寫了父類的虛函數,就用派生類自己的虛函數地址覆蓋虛表中相同偏移量位置處的基類虛函數地址。
- d.派生類虛表的構造過程是按照虛函數在類中的聲明次序一次增加到虛表中的。
- e.派生類的虛表是由父類的虛表內容繼承下來和派生類自己虛函數成員組成。
- f.虛表的本質是存放虛函數地址的指針數組,數組的最后放nullptr。
- g.注意??:虛表存的是虛函數指針,不是存放的虛函數,虛函數和普通函數一樣,存放在代碼段,只是它的地址存放到虛函數表當中了而已,并且,類對象中存的是指向虛表的指針。
二、多態的原理
class Person { public:virtual void BuyTicket() { cout << "買票-全價" << endl; } };class Student : public Person { public:virtual void BuyTicket() { cout << "買票-半價" << endl; } };void Func(Person& p) {p.BuyTicket(); }int main() {Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0; }通過上面的代碼,很容易發現:
那么再看下面的代碼:
#include <iostream> using namespace std;class Father { public:void fun(){cout << "I am father!" << endl;}};class Son:public Father { public:void fun(){cout << "I am son!" << endl;} };int main() {Son son;Father *Pfather= &son;Pfather->fun();system("pause");return 0;}- 它不是真正的多態,不滿足多態的條件,打印結果為I am father!。
- 我相信很多人可能會誤將它和C++的多態搞混,認為Son的對象son應該調用Son的成員函數,但事實卻不是如此,這是為什么呢?
從編譯器的角度看:
- C++編譯器在編譯時,會確定每個對象調用函數(非虛函數)的地址,這叫做早期綁定(也叫做靜態綁定)。
- 當我們定義了派生類的對象,并取它的地址賦值給基類的指針,這時編譯器會自動為派生類對象進行類型轉換,將派生類對象轉換為基類對象,站在內存的角度來看,訪問的就是基類的成員。
這是因為派生類的對象的對象模型如下:
- 基類的成員屬于派生類成員的一部分,那么父類和子類的成員變量如何初始化呢?
- 我們定義了Son的對象,編譯器會自動調用Son的構造函數。
- 在執行派生類的構造函數體之前,編譯器會先調用父類的構造函數,先為父類的成員變量初始化,再為派生類的對象初始化,最后執行派生類構造函數的函數體。
- 當我們將Son 類對象轉化為父類Father 類型時,該對象就被認為是派生類對象模型的上半部分,將該對象當成父類對象執行相應的代碼,自然就調用父類的函數了。
真正多態的例子:
#include <iostream> using namespace std;class Father { public:virtual void fun(){cout << "I am father!" << endl;}};class Son:public Father { public:void fun(){cout << "I am son!" << endl;} };int main() {Son son;Father *Pfather= &son;Father& father = son;Pfather->fun();father.fun();Father fath;Father* Pfath = &fath;Pfath->fun();system("pause");return 0;}
在這個實現機制下,發生了什么?
- 當我們將函數聲明為 virtual 時,編譯器不會在編譯時就確定對象要調用的函數的地址,而是在運行時再去確定要調用的函數的地址,這就是晚綁定,也叫做動態綁定。
最后再舉一例說明多態原理:
class Base { public :virtual void func1() { cout<<"Base::func1" <<endl;}virtual void func2() {cout<<"Base::func2" <<endl;} private :int a; };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 b; };typedef void(*VFPTR) (); void PrintVTable(VFPTR vTable[]) {// 依次取虛表中的虛函數指針打印并調用。調用就可以看出存的是哪個函數cout << " 虛表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d個虛函數地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl; }int main() {Base b;Derive d; // 思路:取出b、d對象的頭4bytes,就是虛表的指針,前面我們說了虛函數表本質是 //一個存虛函數指針的指針數組,這個數組最后面放了一個nullptr // 1.先取b的地址,強轉成一個int*的指針 // 2.再解引用取值,就取到了b對象頭4bytes的值,這個值就是指向虛表的指針 // 3.再強轉成VFPTR*,因為虛表就是一個存VFPTR類型(虛函數指針類型)的數組。 // 4.虛表指針傳遞給PrintVTable進行打印虛表 // 5.需要說明的是這個打印虛表的代碼經常會崩潰,因為編譯器有時對虛表的處理不干凈, //虛表最后面沒有放nullptr,導致越界,這是編譯器的問題。我們只需要點 //目錄欄的-生成-清理解決方案,再編譯就好了。VFPTR* vTableb = (VFPTR*)(*(int*)&b);PrintVTable(vTableb);VFPTR* vTabled = (VFPTR*)(*(int*)&d);PrintVTable(vTabled);return 0; }三、多繼承中的虛表
- 1.多繼承的虛表
觀察下圖發現:多繼承派生類中自己的不是重寫基類的虛函數都放在第一個繼承基類部分的虛函數表中
- 2.菱形繼承和菱形虛擬繼承的虛表
C++虛函數表解析
C++對象內存布局
總結:
四、常見問題
- 1.inline函數可以是虛函數嗎?
答案是inline函數不能是虛函數;原因是內聯函數會在編譯階段把函數內容給展開,不需要通過虛函數表進行調用。并且在運行期間內聯函數就沒有函數地址,也就無法把函數地址放到虛函數表中。
- 2.靜態成員函數可以是虛函數嗎?
答案是靜態成員函數不可以是虛函數;靜態成員函數不屬于某個具體的對象,沒有this指針,靜態成員函數的調用方式可以用類名::靜態成員函數進行調用,從而也就導致無法訪問虛函數表,也就無法把靜態成員函數放到虛函數表中。
- 3.構造函數可以是虛函數嗎?
答案是構造函數不可以是虛函數;因為如果類中有虛函數,那么編譯器就會默認給4個字節用來存放虛函數指針,其內容是在構造函數初始化階段完成填充的,如果把構造函數設為虛函數,就須要通過 虛函數指針來完成調用構造函數,但是對象還沒有實例化,也就是內存空間還沒有,怎么找虛函數指針呢?所以構造函數不能是虛函數。(當然構造函數也不能被const修飾)
- 4.析構函數可以是虛函數嗎?
答案是析構函數可以是虛函數;在多態的時候,比如基類的指針指向派生類的對象,如果刪除該指針delete []p,就會調用該指針指向的派生類析構函數,而派生類的析構函數又自動調用基類的析構函數,這樣整個派生類的對象完全被釋放。如果析構函數不被聲明成虛函數,會造成派生類對象析構不完全。所以析構函數聲明為虛函數的十分必要的。(如果派生類中涉及資源管理問題,最好把基類析構設成虛函數)
- 5.對象訪問普通函數快還是訪問虛函數快?
答案:首先如果是普通對象,是一樣快的。如果是指針對象或者是引用對象,則調用的普通函數快,因為構成多態,運行時調用虛函數需要到虛函數表中去查找
總結
- 上一篇: C++中的多态(一)
- 下一篇: C++中的模版