C++类的大小计算汇总

  C++中类涉及到虚函数成员、静态成员、虚继承、多继承、空类等。

  类,作为一种类型定义,是没有大小可言的。

  类的大小,指的是类的对象所占的大小。因此,用sizeof对一个类型名操作,得到的是具有该类型实体的大小。

  • 类大小的计算,遵循结构体的对齐原则;
  • 类的大小,与普通数据成员有关,与成员函数和静态成员无关。即普通成员函数、静态成员函数、静态数据成员、静态常量数据成员,均对类的大小无影响;
  • 虚函数对类的大小有影响,是因为虚函数表指针带来的影响;
  • 虚继承对类的大小有影响,是因为虚基表指针带来的影响;
  • 静态数据成员之所以不计算在类的对象大小内,是因为类的静态数据成员被该类所有的对象所共享,并不属于具体哪个对象,静态数据成员定义在内存的全局区;
  • 空类的大小(类的大小为1),以及含有虚函数,虚继承,多继承是特殊情况;
  • 计算涉及到内置类型的大小,以下所述结果是在64位gcc编译器下得到(int大小为4,指针大小为8);

 

一、简单情况的计算

#include<iostream>
using namespace std;


class base
{
    public:
    base()=default;
    ~base()=default;
    private:
    static int a;
    int b;
    char c;

};


int main()
{
    base obj;
    cout<<sizeof(obj)<<endl;
}

   计算结果:8(静态变量a不计算在对象的大小内,由于字节对齐,结果为4+4=8)。

  

二、空类的大小

  C++的空类是指这个类不带任何数据,即类中没有非静态(non-static)数据成员变量,没有虚函数(virtual function),也没有虚基类(virtual base class)。

  直观地看,空类对象不使用任何空间,因为没有任何隶属对象的数据需要存储。然而,C++标准规定,凡是一个独立的(非附属)对象都必须具有非零大小。换句话说,c++空类的大小不为0 。

#include <iostream>
using namespace std;

class NoMembers
{
};

int main()
{
    NoMembers n;
    cout << sizeof(n) << endl;
}

  计算结果1。

  C++标准指出,不允许一个对象(当然包括类对象)的大小为0,不同的对象不能具有相同的地址。

  这是由于:

  • new需要分配不同的内存地址,不能分配内存大小为0的空间;
  • 避免除以 sizeof(T)时得到除以0错误;

  故使用一个字节来区分空类。

  但是,有两种情况值得我们注意

  第一种情况,空类的继承: 

  当派生类继承空类后,派生类如果有自己的数据成员,而空基类的一个字节并不会加到派生类中去。

  

class Empty {};
struct D : public Empty { int a;};

   sizeof(D)为4。

  第二种情况,一个类包含一个空类对象数据成员:

  

class Empty {};
class HoldsAnInt {
    int x;
    Empty e;
};

   sizeof(HoldsAnInt)为8。 

  在这种情况下,空类的1字节是会被计算进去的。而又由于字节对齐的原则,所以结果为4+4=8。

  继承空类的派生类,如果派生类也为空类,大小也都为1。 

  

三、含有虚函数成员的类

  虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。

  每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就会为这个类创建一个虚函数表(VTABLE)保存该类所有虚函数的地址,其实这个VTABLE的作用就是保存自己类中所有虚函数的地址,可以把VTABLE形象地看成一个函数指针数组,这个数组的每个元素存放的就是虚函数的地址。在每个带有虚函数的类中,编译器秘密地置入一指针,称为vpointer(缩写为VPTR),指向这个对象的VTABLE。 当构造该派生类对象时,其成员VPTR被初始化指向该派生类的VTABLE。所以可以认为VTABLE是该类的所有对象共有的,在定义该类时被初始化;而VPTR则是每个类对象都有独立一份的,且在该类对象被构造时被初始化。

class Base {

public:

virtual void f() { cout << "Base::f" << endl; }

virtual void g() { cout << "Base::g" << endl; }

virtual void h() { cout << "Base::h" << endl; }

};

   当定一个Base类的实例b时,其b中成员的存放如下:  

  指向虚函数表的指针在对象b的最前面。

  虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符”\0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在Visual Studio下,这个值是NULL。而在linux下,如果这个值是1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。

  因为对象b中多了一个指向虚函数表的指针,而指针的sizeof是8,因此含有虚函数的类或实例最后的sizeof是实际的数据成员的sizeof加8。

class Base {

public: int a; virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } }
 

  sizeof(Base)为16(vptr指针的大小为8,又因为对象中还包含一个int变量,字节对齐得8+8=16)。

 

四、基类含有虚函数的继承

(1)在派生类中不对基类的虚函数进行覆盖,同时派生类中还拥有自己的虚函数,比如有如下的派生类:

