日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

C++中多态(二)

發布時間:2024/4/11 c/c++ 20 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C++中多态(二) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

C++中多態(二)

文章目錄

  • C++中多態(二)
    • 一、多態的虛函數相關概念
    • 二、多態的原理
    • 三、多繼承中的虛表
    • 四、常見問題

一、多態的虛函數相關概念

  • 1.虛函數表(和繼承的虛基表要注意區分)
// 這里??家坏拦P試題:sizeof(Base)是多少? class Base { public:virtual void Func1(){cout << "Func1()" << endl; } private:int _b = 1; };

答案是8bytes

  • 通過觀察測試我們發現b對象是8bytes,除了_b成員,還多一個__vfptr放在對象的前面(注意有些平臺可能會放到對象的最后面,這個跟平臺有關)
  • 對象中的這個指針我們叫做虛函數表指針(v代表virtual,f代表function)。
  • 一個含有虛函數的類中都至少都有一個虛函數表指針,因為虛函數的地址要被放到虛函數表中,虛函數表也簡稱虛表
  • 即如果某類中包含虛函數,那么編譯器會給該類的對象多增加4個字節,其內容是在該對象的構造函數中完成填充的,如果類中沒有顯示定義構造函數,那么編譯器會默認生成一個構造函數,完成給該類對象前4個字節的內容填充。
  • 2.派生類中虛表的生成:
class Base { public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;} private:int _b = 1; };class Derive : public Base { public:virtual void Func1(){cout << "Derive::Func1()" << endl;} private:int _d = 2; };int main() {Base b;Derive d;return 0; }
  • 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; }

