C++(虛)繼承類的內存佔用大小

首先,平時所聲明的類只是一種類型定義,它本身是沒有大小可言的。 因此,如果用sizeof運算符對一個類型名操作,那得到的是具有該類型實體的大小。

計算一個類對象的大小時的規律:

1、空類、單一繼承的空類、多重繼承的空類所佔空間大小爲:1(字節,下同);

2、一個類中,虛函數本身、成員函數(包括靜態與非靜態)和靜態數據成員都是不佔用類對象的存儲空間的;

3、因此一個對象的大小≥所有非靜態成員大小的總和;

4、當類中聲明瞭虛函數(不管是1個還是多個),那麼在實例化對象時,編譯器會自動在對象裏安插一個指針vPtr指向虛函數表VTable;

5、虛承繼的情況:由於涉及到虛函數表和虛基表,會同時增加一個(多重虛繼承下對應多個)vfPtr指針指向虛函數表vfTable和一個vbPtr指針指向虛基表vbTable,這兩者所佔的空間大小爲:8(或8乘以多繼承時父類的個數);

6、在考慮以上內容所佔空間的大小時,還要注意編譯器下的“補齊”padding的影響,即編譯器會插入多餘的字節補齊;

7、類對象的大小=各非靜態數據成員(包括父類的非靜態數據成員但都不包括所有的成員函數)的總和+ vfptr指針(多繼承下可能不止一個)+vbptr指針(多繼承下可能不止一個)+編譯器額外增加的字節。

示例一:含有普通繼承


  1. class A       
  2. {       
  3. };      
  4.    
  5. class B       
  6. {    
  7.     char ch;       
  8.     virtual void func0()  {  }     
  9. };     
  10.    
  11. class C      
  12. {    
  13.     char ch1;    
  14.     char ch2;    
  15.     virtual void func()  {  }      
  16.     virtual void func1()  {  }     
  17. };    
  18.    
  19. class D: public A, public C    
  20. {       
  21.     int d;       
  22.     virtual void func()  {  }     
  23.     virtual void func1()  {  }    
  24. };       
  25.    
  26. class E: public B, public C    
  27. {       
  28.     int e;       
  29.     virtual void func0()  {  }     
  30.     virtual void func1()  {  }    
  31. };    
  32.    
  33. int main(void)    
  34. {    
  35.     cout<<"A="<<sizeof(A)<<endl;    //result=1    
  36.     cout<<"B="<<sizeof(B)<<endl;    //result=8        
  37.     cout<<"C="<<sizeof(C)<<endl;    //result=8    
  38.     cout<<"D="<<sizeof(D)<<endl;    //result=12    
  39.     cout<<"E="<<sizeof(E)<<endl;    //result=20    
  40.     return 0;    
  41. }  

前面三個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。

示例二:含有虛繼承

[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. class CommonBase    
  2. {    
  3.     int co;    
  4. };    
  5.    
  6. class Base1: virtual public CommonBase    
  7. {    
  8. public:    
  9.     virtual void print1() {  }    
  10.     virtual void print2() {  }    
  11. private:    
  12.     int b1;    
  13. };    
  14.    
  15. class Base2: virtual public CommonBase    
  16. {    
  17. public:    
  18.     virtual void dump1() {  }    
  19.     virtual void dump2() {  }    
  20. private:    
  21.     int b2;    
  22. };    
  23.    
  24. class Derived: public Base1, public Base2    
  25. {    
  26. public:    
  27.     void print2() {  }    
  28.     void dump2() {  }    
  29. private:    
  30.     int d;    
  31. };  

sizeof(Derived)=32,其在內存中分佈的情況如下:

[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. class Derived size(32):    
  2.      +---    
  3.      | +--- (base class Base1)    
  4.  | | {vfptr}    
  5.  | | {vbptr}    
  6.  | | b1    
  7.      | +---    
  8.      | +--- (base class Base2)    
  9.  | | {vfptr}    
  10.  | | {vbptr}    
  11.  | | b2    
  12.     | +---    
  13.  | d    
  14.     +---    
  15.     +--- (virtual base CommonBase)    
  16.  | co    
  17.     +---  

示例3:

[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. class A    
  2. {    
  3. public:    
  4.     virtual void aa() {  }    
  5.     virtual void aa2() {  }    
  6. private:    
  7.     char ch[3];    
  8. };    
  9.    
  10. class B: virtual public A    
  11. {    
  12. public:    
  13.     virtual void bb() {  }    
  14.     virtual void bb2() {  }    
  15. };    
  16.    
  17. int main(void)    
  18. {    
  19.     cout<<"A's size is "<<sizeof(A)<<endl;    
  20.     cout<<"B's size is "<<sizeof(B)<<endl;    
  21.     return 0;    
  22. }  

執行結果: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)、按照函數名稱查表,這種方案可以避免如上的問題;但是由於要比較名稱,有時候要遍歷所有的繼承結構,時間效率性能不是很高。

3、總結:

如果繼承體系的基類的virtual成員不多,而且在派生類要重寫的部分佔了其中的大多數時候,用C++的虛函數機制是比較好的;但是如果繼承體系的基類的virtual成員很多,或者是繼承體系比較龐大的時候,而且派生類中需要重寫的部分比較少,那就用名稱查找表,這樣效率會高一些,很多的GUI庫都是這樣的,比如MFC,QT。PS:其實,自從計算機出現之後,時間和空間就成了永恆的主題,因爲兩者在98%的情況下都無法協調,此長彼消;這個就是計算機科學中的根本瓶頸之所在。軟件科學和算法的發展,就看能不能突破這對時空權衡了。呵呵。。

何止計算機科學如此,整個宇宙又何嘗不是如此呢?最基本的宇宙之謎,還是時間和空間。

C++如何不用虛函數實現多態,可以考慮使用函數指針來實現多態

[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. #include<iostream>    
  2. using namespace std;    
  3.    
  4. typedef void (*fVoid)();    
  5.    
  6. class A    
  7. {    
  8. public:    
  9.     static void test()    
  10.     {    
  11.         printf("hello A\n");    
  12.     }    
  13.    
  14.     fVoid print;    
  15.    
  16.     A()    
  17.     {    
  18.         print = A::test;    
  19.     }    
  20. };    
  21.    
  22. class B : public A    
  23. {    
  24. public:    
  25.     static void test()    
  26.     {    
  27.         printf("hello B\n");    
  28.     }    
  29.    
  30.     B()    
  31.     {    
  32.         print = B::test;    
  33.     }    
  34. };    
  35.    
  36.    
  37. int main(void)    
  38. {    
  39.     A aa;    
  40.     aa.print();    
  41.    
  42.     B b;    
  43.     A* a = &b;    
  44.     a->print();    
  45.    
  46.     return 0;    
  47. }  

這樣做的好處主要是繞過了vtable。我們都知道虛函數表有時候會帶來一些性能損失。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章