当前位置: 首页 > 网站开发 > 正文

C++(虚)继承类的内存占用大小

关键字:
1 星2 星3 星4 星5 星 (2 次投票, 评分: 5.00, 总分: 5)
Loading ... Loading ...
baidu_share

首先,平时所声明的类只是一种类型定义,它本身是没有大小可言的。 因此,如果用sizeof运算符对一个类型名操作,那得到的是具有该类型实体的大小。

计算一个类对象的大小时的规律:

1、空类、单一继承的空类、多重继承的空类所占空间大小为:1(字节,下同);

2、一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间的;

3、因此一个对象的大小≥所有非静态成员大小的总和;

4、当类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针vPtr指向虚函数表VTable;

5、虚承继的情况:由于涉及到虚函数表和虚基表,会同时增加一个(多重虚继承下对应多个)vfPtr指针指向虚函数表vfTable和一个vbPtr指针指向虚基表vbTable,这两者所占的空间大小为:8(或8乘以多继承时父类的个数);

6、在考虑以上内容所占空间的大小时,还要注意编译器下的“补齐”padding的影响,即编译器会插入多余的字节补齐;

7、类对象的大小=各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和+ vfptr指针(多继承下可能不止一个)+vbptr指针(多继承下可能不止一个)+编译器额外增加的字节。

示例一:含有普通继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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。

示例二:含有虚继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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,其在内存中分布的情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Derived size(32):  
     +---  
     | +--- (base class Base1)  
 | | {vfptr}  
 | | {vbptr}  
 | | b1  
     | +---  
     | +--- (base class Base2)  
 | | {vfptr}  
 | | {vbptr}  
 | | b2  
    | +---  
 | d  
    +---  
    +--- (virtual base CommonBase)  
 | co  
    +---

示例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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++如何不用虚函数实现多态
可以考虑使用函数指针来实现多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#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 | IT技术精华网

C++(虚)继承类的内存占用大小:等您坐沙发呢!

发表评论