通過上面的代碼,很容易發現:

  • 當傳入Person類型的Mike時,p->BuyTicket在mike的虛表中找到虛函數是Person::BuyTicket()
  • 當傳入Student類型的Johnson時,p->BuyTicket在johson的虛表中找到虛函數是Student::BuyTicket()
  • 這樣就實現了當不同的對象去調用完成同一功能函數時,展現不同的結果。
  • 這樣我們就得深思多態的兩個條件:一是派生類中實現基類虛函數的重寫,二是用基類的引用或指針來調用虛函數
  • 再通過下面的匯編代碼分析,看出滿足多態以后的函數調用,不是在編譯時確定的,是運行起來以后到對象的中取找的。不滿足多態的函數調用時編譯時確認好的
  • void Func(Person* p) {p->BuyTicket(); }int main() {Person mike;Func(&mike);mike.BuyTicket();return 0; }// 以下匯編代碼中跟你這個問題不相關的都被去掉了 void Func(Person* p) {...p->BuyTicket();// p中存的是mike對象的指針,將p移動到eax中001940DE mov eax,dword ptr [p]// [eax]就是取eax值指向的內容,這里相當于把mike對象頭4個字節(虛表指針)移動到 了edx001940E1 mov edx,dword ptr [eax]// [edx]就是取edx值指向的內容,這里相當于把虛表中的頭4字節存的虛函數指針移動到了eax00B823EE mov eax,dword ptr [edx]// call eax中存虛函數的指針。這里可以看出滿足多態的調用,不是在編譯時確定的,//是運行起來以后到對象的中取找的。001940EA call eax001940EC cmp esi,esp }int main() {...// 首先BuyTicket雖然是虛函數,但是mike是對象,不滿足多態的條件,//所以這里是普通函數的調用轉換成地址時,是在編譯時已經從符號表確認了函數的地址,直接call 地址mike.BuyTicket();00195182 lea ecx,[mike]00195185 call Person::BuyTicket (01914F6h)... }

    那么再看下面的代碼:

    #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 時,編譯器不會在編譯時就確定對象要調用的函數的地址,而是在運行時再去確定要調用的函數的地址,這就是晚綁定,也叫做動態綁定。

  • 第一:編譯器在發現Father 類中有虛函數時,會自動為每個含有虛函數的類生成一份虛函數表,也叫做虛表,該表是一個一維數組,虛表里保存了虛函數的入口地址。
  • 第二:編譯器會在每個對象的前四個字節中保存一個虛表指針,即(vptr),指向對象所屬類的虛表。在程序運行時的合適時機,根據對象的類型去初始化vptr,從而讓vptr指向正確的虛表,從而在調用虛函數時,能找到正確的函數。
  • 第三:所謂的合適時機,在派生類定義對象時,程序運行會自動調用構造函數,在構造函數中創建虛表并對虛表初始化。在構造子類對象時,會先調用父類的構造函數,此時,編譯器只“看到了”父類,并為父類對象初始化虛表指針,令它指向父類的虛表;當調用子類的構造函數時,為子類對象初始化虛表指針,令它指向子類的虛表。
  • 最后再舉一例說明多態原理:

    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.多繼承的虛表
    class Base1 { public:virtual void func1() {cout << "Base1::func1" << endl;}virtual void func2() {cout << "Base1::func2" << endl;} private:int b1; };class Base2 { public:virtual void func1() {cout << "Base2::func1" << endl;}virtual void func2() {cout << "Base2::func2" << endl;} private:int b2; };class Derive : public Base1, public Base2 { public:virtual void func1() {cout << "Derive::func1" << endl;}virtual void func3() {cout << "Derive::func3" << endl;} private:int d1; };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() {Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));PrintVTable(vTableb2);return 0; }

    觀察下圖發現:多繼承派生類中自己的不是重寫基類的虛函數都放在第一個繼承基類部分的虛函數表中

    • 2.菱形繼承和菱形虛擬繼承的虛表

    C++虛函數表解析
    C++對象內存布局

    總結:

  • 靜態綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態多態,比如:函數重載
  • 動態綁定又稱后期綁定(晚綁定),是在程序運行期間,根據具體拿到的類型確定程序的具體行為,調用具體的函數,也稱為動態多態。
  • c++的多態性用一句話概括就是:在基類的函數前加上virtual關鍵字,在派生類中重寫該函數,運行時將會根據對象的實際類型來調用相應的函數,如果對象類型是派生類,就調用派生類的函數,如果對象類型是基類,就調用基類的函數。
  • 四、常見問題

    • 1.inline函數可以是虛函數嗎?

    答案是inline函數不能是虛函數;原因是內聯函數會在編譯階段把函數內容給展開,不需要通過虛函數表進行調用。并且在運行期間內聯函數就沒有函數地址,也就無法把函數地址放到虛函數表中。

    • 2.靜態成員函數可以是虛函數嗎?

    答案是靜態成員函數不可以是虛函數;靜態成員函數不屬于某個具體的對象,沒有this指針,靜態成員函數的調用方式可以用類名::靜態成員函數進行調用,從而也就導致無法訪問虛函數表,也就無法把靜態成員函數放到虛函數表中。

    • 3.構造函數可以是虛函數嗎?

    答案是構造函數不可以是虛函數;因為如果類中有虛函數,那么編譯器就會默認給4個字節用來存放虛函數指針,其內容是在構造函數初始化階段完成填充的,如果把構造函數設為虛函數,就須要通過 虛函數指針來完成調用構造函數,但是對象還沒有實例化,也就是內存空間還沒有,怎么找虛函數指針呢?所以構造函數不能是虛函數。(當然構造函數也不能被const修飾)

    • 4.析構函數可以是虛函數嗎?

    答案是析構函數可以是虛函數;在多態的時候,比如基類的指針指向派生類的對象,如果刪除該指針delete []p,就會調用該指針指向的派生類析構函數,而派生類的析構函數又自動調用基類的析構函數,這樣整個派生類的對象完全被釋放。如果析構函數不被聲明成虛函數,會造成派生類對象析構不完全。所以析構函數聲明為虛函數的十分必要的。(如果派生類中涉及資源管理問題,最好把基類析構設成虛函數)

    • 5.對象訪問普通函數快還是訪問虛函數快?

    答案:首先如果是普通對象,是一樣快的。如果是指針對象或者是引用對象,則調用的普通函數快,因為構成多態,運行時調用虛函數需要到虛函數表中去查找

    總結

    以上是生活随笔為你收集整理的C++中多态(二)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。