C++类的大小

最后更新于 2023-02-22 6085 次阅读


C++中类所占的大小计算并没有想象中那么简单,因为涉及到虚函数成员,静态成员,虚继承,多继承以及空类等,不同情况有对应的计算方式,在此对各种情况进行总结。

首先要明确一个概念,平时所声明的类只是一种类型定义,它本身是没有大小可言的。 我们这里指的类的大小,其实指的是类的对象所占的大小。因此,如果用sizeof运算符对一个类型名操作,得到的是具有该类型实体的大小。

关于类/对象大小的计算

  • 类大小的计算遵循结构体的对齐原则
  • 类的大小与普通数据成员有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响
  • 虚函数对类的大小有影响,是因为虚函数表指针带来的影响
  • 虚继承对类的大小有影响,是因为虚基表指针带来的影响
  • 空类的大小是一个特殊情况,空类的大小为1。

结构体的对齐原则

为了访问速度和效率,需要各种类型数据按照一定的规则在空间上排列;不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。
于是有了字节对齐,4个字节是一个自然对齐
为什么是4个字节?
32位机,即计算机数据总线宽度为32个,一次可以处理32位bit(即4个字节)64位机,就是8字节;

struct MyStruct
{
	char a;              
	//偏移量为0,满足对齐方式,a占用1个字节,(最大类型字节数为8,占8字节);
	double b;            
	//下一个可用的地址的偏移量为1,不是sizeof(double)=8的倍数,需要补足7个字节才能使偏移量变为8(满足对齐方式),因此自动填充7个字节,b存放在偏移量为8的地址上,它占用8个字节。
	int c;             
	//下一个可用的地址的偏移量为16,是sizeof(int)=4的倍数,满足int的对齐方式,所以不需要VC自动填充,c存放在偏移量为16的地址上,它占用4个字节。
};
//所有成员变量都分配了空间,空间总的大小为1+7+8+4=20,不是结构的节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,
//所以需要填充4个字节,以满足结构的大小为sizeof(double)=8的倍数,24字节大小

class Base
{
public:
	Base() {};
	~Base() {};

private:
	static int a;
	int b;
	char c;
};

//8
//因为static不计入对象大小 按照内存对齐规则为8

空类的大小

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

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

class Base
{
};

Base base;
cout << sizeof(base) << 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;
};

//8
//因为在这种情况下,空类的1字节是会被计算进去的。而又由于字节对齐的原则,所以结果为4+4=8。
继承空类的派生类,如果派生类也为空类,大小也都为1

含有虚函数的类

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

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中多了一个指向虚函数表的指针,而指针的sizeof是8,因此含有虚函数的类或实例最后的sizeof是实际的数据成员的sizeof加8。(即如果类中存在虚函数,则会多出一个指向虚函数表的指针计入)

例子

#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=24
    cout<<"E="<<sizeof(E)<<endl;    //result=40
    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指针,所以大小为数据成员加一个指针大小。由于字节对齐,大小为16+8=24
  5. E为多继承派生类,此情况为我们上面所讲的多重继承,含虚函数覆盖的情况。此时大小计算为基类大小加本地数据,考虑字节对齐,结果为16+16+8=40