class Derived: public Base
{

public:

virtual void f1() { cout << "Derived::f1" << endl; }

virtual void g1() { cout << "Derived::g1" << endl; }

virtual void h1() { cout << "Derived::h1" << endl; }

};

   基类和派生类的关系如下: 

  当定义一个Derived的对象d后,其成员的存放如下: 

  可以发现:

  1)虚函数按照其声明顺序放于表中。

  2)基类的虚函数在派生类的虚函数前面。

  此时基类和派生类的sizeof都是数据成员的大小+指针的大小8。

(2)在派生类中对基类的虚函数进行覆盖,假设有如下的派生类:

class Derived: public Base
{

public:

virtual void f() { cout << "Derived::f" << endl; }

virtual void g1() { cout << "Derived::g1" << endl; }

virtual void h1() { cout << "Derived::h1" << endl; }

};

  基类和派生类之间的关系:其中基类的虚函数f在派生类中被覆盖了。  

  当我们定义一个派生类对象d后,其d的成员存放为: 

  可以发现:

  1)覆盖的f()函数被放到了虚表中原来基类虚函数的位置;

  2)没有被覆盖的函数依旧;

  3)派生类的大小仍是基类和派生类的非静态数据成员的大小+一个vptr指针的大小;

Base *b = new Derive();
b->f();

  由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

(3)多继承:无虚函数覆盖 
  假设基类和派生类之间有如下关系: 

  对于派生类实例中的虚函数表,是下面这个样子: 

  可以看到:

  1) 每个基类都有自己的虚表;

  2) 派生类的成员函数被放到了第一个基类的表中(所谓第一个基类是按照声明顺序来判断的);

  由于每个基类都需要一个指针来指向其虚函数表,因此d的sizeof等于d的数据成员加上三个指针的大小。

(4)多重继承,含虚函数覆盖 :
  假设,基类和派生类又如下关系:派生类中覆盖了基类的虚函数f 。

  可以看见,三个基类虚函数表中的f()的位置被替换成了派生类的函数指针。这样,就可以任一静态类型的基类类来指向派生类,并调用派生类的f()了。

Derive d;

Base1 *b1 = &d;

Base2 *b2 = &d;

Base3 *b3 = &d;

b1->f(); //Derive::f()

b2->f(); //Derive::f()

b3->f(); //Derive::f()

b1->g(); //Base1::g()

b2->g(); //Base2::g()

b3->g(); //Base3::g()

   此情况派生类的大小也是类的所有非静态数据成员的大小+三个指针的大小。

#include<iostream>
using namespace std;


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=16      
    cout<<"C="<<sizeof(C)<<endl;    //result=16  
    cout<<"D="<<sizeof(D)<<endl;    //result=16  
    cout<<"E="<<sizeof(E)<<endl;    //result=32  
    return 0;  
} 

 

  结果分析: 

  1.A为空类,所以大小为1 ;

  2.B的大小为char数据成员大小+vptr指针大小。由于字节对齐,大小为8+8=16 ;

  3.C的大小为两个char数据成员大小+vptr指针大小。由于字节对齐,大小为8+8=16 ;

  4.D为多继承派生类,由于D有数据成员,所以继承空类A时,空类A的大小1字节并没有计入当中,D继承C,此情况D只需要一个vptr指针,所以大小为数据成员加一个指针大小。由于字节对齐,大小为8+8=16 ;

  5.E为多继承派生类,此情况为我们上面所讲的多重继承,含虚函数覆盖的情况。此时大小计算为数据成员的大小+2个基类虚函数表指针大小 ,考虑字节对齐,继承顺序B在先,B(8 + 1),然后是C(8+1+1),由于字节对齐,B得与C中最大值对齐,因此B+7变成16,再+C(10),得26,最后+E的其它成员+1,因为要整体对于最大值(8)对齐,因此补齐得32。(之前看到几篇博客这里解释得都有问题)

四.虚继承的情况

  对虚继承层次的对象的内存布局,在不同编译器实现有所区别。 

  在这里,只说一下在gcc编译器下,虚继承大小的计算。它在gcc下实现比较简单,不管是否虚继承,GCC都是将虚表指针在整个继承关系中共享的,不共享的是指向虚基类的指针。

class A {

    int a;
  virtual void myfuncA(){} }; class B:virtual public A{ virtual void myfunB(){} }; class C:virtual public A{ virtual void myfunC(){} }; class D:public B,public C{ virtual void myfunD(){} };

 

  sizeof(A)=16,sizeof(B)=24,sizeof(C)=24,sizeof(D)=48;

  A的大小为int大小加上虚表指针大小;

  B,C中由于是虚继承,因此大小为int大小加指向虚基类的指针的大小。B,C虽然加入了自己的虚函数,但是虚表指针是和基类共享的,因此不会有自己的虚表指针,他们两个共用虚基类A的虚表指针。D由于B,C都是虚继承,其大小等于B+C)。

 

posted @ 2019-01-13 18:15  冯耀耀  阅读(2325)  评论(0编辑  收藏  举报