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;
}
- A为空类,所以大小为1
- B的大小为char数据成员大小+vptr指针大小。由于字节对齐,大小为8+8=16
- C的大小为两个char数据成员大小+vptr指针大小。由于字节对齐,大小为8+8=16
- D为多继承派生类,由于D有数据成员,所以继承空类A时,空类A的大小1字节并没有计入当中,D继承C,此情况D只需要一个vptr指针,所以大小为数据成员加一个指针大小。由于字节对齐,大小为16+8=24
- E为多继承派生类,此情况为我们上面所讲的多重继承,含虚函数覆盖的情况。此时大小计算为基类大小加本地数据,考虑字节对齐,结果为16+16+8=40