天天看點

C++(虛)繼承類的記憶體占用大小

首先,平時所聲明的類隻是一種類型定義,它本身是沒有大小可言的。 是以,如果用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