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

歡迎訪問 生活随笔!

生活随笔

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

c/c++

C++ 多态实现机制

發布時間:2025/4/5 c/c++ 47 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C++ 多态实现机制 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本篇從 C++ 初學者遇到的一個有趣的問題開始。

有趣的問題

考慮下面的 C++ 程序:

class A {void func(){} };class B:public A {void func(){} };int main(void) {cout << sizeof(A) << " " << sizeof(B) << endl;return 0; }

輸出結果是:1 1

再考慮下面很相似的程序:

class A {virtual void funcA(){} };class B:public A {virtual void funcB(){} };int main(void) {cout << sizeof(A) << " " << sizeof(B) << endl;return 0; }

輸出結果是:4 4

再來考慮下面的形似的程序:

class A {virtual void funcA(){} };class B:virtual public A {virtual void funcB(){} };int main(void) {cout << sizeof(A) << " " << sizeof(B) << endl;return 0; }

輸出結果是:4 12

對于第一種情況,沒有出現虛函數,也無任何成員變量,因此是一個空類,空類理論上可以進行實例化,每個實例在內存中都有獨一無二的地址來標明,所以會占用 1B 的空間,無可厚非。

但第二種情況和第三種情況加入了虛函數(virtual function),而且在第三種情況當中,引入了虛基類(virtual base class)的概念,所得到的結果大相徑庭,這是 C++ 引入了 virtual function 和 virtual base class,即多態,更形象的解釋是「以一個 public base class 的指針或者引用,尋址出一個 derived?class object」,但多態帶了一定空間上的開銷,在效率上也有折損。

其實, 多態機制可以歸結為下面三這句話:

  • 一般而言, 我們無法知道指針 ptr 所指的對象的真正類型. 但經由 ptr 總是可以存取到對象的 virtual table.
  • 虛函數 fn() 總是放在 virtual table 中的固定位置, 用一個固定的索引值就可以 fetch 到.
  • 唯一一個執行期需要知道的是 ptr 所指的對象.

下面是 C++ 多態機制實現詳解.

從最簡單的對象模塊開始

最為簡單的對象模型:

靜態/非靜態 成員函數 和 靜態/非靜態 成員變量 的地址都存儲在一個表當中,通過表內存儲的地址指向相應的部分。這樣的設計簡易,便于理解,類的實例只需要維護這張表就好了,賠上的是空間和執行效率:

空間上:沒必要為每一個實例都存儲靜態成員變量和成員函數

效率上:每次執行實例的一個成員函數都要在表內進行搜索

這是最初的假設,實際的實現肯定沒有那么簡單,下面是將變量和函數分割存儲的模型(表格驅動對象模型):

簡易對象模型經改良后可以的得到這種。sizeof(A) 的結果是 8。

為支撐 virtual function ,引入了現在的 C++ 對象模型:

非靜態成員變量同指向虛擬函數表的指針(vptr),靜態成員變量/函數,非靜態成員函數分離存儲。類的每一個實例都存有 vptr 和 非靜態成員變量,他們獨立擁有這些數據,并不和其他的實例共享。這時候,回到第二種情況,class A 和 繼承自 A 的 class B 都擁有虛函數,因此都會有一個 vptr,因此 sizeof 運算得到的結果都為 4.然而,如果往里面添加一個非靜態 int 型變量,那么相應可以得到 8B 的大小;但往里面添加靜態 int 型變量,大小卻沒有改變。

單一繼承

下面是單一繼承里經常看到的一個程序:

class A { public:int a;void foo(){}virtual void funcA(){}virtual void func(){cout << "class A's func." << endl;} };classB : public A { public:int b;void foo(){}virtual void funcB(){}virtual void func(){cout << "class B's func." << endl;} };int main(void) {A *pa = newB;pa->func(); }

輸出結果是:class B'sfunc.

多態就是多種狀態,一個事物可能有多種表現形式,譬如動物,有十二生肖甚至更多的表現形式。當基類里實現了某個虛函數,但派生類沒有實現,那么類 B 的實例里的虛函數表中放置的就是 &A::func。此外,派生類也實現了虛函數,那么類 B 實例里的虛函數表中放置的就是 B::func。A *pa = new B; 因為 B 實現了 func,那么它被放入 A 實例的虛擬函數表中,從而代替 A 實例本身的虛擬函數。pa->func(); 調用的結果就不稀奇了,這是虛函數機制帶來的。

