关于结构体占用空间大小总结

关于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)以后,那样的理解就无法分析了。post

真实的状况是,数据占用内存的大小取决于数据自己的大小和其字节对齐方式,所谓对 齐方式即数据在内存中存储地址的起始偏移应该知足的一个条件。好比说,一个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指定的数值和这个数据成员自身长度中,比较小的那个进行。spa

二、结构(或联合)的总体对齐规则:在数据成员完成各自对齐以后,结构(或联合)自己也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。code

结合一、2推断:当#pragma pack的n值等于或超过全部数据成员长度的时候,这个n值的大小将不产生任何效果。blog

 

上面只是基本数据类型,比较简单,通常复杂的组合数据类型,好比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字节补齐。

 

这一点应该比较容易想到,由于类或结构体的静态成员变量是存储在全局/静态存储区的,而类或结构体是存储在栈上的,二者在内存占用上没有关系也是显而易见的。

相关文章
相关标签/搜索