C++中虛函數工作原理和(虛)繼承類的記憶體佔用大小計算

來源:互聯網
上載者:User
一、虛函數的工作原理
      每當建立一個包含有虛函數的類或從包含有虛函數的類派生一個類時,編譯器就會為這個類建立一個虛函數表(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指標。
     含有虛函數的對象在記憶體中的結構如下:

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。
轉自:http://blog.csdn.net/hackbuteer1/article/details/7883531一、虛函數的工作原理
      每當建立一個包含有虛函數的類或從包含有虛函數的類派生一個類時,編譯器就會為這個類建立一個虛函數表(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指標。
     含有虛函數的對象在記憶體中的結構如下:

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。
轉自:http://blog.csdn.net/hackbuteer1/article/details/7883531

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.