class A 和 class B 的內存布局和 vptr 可能是下面的樣子:

  • ----------
  • |?? int a |
  • ----------
  • |??? vptr | -------->|????? &A::funcA()
  • ----------???????????? -------------------------------------------------
  • ? ? ? ? ? ? ? ? ? ? ? ? ? |????? &A::func()
  • ? ? ? ? ? ? ? ? ? ? ? ? ?-------------------------------------------------
  • ----------
  • |?? int a |
  • ----------
  • |??? vptr | -------->|???? &A::funcA() 依舊是 A 的虛函數
  • ----------???????????? -------------------------------------------------
  • |?? int b | ? ? ? ? ? ? ?|???? &B::func()?A::func()
  • ----------???????????? -------------------------------------------------
  • ? ? ? ? ? ? ? ? ? ? ? ? ? |???? &B::funcB()
  • ? ? ? ? ? ? ? ? ? ? ? ? ? -------------------------------------------------
  • 倘若 虛函數 以外的就沒有「多態」效果了,除非進行強制類型轉換:

    • pa->a;????????? //???? 成功,因為 pa 的類型就是 A
    • pa->b;????????? //???? 失敗,因為 B::b
    • pa->funcB();? //???? 失敗,因為B::funcB() 不是虛函數
    • pa->funcA();? //???? 成功,因為A::funcA()

    總結一下:

    • 當引入虛函數的時候,會添加 vptr 和 其指向的一個虛擬函數表從而增加額外的空間,這些信息在編譯期間就已經確定,而且在執行期不會插足修改任何內容。
    • 在類的構造和析構函數當中添加對應的代碼,從而能夠為 vptr 設定初值或者調整 vptr,這些動作由編譯器完成,class 會產生膨脹。
    • 當出現繼承關系時,虛擬函數表可能需要改寫,即當用基類的指針指向一個派生類的實體地址,然后通過這個指針來調用虛函數。這里要分兩種情況,當派生類已經改寫同名虛函數時,那么此時調用的結果是派生類的實現;而如果派生類沒有實現,那么調用依然是基類的虛函數實現,而且僅僅在多態僅僅在虛函數上表現。
    • 多態僅僅在虛函數上表現,意即倘若同樣用基類的指針指向一個派生類的實體地址,那么這個指針將不能訪問和調用派生類的成員變量和成員函數。
    • 所謂執行期確定的東西,就是基類指針所指向的實體地址是什么類型了,這是唯一執行期確定的。以上是單一繼承的情況,在多重繼承的情況會更為復雜。

    多重繼承

    下面是少有看到的程序代碼:

    class A { public:virtual ~A(){cout << "A destruction" << endl;}int a;void fooA(){}virtual void func(){cout << "A func." << endl;};virtual void funcA(){cout << "funcA." << endl;} };class B { public:virtual ~B(){cout << "B destruction" << endl;}int b;void fooB(){}virtual void func(){cout << "B func." << endl;};virtual void funcB(){cout << "funcB." << endl;} };class C : public A,public B { public:virtual ~C(){cout << "C destruction" << endl;}int c;void fooC(){}virtual void func(){cout << "C func." << endl;};virtual void funcC(){cout << "funcC." << endl;} };int main(void) { return 0; }

    當用基類的指針指向一個派生類的實體地址,基類有兩種情況,一種是 class A 和 class B,如果是 A,問題容易解決,幾乎和上面單一繼承情況類似;但倘若是 B,要做地址上的轉換,情況會比前者復雜。先展現class A,B,C 的內存布局和 vptr:

  • ----------
  • |?? int a |
  • ----------
  • |??? vptr | -------->|????? &A::~A()
  • ----------???????????? -------------------------------------------------
  • ??????????????????????????? |????? &A::func()
  • ??????????????????????????? -------------------------------------------------
  • ??????????????????????????? |????? &A::funcA()
  • ??????????????????????????? -------------------------------------------------
  • ----------
  • |?? int b |
  • ----------
  • |??? vptr | -------->|???? &B::~B()
  • ----------???????????? -------------------------------------------------
  • ??????????????????????????? |???? &B::func()
  • ??????????????????????????? -------------------------------------------------
  • ??????????????????????????? |???? &B::funcB()
  • ??????????????????????????? --------------------------------------------------
  • ?

  • ??????????????????????????? |????? &C::~C()?&A::~A()
  • ----------???????????? -------------------------------------------------
  • |?? int a |?????????????? |????? &C::func()?&A::func()
  • ----------???????????? -------------------------------------------------
  • ----------???????????? |????? &C::funcC()
  • |??? vptr | -------->-------------------------------------------------
  • ----------???????????? |????? &A::funcA()
  • ----------???????????? -------------------------------------------------
  • |?? int b |?????????????? |????? &B::funcB() 跳
  • ----------???????????? -------------------------------------------------
  • ----------
  • |??? vptr | -------->|???? &C::~C()?&B::~B() 跳
  • ----------???????????? -------------------------------------------------
  • |?? int c |?????????????? |???? &C::func()?&B::func()?跳
  • ----------???????????? -------------------------------------------------
  • ?????????????????????????? |???? &B::funcB()
  • ??????????????????????????? --------------------------------------------------
  • 多重繼承中,會有保留兩個虛擬函數表,一個是與 A 共享的,一個是與 B 相關的,他們都在原有的基礎上進行了修改:

    對于 A 的虛擬函數表:

    • 覆蓋派生類實現的同名虛函數,并用派生類實現的析構函數覆蓋原有虛函數
    • 添加了派生類獨有的虛函數
    • 添加了右端父類即 B 的獨有虛函數,需跳轉

    對于 B 的虛擬函數表:

    • 覆蓋派生類實現的同名虛函數,并用派生類實現的析構函數覆蓋原有虛函數,但需跳轉
  • int main(void)
  • {
  • ???? A *pa = new C;
  • ???? B *pb = new C;
  • ???? C *pc = new C;
  • ???? pa->func();
  • ???? pb->func();
  • ???? pc->funcC();
  • ???? delete pb;
  • ???? delete pa;
  • ???? delete pc;
  • }
  • 輸出結果是:

    C func.
    C func.
    funcC.
    C destruction
    B destruction
    A destruction
    C destruction
    B destruction
    A destruction
    C destruction
    B destruction
    A destruction

    7 行和 8 行的行為有很大的區別,7 行的調用和上面的單一繼承的情況類似,不贅述。8 行的 pb->func(); 中,pb 所指向的是上圖第 9 行的位置,編譯器已在內部做了轉換,也就是 pa 和 pb 所指的位置不一樣,pa 指向的是上圖第 3 行的位置。接著需要注意的是,pb->func(); 調用時,在虛擬函數表中找到的地址需要再進行一次跳轉,目標是 A 的虛擬函數表中的 &C::func(),然后才真正執行此函數。所以,上面的情況作了指針的調整。

    那什么時候會出現跳,常見的有兩種情況:

  • 右端基類,對應上面的具體是 B,調用派生類虛擬函數,比如 pb->~C() 和 pb->func()
  • 派生類調用右端基類的虛擬函數,比如 pc->funcB()
  • 所以 delete pa; 和 delete pa; 的操作是不一樣的,pb->funcB(); 和 pc->funcB(); 也不一樣。

    C++ 為實現多態引入虛函數機制,帶來了空間和執行上的折損。

    單一/多重繼承的構造和析構

    單一繼承中,構造函數調用順序是從上到下(單一繼承),從左到右(多重繼承),析構函數調用順序反過來。在上一段程序中,

  • ???? delete pa;
  • ???? delete pb;
  • ???? delete pc;
  • 都自動調用了基類和派生類的析構函數,其中只有 delete pc; 涉及了虛擬函數機制?!禘ffective C++》中07條款中有這樣一句話:當derived class 對象經由一個 base 指針被刪除,而該對象帶有一個 non-virtual 析構函數,其結果未有定義---實際執行時通常發生的是對象的 derived 成分未被銷毀。

    特地,寫了下面的程序:

    class A { public:~A(){cout << "A destruction" << endl;}int a; };class B { public:~B(){cout << "B destruction" << endl;} };class C : public A,public B { public:~C(){cout << "C destruction" << endl;} };int main(void) {A *pa = new C;B *pb = new C;C *pc = new C;delete pa; // 沒有問題delete pb; // 出錯delete pc; // 沒有問題 }

    所說的「未定義」就在 delete pa; 和 delete pb; 體現出來。

    強烈建議,在設計繼承關系的時候,為每一個基類實現 virtual 析構函數。

    回到開始的問題:

  • 第一種情況是因為編譯器安插了一個字節,為的是一個類的對象能再內存有獨一無二的地址,無可厚非。
  • 第二種情況是因為編譯器安插了 vptr。
  • 第三種情況是因為編譯器除了安插 A 和 B 的 vptr 外,還有一個指向虛基類的指針。
  • 另外,虛擬繼承在應用比較少應用,一個例子就是:

    class ios {...};class istream : public virtual ios {...};calss ostream : public virtual ios {...};class iostream : public istream,public ostream {...};

    這里 istream,ostream,iostream 共享同一份 ios。要和下面的情況區分開來:

    class ios {...};class istream : public ios {...};calss ostream : public ios {...};class iostream : public istream,public ostream {...};

    這里實際有兩份 ios !全文完。daoluan.net

    轉載于:https://www.cnblogs.com/daoluanxiaozi/archive/2013/04/25/3042732.html

    總結

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

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