每當創建一個包含有虛函數的類或從包含有虛函數的類派生一個類時,編譯器就會爲這個類創建一個虛函數表(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;
- }
而對於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()
- {
- }
- };
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指針(多繼承下可能不止一個)+編譯器額外增加的字節。
示例一:含有普通繼承
- 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=1
- cout<<"B="<<sizeof(B)<<endl; //result=8
- cout<<"C="<<sizeof(C)<<endl; //result=8
- cout<<"D="<<sizeof(D)<<endl; //result=12
- cout<<"E="<<sizeof(E)<<endl; //result=20
- return 0;
- }
求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;
- };
- class Derived size(32):
- +---
- | +--- (base class Base1)
- | | {vfptr}
- | | {vbptr}
- | | b1
- | +---
- | +--- (base class Base2)
- | | {vfptr}
- | | {vbptr}
- | | b2
- | +---
- | d
- +---
- +--- (virtual base CommonBase)
- | co
- +---
- 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;
- }
B's size is 16
說明:對於虛繼承,類B因爲有自己的虛函數,所以它本身有一個虛指針,指向自己的虛表。另外,類B虛繼承類A時,首先要通過加入一個虛指針來指向父類A,然後還要包含父類A的所有內容。因此是4+4+8=16。