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、爲虛函數調用插入代碼,所有這些都是自動發生的,所以我們不必擔心這些。
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. class A  
  5. {  
  6. public:  
  7.     virtual void fun1()  
  8.     {  
  9.         cout << "A::fun1()" << endl;  
  10.     }  
  11.     virtual void fun2()  
  12.     {  
  13.         cout << "A::fun2()" << endl;  
  14.     }  
  15. };  
  16.   
  17. class B : public A  
  18. {  
  19. public:  
  20.     void fun1()  
  21.     {  
  22.         cout << "B::fun1()" << endl;  
  23.     }  
  24.     void fun2()  
  25.     {  
  26.         cout << "B::fun2()" << endl;  
  27.     }  
  28. };  
  29.   
  30. int main()  
  31. {  
  32.     A *pa = new B;  
  33.     pa->fun1();  
  34.     delete pa;  
  35.   
  36.     system("pause");   
  37.     return 0;  
  38. }  
      毫無疑問,調用了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指針。
     含有虛函數的對象在內存中的結構如下:
  1. class A  
  2. {  
  3. private:  
  4.     int a;  
  5.     int b;  
  6. public:  
  7.     virtual void fun0()  
  8.     {  
  9.         cout<<"A::fun0"<<endl;  
  10.     }  
  11. };  

1、直接繼承
那我們來看看編譯器是怎麼建立VPTR指向的這個虛函數表的,先看下面兩個類:
  1. class base  
  2. {  
  3. private:  
  4.     int a;  
  5. public:  
  6.     void bfun()  
  7.     {  
  8.     }  
  9.     virtual void vfun1()  
  10.     {  
  11.     }  
  12.     virtual void vfun2()  
  13.     {  
  14.     }  
  15. };  
  16.   
  17. class derived : public base  
  18. {  
  19. private:  
  20.     int b;  
  21. public:  
  22.     void dfun()  
  23.     {  
  24.     }  
  25.     virtual void vfun1()  
  26.     {  
  27.     }  
  28.     virtual void vfun3()  
  29.     {  
  30.     }  
  31. };  
兩個類的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,對象就"知道"它自己是什麼類型。但只有當虛函數被調用時這種自我認知纔有用。    
2、虛繼承
     這個是比較不好理解的,對於虛繼承,若派生類有自己的虛函數,則它本身需要有一個虛指針,指向自己的虛表。另外,派生類虛繼承父類時,首先要通過加入一個虛指針來指向父類,因此有可能會有兩個虛指針。
二、(虛)繼承類的內存佔用大小
     首先,平時所聲明的類只是一種類型定義,它本身是沒有大小可言的。 因此,如果用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。
示例二:含有虛繼承
  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,其在內存中分佈的情況如下:
  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:
  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。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章