首先,平時所聲明的類隻是一種類型定義,它本身是沒有大小可言的。 是以,如果用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。我們都知道虛函數表有時候會帶來一些性能損失。
轉載位址:http://www.chepoo.com/c-virtual-class-mem.html