C++ 多态实现机制
本篇從 C++ 初學(xué)者遇到的一個有趣的問題開始。
有趣的問題
考慮下面的 C++ 程序:
class A {void func(){} };class B:public A {void func(){} };int main(void) {cout << sizeof(A) << " " << sizeof(B) << endl;return 0; }輸出結(jié)果是: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; }輸出結(jié)果是: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; }輸出結(jié)果是:4 12
對于第一種情況,沒有出現(xiàn)虛函數(shù),也無任何成員變量,因此是一個空類,空類理論上可以進行實例化,每個實例在內(nèi)存中都有獨一無二的地址來標(biāo)明,所以會占用 1B 的空間,無可厚非。
但第二種情況和第三種情況加入了虛函數(shù)(virtual function),而且在第三種情況當(dāng)中,引入了虛基類(virtual base class)的概念,所得到的結(jié)果大相徑庭,這是 C++ 引入了 virtual function 和 virtual base class,即多態(tài),更形象的解釋是「以一個 public base class 的指針或者引用,尋址出一個 derived?class object」,但多態(tài)帶了一定空間上的開銷,在效率上也有折損。
其實, 多態(tài)機制可以歸結(jié)為下面三這句話:
- 一般而言, 我們無法知道指針 ptr 所指的對象的真正類型. 但經(jīng)由 ptr 總是可以存取到對象的 virtual table.
- 虛函數(shù) fn() 總是放在 virtual table 中的固定位置, 用一個固定的索引值就可以 fetch 到.
- 唯一一個執(zhí)行期需要知道的是 ptr 所指的對象.
下面是 C++ 多態(tài)機制實現(xiàn)詳解.
從最簡單的對象模塊開始
最為簡單的對象模型:
靜態(tài)/非靜態(tài) 成員函數(shù) 和 靜態(tài)/非靜態(tài) 成員變量 的地址都存儲在一個表當(dāng)中,通過表內(nèi)存儲的地址指向相應(yīng)的部分。這樣的設(shè)計簡易,便于理解,類的實例只需要維護這張表就好了,賠上的是空間和執(zhí)行效率:
空間上:沒必要為每一個實例都存儲靜態(tài)成員變量和成員函數(shù)
效率上:每次執(zhí)行實例的一個成員函數(shù)都要在表內(nèi)進行搜索
這是最初的假設(shè),實際的實現(xiàn)肯定沒有那么簡單,下面是將變量和函數(shù)分割存儲的模型(表格驅(qū)動對象模型):
簡易對象模型經(jīng)改良后可以的得到這種。sizeof(A) 的結(jié)果是 8。
為支撐 virtual function ,引入了現(xiàn)在的 C++ 對象模型:
非靜態(tài)成員變量同指向虛擬函數(shù)表的指針(vptr),和靜態(tài)成員變量/函數(shù),非靜態(tài)成員函數(shù)分離存儲。類的每一個實例都存有 vptr 和 非靜態(tài)成員變量,他們獨立擁有這些數(shù)據(jù),并不和其他的實例共享。這時候,回到第二種情況,class A 和 繼承自 A 的 class B 都擁有虛函數(shù),因此都會有一個 vptr,因此 sizeof 運算得到的結(jié)果都為 4.然而,如果往里面添加一個非靜態(tài) int 型變量,那么相應(yīng)可以得到 8B 的大小;但往里面添加靜態(tài) int 型變量,大小卻沒有改變。
單一繼承
下面是單一繼承里經(jīng)常看到的一個程序:
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(); }輸出結(jié)果是:class B'sfunc.
多態(tài)就是多種狀態(tài),一個事物可能有多種表現(xiàn)形式,譬如動物,有十二生肖甚至更多的表現(xiàn)形式。當(dāng)基類里實現(xiàn)了某個虛函數(shù),但派生類沒有實現(xiàn),那么類 B 的實例里的虛函數(shù)表中放置的就是 &A::func。此外,派生類也實現(xiàn)了虛函數(shù),那么類 B 實例里的虛函數(shù)表中放置的就是 B::func。A *pa = new B; 因為 B 實現(xiàn)了 func,那么它被放入 A 實例的虛擬函數(shù)表中,從而代替 A 實例本身的虛擬函數(shù)。pa->func(); 調(diào)用的結(jié)果就不稀奇了,這是虛函數(shù)機制帶來的。
class A 和 class B 的內(nèi)存布局和 vptr 可能是下面的樣子:
倘若 虛函數(shù) 以外的就沒有「多態(tài)」效果了,除非進行強制類型轉(zhuǎn)換:
- pa->a;????????? //???? 成功,因為 pa 的類型就是 A
- pa->b;????????? //???? 失敗,因為 B::b
- pa->funcB();? //???? 失敗,因為B::funcB() 不是虛函數(shù)
- pa->funcA();? //???? 成功,因為A::funcA()
總結(jié)一下:
- 當(dāng)引入虛函數(shù)的時候,會添加 vptr 和 其指向的一個虛擬函數(shù)表從而增加額外的空間,這些信息在編譯期間就已經(jīng)確定,而且在執(zhí)行期不會插足修改任何內(nèi)容。
- 在類的構(gòu)造和析構(gòu)函數(shù)當(dāng)中添加對應(yīng)的代碼,從而能夠為 vptr 設(shè)定初值或者調(diào)整 vptr,這些動作由編譯器完成,class 會產(chǎn)生膨脹。
- 當(dāng)出現(xiàn)繼承關(guān)系時,虛擬函數(shù)表可能需要改寫,即當(dāng)用基類的指針指向一個派生類的實體地址,然后通過這個指針來調(diào)用虛函數(shù)。這里要分兩種情況,當(dāng)派生類已經(jīng)改寫同名虛函數(shù)時,那么此時調(diào)用的結(jié)果是派生類的實現(xiàn);而如果派生類沒有實現(xiàn),那么調(diào)用依然是基類的虛函數(shù)實現(xiàn),而且僅僅在多態(tài)僅僅在虛函數(shù)上表現(xiàn)。
- 多態(tài)僅僅在虛函數(shù)上表現(xiàn),意即倘若同樣用基類的指針指向一個派生類的實體地址,那么這個指針將不能訪問和調(diào)用派生類的成員變量和成員函數(shù)。
- 所謂執(zhí)行期確定的東西,就是基類指針?biāo)赶虻膶嶓w地址是什么類型了,這是唯一執(zhí)行期確定的。以上是單一繼承的情況,在多重繼承的情況會更為復(fù)雜。
多重繼承
下面是少有看到的程序代碼:
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; }當(dāng)用基類的指針指向一個派生類的實體地址,基類有兩種情況,一種是 class A 和 class B,如果是 A,問題容易解決,幾乎和上面單一繼承情況類似;但倘若是 B,要做地址上的轉(zhuǎn)換,情況會比前者復(fù)雜。先展現(xiàn)class A,B,C 的內(nèi)存布局和 vptr:
?
多重繼承中,會有保留兩個虛擬函數(shù)表,一個是與 A 共享的,一個是與 B 相關(guān)的,他們都在原有的基礎(chǔ)上進行了修改:
對于 A 的虛擬函數(shù)表:
- 覆蓋派生類實現(xiàn)的同名虛函數(shù),并用派生類實現(xiàn)的析構(gòu)函數(shù)覆蓋原有虛函數(shù)
- 添加了派生類獨有的虛函數(shù)
- 添加了右端父類即 B 的獨有虛函數(shù),需跳轉(zhuǎn)
對于 B 的虛擬函數(shù)表:
- 覆蓋派生類實現(xiàn)的同名虛函數(shù),并用派生類實現(xiàn)的析構(gòu)函數(shù)覆蓋原有虛函數(shù),但需跳轉(zhuǎn)
輸出結(jié)果是:
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 行的行為有很大的區(qū)別,7 行的調(diào)用和上面的單一繼承的情況類似,不贅述。8 行的 pb->func(); 中,pb 所指向的是上圖第 9 行的位置,編譯器已在內(nèi)部做了轉(zhuǎn)換,也就是 pa 和 pb 所指的位置不一樣,pa 指向的是上圖第 3 行的位置。接著需要注意的是,pb->func(); 調(diào)用時,在虛擬函數(shù)表中找到的地址需要再進行一次跳轉(zhuǎn),目標(biāo)是 A 的虛擬函數(shù)表中的 &C::func(),然后才真正執(zhí)行此函數(shù)。所以,上面的情況作了指針的調(diào)整。
那什么時候會出現(xiàn)跳,常見的有兩種情況:
所以 delete pa; 和 delete pa; 的操作是不一樣的,pb->funcB(); 和 pc->funcB(); 也不一樣。
C++ 為實現(xiàn)多態(tài)引入虛函數(shù)機制,帶來了空間和執(zhí)行上的折損。
單一/多重繼承的構(gòu)造和析構(gòu)
單一繼承中,構(gòu)造函數(shù)調(diào)用順序是從上到下(單一繼承),從左到右(多重繼承),析構(gòu)函數(shù)調(diào)用順序反過來。在上一段程序中,
都自動調(diào)用了基類和派生類的析構(gòu)函數(shù),其中只有 delete pc; 涉及了虛擬函數(shù)機制。《Effective C++》中07條款中有這樣一句話:當(dāng)derived class 對象經(jīng)由一個 base 指針被刪除,而該對象帶有一個 non-virtual 析構(gòu)函數(shù),其結(jié)果未有定義---實際執(zhí)行時通常發(fā)生的是對象的 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; 體現(xiàn)出來。
強烈建議,在設(shè)計繼承關(guān)系的時候,為每一個基類實現(xiàn) virtual 析構(gòu)函數(shù)。
回到開始的問題:
另外,虛擬繼承在應(yīng)用比較少應(yīng)用,一個例子就是:
class ios {...};class istream : public virtual ios {...};calss ostream : public virtual ios {...};class iostream : public istream,public ostream {...};這里 istream,ostream,iostream 共享同一份 ios。要和下面的情況區(qū)分開來:
class ios {...};class istream : public ios {...};calss ostream : public ios {...};class iostream : public istream,public ostream {...};這里實際有兩份 ios !全文完。daoluan.net
轉(zhuǎn)載于:https://www.cnblogs.com/daoluanxiaozi/archive/2013/04/25/3042732.html
總結(jié)
以上是生活随笔為你收集整理的C++ 多态实现机制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 军费保障幼师是部队职工吗
- 下一篇: 印度遭遇蝗虫疫情双重灾难,蝗灾可能引起大