C++中虚函数工作原理和(虚)继承类的内存占用大小计算
生活随笔
收集整理的這篇文章主要介紹了
C++中虚函数工作原理和(虚)继承类的内存占用大小计算
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
轉載請標明出處,原文地址:http://blog.csdn.net/hackbuteer1/article/details/7883531
一、虛函數的工作原理
????? 虛函數的實現要求對象攜帶額外的信息,這些信息用于在運行時確定該對象應該調用哪一個虛函數。典型情況下,這一信息具有一種被稱為 vptr(virtual table pointer,虛函數表指針)的指針的形式。vptr 指向一個被稱為 vtbl(virtual table,虛函數表)的函數指針數組,每一個包含虛函數的類都關聯到 vtbl。當一個對象調用了虛函數,實際的被調用函數通過下面的步驟確定:找到對象的 vptr 指向的 vtbl,然后在 vtbl 中尋找合適的函數指針。
????? 虛擬函數的地址翻譯取決于對象的內存地址,而不取決于數據類型(編譯器對函數調用的合法性檢查取決于數據類型)。如果類定義了虛函數,該類及其派生類就要生成一張虛擬函數表,即vtable。而在類的對象地址空間中存儲一個該虛表的入口,占4個字節,這個入口地址是在構造對象時由編譯器寫入的。所以,由于對象的內存空間包含了虛表入口,編譯器能夠由這個入口找到恰當的虛函數,這個函數的地址不再由數據類型決定了。故對于一個父類的對象指針,調用虛擬函數,如果給他賦父類對象的指針,那么他就調用父類中的函數,如果給他賦子類對象的指針,他就調用子類中的函數(取決于對象的內存地址)。
????? 虛函數需要注意的大概就是這些個地方了,之前在More effective C++上好像也有見過,不過這次在Visual C++權威剖析這本書中有了更直白的認識,這本書名字很牛逼,看看內容也就那么回事,感覺名不副實,不過說起來也是有其獨到之處的,否則也沒必要出這種書了。
????? 每當創建一個包含有虛函數的類或從包含有虛函數的類派生一個類時,編譯器就會為這個類創建一個虛函數表(VTABLE)保存該類所有虛函數的地址,其實這個VTABLE的作用就是保存自己類中所有虛函數的地址,可以把VTABLE形象地看成一個函數指針數組,這個數組的每個元素存放的就是虛函數的地址。在每個帶有虛函數的類 中,編譯器秘密地置入一指針,稱為v p o i n t e r(縮寫為V P T R),指向這個對象的V TA B L E。 當構造該派生類對象時,其成員VPTR被初始化指向該派生類的VTABLE。所以可以認為VTABLE是該類的所有對象共有的,在定義該類時被初始化;而VPTR則是每個類對象都有獨立一份的,且在該類對象被構造時被初始化。
????? 通過基類指針做虛函數調 用時(也就是做多態調用時),編譯器靜態地插入取得這個V P T R,并在V TA B L E表中查找函數地址的代碼,這樣就能調用正確的函數使晚捆綁發生。為每個類設置V TA B L E、初始化V P T R、為虛函數調用插入代碼,所有這些都是自動發生的,所以我們不必擔心這些。
#include<iostream> using namespace std;class A { public:virtual void fun1(){cout << "A::fun1()" << endl;}virtual void fun2(){cout << "A::fun2()" << endl;} };class B : public A { public:void fun1(){cout << "B::fun1()" << endl;}void fun2(){cout << "B::fun2()" << endl;} };int main() {A *pa = new B;pa->fun1();delete pa;system("pause"); return 0; } ???????????? 毫無疑問,調用了B::fun1(),但是B::fun1()不是像普通函數那樣直接找到函數地址而執行的。真正的執行方式是:首先取出pa指針所指向的對象的vptr的值,這個值就是vtbl的地址,由于調用的函數B::fun1()是第一個虛函數,所以取出vtbl第一個表項里的值,這個值就是B::fun1()的地址了,最后調用這個函數。因此只要vptr不同,指向的vtbl就不同,而不同的vtbl里裝著對應類的虛函數地址,所以這樣虛函數就可以完成它的任務,多態就是這樣實現的。
????? 而對于class A和class B來說,他們的vptr指針存放在何處?其實這個指針就放在他們各自的實例對象里。由于class A和class B都沒有數據成員,所以他們的實例對象里就只有一個vptr指針。
??? ? 虛擬函數使用的缺點
優點講了一大堆,現在談一下缺點,虛函數最主要的缺點是執行效率較低,看一看虛擬函數引發的多態性的實現過程,你就能體會到其中的原因,另外就是由于要攜帶額外的信息(VPTR),所以導致類多占的內存空間也會比較大,對象也是一樣的。
???? 含有虛函數的對象在內存中的結構如下:
class A { private:int a;int b; public:virtual void fun0(){cout<<"A::fun0"<<endl;} };
1、直接繼承
那我們來看看編譯器是怎么建立VPTR指向的這個虛函數表的,先看下面兩個類:
class base { private:int a; public:void bfun(){}virtual void vfun1(){}virtual void vfun2(){} };class derived : public base { private:int b; public:void dfun(){}virtual void vfun1(){}virtual void vfun3(){} }; 兩個類的VPTR指向的虛函數表(VTABLE)分別如下:
base類
???????????????????? ——————
VPTR——> ?? |&base::vfun1 |
????????????????????? ——————
??????????????????? |&base::vfun2 |
??????????????????? ——————
???? ?
derived類
????????????????????? ———————
VPTR——>??? |&derived::vfun1 |
???????????????????? ———————
??????????????? ?? |&base::vfun2???? |
??????????????????? ———————
?????????????????? |&derived::vfun3 |
??????????????????? ———————
????? 每當創建一個包含有虛函數的類或從包含有虛函數的類派生一個類時,編譯器就為這個類創建一個VTABLE,如上圖所示。在這個表中,編譯器放置了在這個類中或在它的基類中所有已聲明為virtual的函數的地址。如果在這個派生類中沒有對在基類中聲明為virtual的函數進行重新定義,編譯器就使用基類 的這個虛函數地址。(在derived的VTABLE中,vfun2的入口就是這種情況。)然后編譯器在這個類中放置VPTR。當使用簡單繼承時,對于每個對象只有一個VPTR。VPTR必須被初始化為指向相應的VTABLE,這在構造函數中發生。
?????? 一旦VPTR被初始化為指向相應的VTABLE,對象就"知道"它自己是什么類型。但只有當虛函數被調用時這種自我認知才有用。
?????? 沒有虛函數類對象的大小正好是數據成員的大小,包含有一個或者多個虛函數的類對象編譯器向里面插入了一個VPTR指針(void *),指向一個存放函數地址的表就是我們上面說的VTABLE,這些都是編譯器為我們做的我們完全可以不關心這些。所以有虛函數的類對象的大小是數據成員的大小加上一個VPTR指針(void *)的大小。
總結一下VPTR 和 VTABLE 和類對象的關系:
?????? 每一個具有虛函數的類都有一個虛函數表VTABLE,里面按在類中聲明的虛函數的順序存放著虛函數的地址,這個虛函數表VTABLE是這個類的所有對象所共有的,也就是說無論用戶聲明了多少個類對象,但是這個VTABLE虛函數表只有一個。
?????? 在每個具有虛函數的類的對象里面都有一個VPTR虛函數指針,這個指針指向VTABLE的首地址,每個類的對象都有這么一種指針。
2、虛繼承
???? 這個是比較不好理解的,對于虛繼承,若派生類有自己的虛函數,則它本身需要有一個虛指針,指向自己的虛表。另外,派生類虛繼承父類時,首先要通過加入一個虛指針來指向父類,因此有可能會有兩個虛指針。
二、(虛)繼承類的內存占用大小
???? 首先,平時所聲明的類只是一種類型定義,它本身是沒有大小可言的。 因此,如果用sizeof運算符對一個類型名操作,那得到的是具有該類型實體的大小。
計算一個類對象的大小時的規律:
??? 1、空類、單一繼承的空類、多重繼承的空類所占空間大小為:1(字節,下同);
??? 2、一個類中,虛函數本身、成員函數(包括靜態與非靜態)和靜態數據成員都是不占用類對象的存儲空間的;
??? 3、因此一個對象的大小≥所有非靜態成員大小的總和;
??? 4、當類中聲明了虛函數(不管是1個還是多個),那么在實例化對象時,編譯器會自動在對象里安插一個指針vPtr指向虛函數表VTable;
??? 5、虛承繼的情況:由于涉及到虛函數表和虛基表,會同時增加一個(多重虛繼承下對應多個)vfPtr指針指向虛函數表vfTable和一個vbPtr指針指向虛基表vbTable,這兩者所占的空間大小為:8(或8乘以多繼承時父類的個數);
??? 6、在考慮以上內容所占空間的大小時,還要注意編譯器下的“補齊”padding的影響,即編譯器會插入多余的字節補齊;
??? 7、類對象的大小=各非靜態數據成員(包括父類的非靜態數據成員但都不包括所有的成員函數)的總和+ vfptr指針(多繼承下可能不止一個)+vbptr指針(多繼承下可能不止一個)+編譯器額外增加的字節。
示例一:含有普通繼承
class A { }; class B {char ch; virtual void func0() { } }; class C {char ch1;char ch2;virtual void func() { } virtual void func1() { } };class D: public A, public C { int d; virtual void func() { } virtual void func1() { } }; class E: public B, public C { int e; virtual void func0() { } virtual void func1() { } };int main(void) {cout<<"A="<<sizeof(A)<<endl; //result=1cout<<"B="<<sizeof(B)<<endl; //result=8 cout<<"C="<<sizeof(C)<<endl; //result=8cout<<"D="<<sizeof(D)<<endl; //result=12cout<<"E="<<sizeof(E)<<endl; //result=20return 0; } 前面三個A、B、C類的內存占用空間大小就不需要解釋了,注意一下內存對齊就可以理解了。
求sizeof(D)的時候,需要明白,首先VPTR指向的虛函數表中保存的是類D中的兩個虛函數的地址,然后存放基類C中的兩個數據成員ch1、ch2,注意內存對齊,然后存放數據成員d,這樣4+4+4=12。
求sizeof(E)的時候,首先是類B的虛函數地址,然后類B中的數據成員,再然后是類C的虛函數地址,然后類C中的數據成員,最后是類E中的數據成員e,同樣注意內存對齊,這樣4+4+4+4+4=20。
示例二:含有虛繼承
class CommonBase {int co; };class Base1: virtual public CommonBase { public:virtual void print1() { }virtual void print2() { } private:int b1; };class Base2: virtual public CommonBase { public:virtual void dump1() { }virtual void dump2() { } private:int b2; };class Derived: public Base1, public Base2 { public:void print2() { }void dump2() { } private:int d; }; sizeof(Derived)=32,其在內存中分布的情況如下:
class Derived size(32):+---| +--- (base class Base1)| | {vfptr}| | {vbptr}| | b1| +---| +--- (base class Base2)| | {vfptr}| | {vbptr}| | b2| +---| d+---+--- (virtual base CommonBase)| co+--- 示例3:
class A { public:virtual void aa() { }virtual void aa2() { } private:char ch[3]; };class B: virtual public A { public:virtual void bb() { }virtual void bb2() { } };int main(void) {cout<<"A's size is "<<sizeof(A)<<endl;cout<<"B's size is "<<sizeof(B)<<endl;return 0; } 執行結果:A's size is 8
????????????? B's size is 16
????? 說明:對于虛繼承,類B因為有自己的虛函數,所以它本身有一個虛指針,指向自己的虛表。另外,類B虛繼承類A時,首先要通過加入一個虛指針來指向父類A,然后還要包含父類A的所有內容。因此是4+4+8=16。
兩種多態實現機制及其優缺點
除了c++的這種多態的實現機制之外,還有另外一種實現機制,也是查表,不過是按名稱查表,是smalltalk等語言的實現機制。這兩種方法的優缺點如下:
(1)、按照絕對位置查表,這種方法由于編譯階段已經做好了索引和表項(如上面的call *(pa->vptr[1]) ),所以運行速度比較快;缺點是:當A的virtual成員比較多(比如1000個),而B重寫的成員比較少(比如2個),這種時候,B的vtableB的剩下的998個表項都是放A中的virtual成員函數的指針,如果這個派生體系比較大的時候,就浪費了很多的空間。
比如:GUI庫,以MFC庫為例,MFC有很多類,都是一個繼承體系;而且很多時候每個類只是1,2個成員函數需要在派生類重寫,如果用C++的虛函數機制,每個類有一個虛表,每個表里面有大量的重復,就會造成空間利用率不高。于是MFC的消息映射機制不用虛函數,而用第二種方法來實現多態,那就是:
(2)、按照函數名稱查表,這種方案可以避免如上的問題;但是由于要比較名稱,有時候要遍歷所有的繼承結構,時間效率性能不是很高。(關于MFC的消息映射的實現,看下一篇文章)
3、總結:
如果繼承體系的基類的virtual成員不多,而且在派生類要重寫的部分占了其中的大多數時候,用C++的虛函數機制是比較好的;
但是如果繼承體系的基類的virtual成員很多,或者是繼承體系比較龐大的時候,而且派生類中需要重寫的部分比較少,那就用名稱查找表,這樣效率會高一些,很多的GUI庫都是這樣的,比如MFC,QT。
PS:其實,自從計算機出現之后,時間和空間就成了永恒的主題,因為兩者在98%的情況下都無法協調,此長彼消;這個就是計算機科學中的根本瓶頸之所在。軟件科學和算法的發展,就看能不能突破這對時空權衡了。呵呵。。
何止計算機科學如此,整個宇宙又何嘗不是如此呢?最基本的宇宙之謎,還是時間和空間。
C++如何不用虛函數實現多態
可以考慮使用函數指針來實現多態
#include<iostream> using namespace std;typedef void (*fVoid)();class A { public:static void test(){printf("hello A\n");}fVoid print;A(){print = A::test;} };class B : public A { public:static void test(){printf("hello B\n");}B(){print = B::test;} };int main(void) {A aa;aa.print();B b;A* a = &b;a->print();return 0; } 這樣做的好處主要是繞過了vtable。我們都知道虛函數表有時候會帶來一些性能損失。
轉載請標明出處,原文地址:http://blog.csdn.net/hackbuteer1/article/details/7883531
一、虛函數的工作原理
????? 虛函數的實現要求對象攜帶額外的信息,這些信息用于在運行時確定該對象應該調用哪一個虛函數。典型情況下,這一信息具有一種被稱為 vptr(virtual table pointer,虛函數表指針)的指針的形式。vptr 指向一個被稱為 vtbl(virtual table,虛函數表)的函數指針數組,每一個包含虛函數的類都關聯到 vtbl。當一個對象調用了虛函數,實際的被調用函數通過下面的步驟確定:找到對象的 vptr 指向的 vtbl,然后在 vtbl 中尋找合適的函數指針。
????? 虛擬函數的地址翻譯取決于對象的內存地址,而不取決于數據類型(編譯器對函數調用的合法性檢查取決于數據類型)。如果類定義了虛函數,該類及其派生類就要生成一張虛擬函數表,即vtable。而在類的對象地址空間中存儲一個該虛表的入口,占4個字節,這個入口地址是在構造對象時由編譯器寫入的。所以,由于對象的內存空間包含了虛表入口,編譯器能夠由這個入口找到恰當的虛函數,這個函數的地址不再由數據類型決定了。故對于一個父類的對象指針,調用虛擬函數,如果給他賦父類對象的指針,那么他就調用父類中的函數,如果給他賦子類對象的指針,他就調用子類中的函數(取決于對象的內存地址)。
????? 虛函數需要注意的大概就是這些個地方了,之前在More effective C++上好像也有見過,不過這次在Visual C++權威剖析這本書中有了更直白的認識,這本書名字很牛逼,看看內容也就那么回事,感覺名不副實,不過說起來也是有其獨到之處的,否則也沒必要出這種書了。
????? 每當創建一個包含有虛函數的類或從包含有虛函數的類派生一個類時,編譯器就會為這個類創建一個虛函數表(VTABLE)保存該類所有虛函數的地址,其實這個VTABLE的作用就是保存自己類中所有虛函數的地址,可以把VTABLE形象地看成一個函數指針數組,這個數組的每個元素存放的就是虛函數的地址。在每個帶有虛函數的類 中,編譯器秘密地置入一指針,稱為v p o i n t e r(縮寫為V P T R),指向這個對象的V TA B L E。 當構造該派生類對象時,其成員VPTR被初始化指向該派生類的VTABLE。所以可以認為VTABLE是該類的所有對象共有的,在定義該類時被初始化;而VPTR則是每個類對象都有獨立一份的,且在該類對象被構造時被初始化。
????? 通過基類指針做虛函數調 用時(也就是做多態調用時),編譯器靜態地插入取得這個V P T R,并在V TA B L E表中查找函數地址的代碼,這樣就能調用正確的函數使晚捆綁發生。為每個類設置V TA B L E、初始化V P T R、為虛函數調用插入代碼,所有這些都是自動發生的,所以我們不必擔心這些。
#include<iostream> using namespace std;class A { public:virtual void fun1(){cout << "A::fun1()" << endl;}virtual void fun2(){cout << "A::fun2()" << endl;} };class B : public A { public:void fun1(){cout << "B::fun1()" << endl;}void fun2(){cout << "B::fun2()" << endl;} };int main() {A *pa = new B;pa->fun1();delete pa;system("pause"); return 0; } ???????????? 毫無疑問,調用了B::fun1(),但是B::fun1()不是像普通函數那樣直接找到函數地址而執行的。真正的執行方式是:首先取出pa指針所指向的對象的vptr的值,這個值就是vtbl的地址,由于調用的函數B::fun1()是第一個虛函數,所以取出vtbl第一個表項里的值,這個值就是B::fun1()的地址了,最后調用這個函數。因此只要vptr不同,指向的vtbl就不同,而不同的vtbl里裝著對應類的虛函數地址,所以這樣虛函數就可以完成它的任務,多態就是這樣實現的。
????? 而對于class A和class B來說,他們的vptr指針存放在何處?其實這個指針就放在他們各自的實例對象里。由于class A和class B都沒有數據成員,所以他們的實例對象里就只有一個vptr指針。
??? ? 虛擬函數使用的缺點
優點講了一大堆,現在談一下缺點,虛函數最主要的缺點是執行效率較低,看一看虛擬函數引發的多態性的實現過程,你就能體會到其中的原因,另外就是由于要攜帶額外的信息(VPTR),所以導致類多占的內存空間也會比較大,對象也是一樣的。
???? 含有虛函數的對象在內存中的結構如下:
class A { private:int a;int b; public:virtual void fun0(){cout<<"A::fun0"<<endl;} };
1、直接繼承
那我們來看看編譯器是怎么建立VPTR指向的這個虛函數表的,先看下面兩個類:
class base { private:int a; public:void bfun(){}virtual void vfun1(){}virtual void vfun2(){} };class derived : public base { private:int b; public:void dfun(){}virtual void vfun1(){}virtual void vfun3(){} }; 兩個類的VPTR指向的虛函數表(VTABLE)分別如下:
base類
???????????????????? ——————
VPTR——> ?? |&base::vfun1 |
????????????????????? ——————
??????????????????? |&base::vfun2 |
??????????????????? ——————
???? ?
derived類
????????????????????? ———————
VPTR——>??? |&derived::vfun1 |
???????????????????? ———————
??????????????? ?? |&base::vfun2???? |
??????????????????? ———————
?????????????????? |&derived::vfun3 |
??????????????????? ———————
????? 每當創建一個包含有虛函數的類或從包含有虛函數的類派生一個類時,編譯器就為這個類創建一個VTABLE,如上圖所示。在這個表中,編譯器放置了在這個類中或在它的基類中所有已聲明為virtual的函數的地址。如果在這個派生類中沒有對在基類中聲明為virtual的函數進行重新定義,編譯器就使用基類 的這個虛函數地址。(在derived的VTABLE中,vfun2的入口就是這種情況。)然后編譯器在這個類中放置VPTR。當使用簡單繼承時,對于每個對象只有一個VPTR。VPTR必須被初始化為指向相應的VTABLE,這在構造函數中發生。
?????? 一旦VPTR被初始化為指向相應的VTABLE,對象就"知道"它自己是什么類型。但只有當虛函數被調用時這種自我認知才有用。
?????? 沒有虛函數類對象的大小正好是數據成員的大小,包含有一個或者多個虛函數的類對象編譯器向里面插入了一個VPTR指針(void *),指向一個存放函數地址的表就是我們上面說的VTABLE,這些都是編譯器為我們做的我們完全可以不關心這些。所以有虛函數的類對象的大小是數據成員的大小加上一個VPTR指針(void *)的大小。
總結一下VPTR 和 VTABLE 和類對象的關系:
?????? 每一個具有虛函數的類都有一個虛函數表VTABLE,里面按在類中聲明的虛函數的順序存放著虛函數的地址,這個虛函數表VTABLE是這個類的所有對象所共有的,也就是說無論用戶聲明了多少個類對象,但是這個VTABLE虛函數表只有一個。
?????? 在每個具有虛函數的類的對象里面都有一個VPTR虛函數指針,這個指針指向VTABLE的首地址,每個類的對象都有這么一種指針。
2、虛繼承
???? 這個是比較不好理解的,對于虛繼承,若派生類有自己的虛函數,則它本身需要有一個虛指針,指向自己的虛表。另外,派生類虛繼承父類時,首先要通過加入一個虛指針來指向父類,因此有可能會有兩個虛指針。
二、(虛)繼承類的內存占用大小
???? 首先,平時所聲明的類只是一種類型定義,它本身是沒有大小可言的。 因此,如果用sizeof運算符對一個類型名操作,那得到的是具有該類型實體的大小。
計算一個類對象的大小時的規律:
??? 1、空類、單一繼承的空類、多重繼承的空類所占空間大小為:1(字節,下同);
??? 2、一個類中,虛函數本身、成員函數(包括靜態與非靜態)和靜態數據成員都是不占用類對象的存儲空間的;
??? 3、因此一個對象的大小≥所有非靜態成員大小的總和;
??? 4、當類中聲明了虛函數(不管是1個還是多個),那么在實例化對象時,編譯器會自動在對象里安插一個指針vPtr指向虛函數表VTable;
??? 5、虛承繼的情況:由于涉及到虛函數表和虛基表,會同時增加一個(多重虛繼承下對應多個)vfPtr指針指向虛函數表vfTable和一個vbPtr指針指向虛基表vbTable,這兩者所占的空間大小為:8(或8乘以多繼承時父類的個數);
??? 6、在考慮以上內容所占空間的大小時,還要注意編譯器下的“補齊”padding的影響,即編譯器會插入多余的字節補齊;
??? 7、類對象的大小=各非靜態數據成員(包括父類的非靜態數據成員但都不包括所有的成員函數)的總和+ vfptr指針(多繼承下可能不止一個)+vbptr指針(多繼承下可能不止一個)+編譯器額外增加的字節。
示例一:含有普通繼承
class A { }; class B {char ch; virtual void func0() { } }; class C {char ch1;char ch2;virtual void func() { } virtual void func1() { } };class D: public A, public C { int d; virtual void func() { } virtual void func1() { } }; class E: public B, public C { int e; virtual void func0() { } virtual void func1() { } };int main(void) {cout<<"A="<<sizeof(A)<<endl; //result=1cout<<"B="<<sizeof(B)<<endl; //result=8 cout<<"C="<<sizeof(C)<<endl; //result=8cout<<"D="<<sizeof(D)<<endl; //result=12cout<<"E="<<sizeof(E)<<endl; //result=20return 0; } 前面三個A、B、C類的內存占用空間大小就不需要解釋了,注意一下內存對齊就可以理解了。
求sizeof(D)的時候,需要明白,首先VPTR指向的虛函數表中保存的是類D中的兩個虛函數的地址,然后存放基類C中的兩個數據成員ch1、ch2,注意內存對齊,然后存放數據成員d,這樣4+4+4=12。
求sizeof(E)的時候,首先是類B的虛函數地址,然后類B中的數據成員,再然后是類C的虛函數地址,然后類C中的數據成員,最后是類E中的數據成員e,同樣注意內存對齊,這樣4+4+4+4+4=20。
示例二:含有虛繼承
class CommonBase {int co; };class Base1: virtual public CommonBase { public:virtual void print1() { }virtual void print2() { } private:int b1; };class Base2: virtual public CommonBase { public:virtual void dump1() { }virtual void dump2() { } private:int b2; };class Derived: public Base1, public Base2 { public:void print2() { }void dump2() { } private:int d; }; sizeof(Derived)=32,其在內存中分布的情況如下:
class Derived size(32):+---| +--- (base class Base1)| | {vfptr}| | {vbptr}| | b1| +---| +--- (base class Base2)| | {vfptr}| | {vbptr}| | b2| +---| d+---+--- (virtual base CommonBase)| co+--- 示例3:
class A { public:virtual void aa() { }virtual void aa2() { } private:char ch[3]; };class B: virtual public A { public:virtual void bb() { }virtual void bb2() { } };int main(void) {cout<<"A's size is "<<sizeof(A)<<endl;cout<<"B's size is "<<sizeof(B)<<endl;return 0; } 執行結果:A's size is 8
????????????? B's size is 16
????? 說明:對于虛繼承,類B因為有自己的虛函數,所以它本身有一個虛指針,指向自己的虛表。另外,類B虛繼承類A時,首先要通過加入一個虛指針來指向父類A,然后還要包含父類A的所有內容。因此是4+4+8=16。
兩種多態實現機制及其優缺點
除了c++的這種多態的實現機制之外,還有另外一種實現機制,也是查表,不過是按名稱查表,是smalltalk等語言的實現機制。這兩種方法的優缺點如下:
(1)、按照絕對位置查表,這種方法由于編譯階段已經做好了索引和表項(如上面的call *(pa->vptr[1]) ),所以運行速度比較快;缺點是:當A的virtual成員比較多(比如1000個),而B重寫的成員比較少(比如2個),這種時候,B的vtableB的剩下的998個表項都是放A中的virtual成員函數的指針,如果這個派生體系比較大的時候,就浪費了很多的空間。
比如:GUI庫,以MFC庫為例,MFC有很多類,都是一個繼承體系;而且很多時候每個類只是1,2個成員函數需要在派生類重寫,如果用C++的虛函數機制,每個類有一個虛表,每個表里面有大量的重復,就會造成空間利用率不高。于是MFC的消息映射機制不用虛函數,而用第二種方法來實現多態,那就是:
(2)、按照函數名稱查表,這種方案可以避免如上的問題;但是由于要比較名稱,有時候要遍歷所有的繼承結構,時間效率性能不是很高。(關于MFC的消息映射的實現,看下一篇文章)
3、總結:
如果繼承體系的基類的virtual成員不多,而且在派生類要重寫的部分占了其中的大多數時候,用C++的虛函數機制是比較好的;
但是如果繼承體系的基類的virtual成員很多,或者是繼承體系比較龐大的時候,而且派生類中需要重寫的部分比較少,那就用名稱查找表,這樣效率會高一些,很多的GUI庫都是這樣的,比如MFC,QT。
PS:其實,自從計算機出現之后,時間和空間就成了永恒的主題,因為兩者在98%的情況下都無法協調,此長彼消;這個就是計算機科學中的根本瓶頸之所在。軟件科學和算法的發展,就看能不能突破這對時空權衡了。呵呵。。
何止計算機科學如此,整個宇宙又何嘗不是如此呢?最基本的宇宙之謎,還是時間和空間。
C++如何不用虛函數實現多態
可以考慮使用函數指針來實現多態
#include<iostream> using namespace std;typedef void (*fVoid)();class A { public:static void test(){printf("hello A\n");}fVoid print;A(){print = A::test;} };class B : public A { public:static void test(){printf("hello B\n");}B(){print = B::test;} };int main(void) {A aa;aa.print();B b;A* a = &b;a->print();return 0; } 這樣做的好處主要是繞過了vtable。我們都知道虛函數表有時候會帶來一些性能損失。
轉載請標明出處,原文地址:http://blog.csdn.net/hackbuteer1/article/details/7883531
總結
以上是生活随笔為你收集整理的C++中虚函数工作原理和(虚)继承类的内存占用大小计算的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: STL源码剖析---红黑树原理详解下
- 下一篇: C/C++笔试题目大全