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

来源:互联网 发布:网页设计程序员工资 编辑:程序博客网 时间:2024/06/05 01:54
(虚)继承类的内存占用大小
     首先,平时所声明的类只是一种类型定义,它本身是没有大小可言的。 因此,如果用sizeof运算符对一个类型名操作,那得到的是具有该类型实体的大小。
计算一个类对象的大小时的规律:
    1、空类、单一继承的空类、多重继承的空类所占空间大小为:1(字节,下同);
    2、一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间的;
    3、因此一个对象的大小≥所有非静态成员大小的总和; 
    4、当类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针vPtr指向虚函数表VTable
    5、虚承继的情况:由于涉及到虚函数表和虚基表,会同时增加一个(多重虚继承下对应多个)vfPtr指针指向虚函数表vfTable和一个vbPtr指针指向虚基表vbTable,这两者所占的空间大小为:8(或8乘以多继承时父类的个数);
    6、在考虑以上内容所占空间的大小时,还要注意编译器下的“补齐”padding的影响,即编译器会插入多余的字节补齐;

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



示例一:含有普通继承

[cpp] view plain copy
  1. class A     
  2. {     
  3. };    
  4.   
  5. class B     
  6. {  
  7.     char ch;     
  8.     virtual void func0()  {  }   
  9. };   
  10.   
  11. class C    
  12. {  
  13.     char ch1;  
  14.     char ch2;  
  15.     virtual void func()  {  }    
  16.     virtual void func1()  {  }   
  17. };  
  18.   
  19. class D: public A, public C  
  20. {     
  21.     int d;     
  22.     virtual void func()  {  }   
  23.     virtual void func1()  {  }  
  24. };     
  25.   
  26. class E: public B, public C  
  27. {     
  28.     int e;     
  29.     virtual void func0()  {  }   
  30.     virtual void func1()  {  }  
  31. };  
  32.   
  33. int main(void)  
  34. {  
  35.     cout<<"A="<<sizeof(A)<<endl;    //result=1  
  36.     cout<<"B="<<sizeof(B)<<endl;    //result=8      
  37.     cout<<"C="<<sizeof(C)<<endl;    //result=8  
  38.     cout<<"D="<<sizeof(D)<<endl;    //result=12  
  39.     cout<<"E="<<sizeof(E)<<endl;    //result=20  
  40.     return 0;  
  41. }  
前面三个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。
示例二:含有虚继承
[cpp] view plain copy
  1. class CommonBase  
  2. {  
  3.     int co;  
  4. };  
  5.   
  6. class Base1: virtual public CommonBase  
  7. {  
  8. public:  
  9.     virtual void print1() {  }  
  10.     virtual void print2() {  }  
  11. private:  
  12.     int b1;  
  13. };  
  14.   
  15. class Base2: virtual public CommonBase  
  16. {  
  17. public:  
  18.     virtual void dump1() {  }  
  19.     virtual void dump2() {  }  
  20. private:  
  21.     int b2;  
  22. };  
  23.   
  24. class Derived: public Base1, public Base2  
  25. {  
  26. public:  
  27.     void print2() {  }  
  28.     void dump2() {  }  
  29. private:  
  30.     int d;  
  31. };  
sizeof(Derived)=32,其在内存中分布的情况如下:
[cpp] view plain copy
  1. class Derived size(32):  
  2.      +---  
  3.      | +--- (base class Base1)  
  4.  | | {vfptr}  
  5.  | | {vbptr}  
  6.  | | b1  
  7.      | +---  
  8.      | +--- (base class Base2)  
  9.  | | {vfptr}  
  10.  | | {vbptr}  
  11.  | | b2  
  12.     | +---  
  13.  | d  
  14.     +---  
  15.     +--- (virtual base CommonBase)  
  16.  | co  
  17.     +---  
示例3:
[cpp] view plain copy
  1. class A  
  2. {  
  3. public:  
  4.     virtual void aa() {  }  
  5.     virtual void aa2() {  }  
  6. private:  
  7.     char ch[3];  
  8. };  
  9.   
  10. class B: virtual public A  
  11. {  
  12. public:  
  13.     virtual void bb() {  }  
  14.     virtual void bb2() {  }  
  15. };  
  16.   
  17. int main(void)  
  18. {  
  19.     cout<<"A's size is "<<sizeof(A)<<endl;  
  20.     cout<<"B's size is "<<sizeof(B)<<endl;  
  21.     return 0;  
  22. }  
执行结果: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)、按照函数名称查表,这种方案可以避免如上的问题;但是由于要比较名称,有时候要遍历所有的继承结构,时间效率性能不是很高。(关于MFC的消息映射的实现,看下一篇文章)
3、总结:
如果继承体系的基类的virtual成员不多,而且在派生类要重写的部分占了其中的大多数时候,用C++的虚函数机制是比较好的;
但是如果继承体系的基类的virtual成员很多,或者是继承体系比较庞大的时候,而且派生类中需要重写的部分比较少,那就用名称查找表,这样效率会高一些,很多的GUI库都是这样的,比如MFC,QT。
PS:其实,自从计算机出现之后,时间和空间就成了永恒的主题,因为两者在98%的情况下都无法协调,此长彼消;这个就是计算机科学中的根本瓶颈之所在。软件科学和算法的发展,就看能不能突破这对时空权衡了。呵呵。。
何止计算机科学如此,整个宇宙又何尝不是如此呢?最基本的宇宙之谜,还是时间和空间。

C++如何不用虚函数实现多态 
可以考虑使用函数指针来实现多态
[cpp] view plain copy
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. typedef void (*fVoid)();  
  5.   
  6. class A  
  7. {  
  8. public:  
  9.     static void test()  
  10.     {  
  11.         printf("hello A\n");  
  12.     }  
  13.   
  14.     fVoid print;  
  15.   
  16.     A()  
  17.     {  
  18.         print = A::test;  
  19.     }  
  20. };  
  21.   
  22. class B : public A  
  23. {  
  24. public:  
  25.     static void test()  
  26.     {  
  27.         printf("hello B\n");  
  28.     }  
  29.   
  30.     B()  
  31.     {  
  32.         print = B::test;  
  33.     }  
  34. };  
  35.   
  36.   
  37. int main(void)  
  38. {  
  39.     A aa;  
  40.     aa.print();  
  41.   
  42.     B b;  
  43.     A* a = &b;  
  44.     a->print();  
  45.   
  46.     return 0;  
  47. }

阅读全文
1 0
原创粉丝点击