首先,平時所聲明的類只是一種類型定義,它本身是沒有大小可言的。 因此,如果用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;
- }
前面三個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)、按照函數名稱查表,這種方案可以避免如上的問題;但是由於要比較名稱,有時候要遍歷所有的繼承結構,時間效率性能不是很高。
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。我們都知道虛函數表有時候會帶來一些性能損失。