关于C/C++中结构体变量占用内存大小的问题,之前一直以为把这个问题搞清楚了,今天看到一道题,发现之前的想法完全是错误的。这道题是这样的:
在32位机器上,下面的代码中
class A
{
public:
int i;
union U
{
char buff[13];
int i;
}u;
void foo(){}
typedef char* (*f)(void*);
enum{red , green, blue}color;
}a;
sizeof(a)的值是多少?如果在代码前面加上#pragma pack(2)呢?
我之前一直有的一个错误的观念是,编译器会将某些大小不足4字节的数据类型合并起来处理。虽然很多情况下效果也是这样的,但是,这样理解是没有把握到问题的本质,在某些情况下就会出错,比如带上#pragma pack(2)之后,那样的理解就没法分析了。
真实的情况是,数据占用内存的大小取决于数据本身的大小和其字节对齐方式,所谓对齐方式即数据在内存中存储地址的起始偏移应该满足的一个条件。比如说,一个int数据,在32位机上(以下的讨论都以此为基础)占用4个字节,如果该数据的偏移是0x00000003,那么CPU就要先取一个char,再取一个short,最后取一个char,三次取数据组合成一个int类型。(为什么不能取一次char,然后再取一个3字节长的数据呢?这个问题从组成原理的角度考虑。32位机器上有4个32位的通用数据寄存器:EAX,EBX,ECX,EDX。每个通用寄存器的低16位又可以单独使用,叫做AX,BX,CX,DX。最后,这四个16位寄存器又可以分成8个独立的8位寄存器:AH、AL等。因此,CPU取数据时或者是一个字节AH或者AL等,或者是两个字节AX,BX等,或者是4个字节EAX,EBX等,而没法一次取三个字节的数据。)如果该数据的偏移是0x00000002,那么CPU就可以先取一个short,然后再取一个short,两次取值完成一个int型数据的组合。但是如果偏移是0x00000004,正好是4字节对齐的,那么CPU就可以一次取出这个int类型的数据。所以,为了提高取值速度,一般编译器都会优化数据对齐方式。优化的标准是什么呢?大小不同的各种基本数据类型的数据该怎么对齐呢?下面的表格作出了总结:
基本数据类型 | 占用内存大小(字节) | 字节对齐方式(首地址偏移) |
double / long long | 8 | 8 |
int / long | 4 | 4 |
float | 4 | 4 |
short | 2 | 2 |
char | 1 | 1 |
其中,字节对齐方式(首地址偏移),表示的是该类型的数据的首地址,应该是该类型的字节数的倍数。当然,这是在默认的情况下,如果用#pragma pack(n) 重定义了字节对齐方式,那么情况就有点复杂了。一般来说,如果定义#pragma pack(n),而按照数据类型得到的对齐方式比n的倍数大,那就按照n的倍数指定的方式来对齐(这体现了开发者可以选择不使用推荐的对齐方式以获得内存较大的利用率);如果按照数据类型得到的对齐方式比n小,那就按照前者指定的方式来对齐(一般如果不指定对齐方式时,编译器设定的对齐方式会比基本类型的对齐方式大)。下面具体到不同类型的大小时,会举一些例子。现在,只要记住这两条规律就可以了。
这时,对齐规则为:
1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
结合1、2推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。
上面只是基本数据类型,比较简单,一般复杂的组合数据类型,比如enum(枚举)、Union(联合)、struct(结构体)、class(类)。一个个来。
数组,数组是第一个元素对齐,以后的各个元素就对齐了。
enum,枚举类型,一般来说大小为4字节,因为4个字节能够枚举4294967296个变量,大小足够了。如果不够,可能会扩充,扩充到多大没试过。
如上图所示。右边是输出,之前的输出不用管它。
Union,联合类型。联合类型的大小是最长的分量的长度,加上补齐的字节。这里容易有一个谬误,有人说补齐的字节是将联合类型的长度补齐为各分量基本类型的倍数,这个说法在默认的字节对齐(4字节或8字节)中没问题,但是当修改对齐方式之后就有问题了。先看一下默认的情况。
union t
{
char buff[13];
int i;
}t;
上述定义的联合体,在默认的字节对齐方式中,大小为16字节。首先计算得到联合最长的分量长度是sizeof(char)*13=13字节。但是13不是sizeof(int)的倍数,所以将13扩充至16,最终得到sizeof(t)=16字节。
这是在默认情况下,扩充后的大小是各分量基本类型大小的倍数。但是,如果指定对齐方式为#pragma pack(2),那情况就不一样了。此时得到的最长分量还是13字节,不过扩充时不是按照4字节的倍数来算,而是按照2的倍数(pragma pack指定的)来算。最终得到大小为14字节。
Union联合体还是比较简单的,因为不牵涉到各分量的起始偏移地址对齐的问题。下面来看看struct结构体。首先要注意的是,struct和class在C++中其实是一样的,struct也可以有构造函数,析构函数,成员函数和(private、protected、public)继承。两者的区别在于class默认的成员类型是private,而struct为public。class默认的继承方式为private,而struct为public。其实核心是struct是数据聚集起来,便于人访问,所以默认的是public,而class是封装,不让人访问,所以是private。
其次要注意的是struct或class中定义的成员函数和构造和析构函数不占整体的空间。如果有虚函数的话,会有4个字节的地址存放虚函数表的地址。
由于struct和class的相同,所以下面都已struct为例进行讨论。
struct占用内存大小的计算有两点,第一点是各个分量的偏移地址的计算,第二点是最终整体大小要进行字节对齐。
struct{
char a[15]; //占15个字节,从0开始偏移,所以下面的int是从15开始偏移
int x;//偏移量 0x15+1=16
}s1;
cout<<sizeof(s1)<<endl; //结果为20字节
struct
{
char a[15]; //
int x; //偏移量 16字节
char b; //偏移量 21字节
}s2; //结果为21字节,按最大基本类型对齐,补充到24字节
cout<<sizeof(s2)<<endl; //结果为24字节
struct
{
char a[15];
int x; //偏移量 16字节
double b; //偏移量 24字节
}s3;//
cout<<sizeof(s3)<<endl; //结果为32字节
上面几个例子的说明。以s3为例。首先,从偏移量为0的地方开始放char,连续放15个,每个占1字节。则int x对应的偏移量是第15个字节,按照上面表格的说明,int类型的偏移量应该能够整除int类型的大小,所以编译器填充1个字节,使int x从第16个字节开始放置。x占4个字节,所以double b的偏移量是第20个字节,同理,20不能整除8(double类型的大小),所以编译器填充4字节到第24个字节,即double b从第24个字节开始放置。最终结果为15+1+4+4+8=32字节。其他的类型同此分析。
不过,上面这个例子还不够明显,再举一个需要最后补充字节的例子。
struct
{
char a[15];
int x; //偏移量 16字节
double b; //偏移量 24字节
char c;//偏移量 32字节
}s3;//共33字节,按最大基本类型对齐,补充到40字节(整除8)
cout<<sizeof(s3)<<endl; //结果为40字节
上面的例子中,最后多了一个char型数据。导致最后得出的大小是33字节,这个大小不能够整除结构体中基本数据类型最大的double,所以要按能整除sizeof(double)来补齐,最终得到40字节。
也即,凡计算struct这种结构体的大小,都分两步:第一,各个分量的偏移;第二,最后的补齐。
下面来看看如果主动设定对齐方式会如何:
#pragma pack(push)
#pragma pack(2)
struct{
char a[13]; //占13个字节,从0开始偏移,所以下面的int是从13开始偏移
int x;//偏移量 0x13+2=14,不按整除4来偏移,按整除2来偏移
}s4;
cout<<sizeof(s4)<<endl; //结果为18字节
struct
{
char a[13]; //
int x; //偏移量 14字节
char b; //偏移量 18字节
}s5; //结果为19字节,按2字节对齐,补充到20字节
cout<<sizeof(s5)<<endl; //结果为20字节
struct
{
char a[13];
int x; //偏移量 14字节
double b; //偏移量 18字节
char c;//偏移量 26字节
}s6;//共27字节,按2字节对齐,补充到28字节(整除8)
cout<<sizeof(s6)<<endl; //结果为28字节
#pragma pack(pop)
上面的代码分析跟之前是一样的,只不过每次改变了对齐方式,结果如注释所云。注意,跟之前的例子相比,为了体现效果,char型数组大小改为13了。
上面提到的对齐方式,也符合之前说到对#pragma pack(n)的两条规律。
如果#pragma pack(1)那结果如何,那就没有对齐了,直接将各个分量相加就是结构体的大小了。
上面的分析,可以应付enum、union、struct(或class)各种单独出现的情况了。下面再看看组合的情况。
struct ss0{
char a[15]; //占15个字节,从0开始偏移,所以下面的int是从15开始偏移
int x;//偏移量 0x15+1=16
}s1;
cout<<sizeof(s1)<<endl; //结果为20字节
struct ss1
{
char a[15]; //
int x; //偏移量 16字节
char b; //偏移量 21字节
}s2; //结果为21字节,按最大基本类型对齐,补充到24字节
cout<<sizeof(s2)<<endl; //结果为24字节
struct ss2
{
char a[15];
int x; //偏移量 16字节
double b; //偏移量 24字节
char c;//偏移量 32字节
}s3;//共33字节,按最大基本类型对齐,补充到40字节(整除8)
cout<<sizeof(s3)<<endl; //结果为40字节
struct
{
char a; //偏移0,1字节
struct ss0 b;//偏移1+3=4,20字节
char f;//偏移24, 1字节
struct ss1 c;//偏移25+3,24字节
char g;//偏移52,1字节
struct ss2 d;//偏移53+3,40字节
char e;//偏移96,1字节
}s7;//共97字节,不能整除sizeof(double),所以补充到104字节
cout<<"here:"<<sizeof(s7)<<endl;
组合起来比较复杂。不过也有原则可循。首先,作为成员变量的结构体的偏移量必须是自己最大成员类型字节长度的整数倍。其次,整体的大小应该是结构体中最大基本类型成员的整数倍。结构体中字节数最大的基本数据类型,应该包括内部结构体的成员变量。根据这些原则,分析一下上面的结果。第一个struct ss0 b的大小之前已经算过,是20字节,其偏移量是1字节,因为strut ss0中最大的数据类型是int类型,故而strut ss0的偏移量应该能够整除sizeof(int)=4,所以偏移量为4。同理,可得strut ss1。然后是strut ss2,其偏移量是53字节,但是strut ss2最大的成员变量的double类型,故而其偏移量应该能够整除sizeof(double),补充为56字节。最后得到97字节的结构体,而struct s7 最大的成员变量是struct ss2中的double,所以struct s7应该按8字节对齐,故补充到能够整除8的104,所以结果就是104字节。
如果将struct ss2去掉,则struct s7中最大的数据类型就是int,最终结果就应该按sizeof(int)对齐。如下所示:
struct
{
char a; //偏移0,1字节
struct ss0 b;//偏移1+3=4,20字节
char f;//偏移24, 1字节
struct ss1 c;//偏移25+3,24字节
char g;//偏移52,1字节
//struct ss2 d;//偏移53+3,40字节
char e;//偏移53,1字节
}s7;//共54字节,不能整除sizeof(int),所以补充到56字节
cout<<"here:"<<sizeof(s7)<<endl;
上述结果是正确的,可知我们的分析是正确的。
如果将struct s7用#pragma pack(2)包围起来,其他的不变,可以推测,结果将是92字节,因为其内部各结构体成员也都不按自己内部最大的数据类型来偏移。代码如下,经测试,结果是正确的。
struct ss0{
char a[15]; //占15个字节,从0开始偏移,所以下面的int是从15开始偏移
int x;//偏移量 0x15+1=16
}s1;
cout<<sizeof(s1)<<endl; //结果为20字节
struct ss1
{
char a[15]; //
int x; //偏移量 16字节
char b; //偏移量 21字节
}s2; //结果为21字节,按最大基本类型对齐,补充到24字节
cout<<sizeof(s2)<<endl; //结果为24字节
struct ss2
{
char a[15];
int x; //偏移量 16字节
double b; //偏移量 24字节
char c;//偏移量 32字节
}s3;//共33字节,按最大基本类型对齐,补充到40字节(整除8)
cout<<sizeof(s3)<<endl; //结果为40字节
#pragma pack(push)
#pragma pack(2)
struct
{
char a; //偏移0,1字节
struct ss0 b;//偏移1+1=2,20字节
char f;//偏移22, 1字节
struct ss1 c;//偏移23+1,24字节
char g;//偏移48,1字节
struct ss2 d;//偏移49+1,40字节
char e;//偏移90,1字节
}s7;//共91字节,不能整除2,所以补充到92字节
cout<<"here:"<<sizeof(s7)<<endl;
#pragma pack(pop)
下面就可以来分析本文开头部分提出的那个变量了。再录入如下:
class A
{
public:
int i;
union U
{
char buff[13];
int i;
}u;
void foo(){}
typedef char* (*f)(void*);
enum{red , green, blue}color;
}a;
int i 的偏移是0,占据4个字节, union U u本身的大小是16字节,偏移是4,满足整除4字节的要求。(注意,这里刚好是偏移符合的情况,如果在int i后面定义一个char,则此处要按4字节对齐,需要补充3个字节。)color的大小是4字节,偏移量是20,满足整除sizeof(int)的要求,所以不用填充。如果color前面再定义一个char,则此处要补充到4字节对齐。综上,最终得到的A的大小是4+16+4=24字节。
如果加上参数#pragma pack(2),则union U u的大小编程14字节,最终得到class A的大小是22字节。
上面的例子不够过瘾,因为class A中出现的基本类型正好不超过int,下面看看这个例子。
struct A
{
public:
int i; //偏移0,4字节
//char c;
union U
{
char buff[13];
double i;
}u; //偏移4,不能整除sizeof(double),所以偏移需要补充到8,大小 16字节
void foo(){}
typedef char* (*f)(void*);
char d;//偏移24,大小1字节
enum{red , green, blue}color;//偏移25,补充到28,大小4字节
char e;//偏移32,大小1字节
}a;//大小33字节,不能整除sizeof(double),补充到40字节
上面的例子中,上面的例子既有内部偏移的对齐,又有最后的补齐。可见struct A补齐时需要对齐的是union U u的成员double i,所以最后是补充到了40字节。
当然,上面所有的分析都可以通过查看成员变量偏移位置的方法来判断。方法如下:
#define FIND(structTest,e) (size_t)&(((structTest*)0)->e)
struct A
{
public:
int i; //偏移0,4字节
//char c;
union U
{
char buff[13];
double i;
}u; //偏移4,不能整除sizeof(double),所以偏移需要补充到8,大小 16字节
void foo(){}
typedef char* (*f)(void*);
char d;//偏移24,大小1字节
enum{red , green, blue}color;//偏移25,补充到28,大小4字节
char e;//偏移32,大小1字节
}a;//大小33字节,不能整除sizeof(double),补充到40字节
//.........省略..........................
cout<<"i 的偏移:"<<FIND(A, i)<<endl;
cout<<"u 的偏移:"<<FIND(A, u)<<endl;
cout<<"color 的偏移:"<<FIND(A, color)<<endl;
FIND定义的宏即可用来查看成员变量的偏移情况。跟之前的分析是相符的。
最后补充一点,编译器默认的#pragma pack(n)中,n的值是有差异的,我上面测试的结果大多都在VC++和G++中测试过,结果相同。只有少部分示例没有在G++中测过。所以,主要的平台,以VC++为准。据说VC++默认采用的8字节对齐。不过,也不好验证,因为当结构体中最大为int类型时,根据前面的两条对齐准则,最终结果会按照int类型来对齐。当结构体中最大为double类型时,此时基本数据类型的对齐方式,与默认的8字节对齐方式相同,也看不出差异。既然如此,也就不用特意去纠结VC++中采用的是几字节对齐方式了。更多的精力应该放在思考怎么样组织结构体,才能使得空间利用效率最高,同时又有较高的访问效率。
补充:类或结构体的静态成员变量不占用结构体或类的空间,也就是说sizeof出来的大小跟静态成员变量的大小无关。在最后补齐字符的时候,也与静态成员变量无关。比如:
struct yy
{
char y1;
int y3;
char y2;
static double y4;
};
double yy::y4;
上述结构体的大小不包括是static double y4变量的空间。最后补齐也是按照4字节补齐,而不是按照8字节补齐。
这一点应该比较容易想到,因为类或结构体的静态成员变量是存储在全局/静态存储区的,而类或结构体是存储在栈上的,两者在内存占用上没有关系也是显而易见的。