关于C/C++中结构体变量占用内存大小的问题,之前一直以为把这个问题搞清楚了,今天看到一道题,发现之前的想法完全是错误的。这道题是这样的:

在32位机器上,下面的代码中

class A
{
public:
int i;
union U
{
char buff[];
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[];
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个字节,从0开始偏移,所以下面的int是从15开始偏移
int x;//偏移量 0x15+1=16
}s1;
cout<<sizeof(s1)<<endl; //结果为20字节 struct
{
char a[]; //
int x; //偏移量 16字节
char b; //偏移量 21字节
}s2; //结果为21字节,按最大基本类型对齐,补充到24字节
cout<<sizeof(s2)<<endl; //结果为24字节 struct
{
char a[];
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[];
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个字节,从0开始偏移,所以下面的int是从13开始偏移
int x;//偏移量 0x13+2=14,不按整除4来偏移,按整除2来偏移
}s4;
cout<<sizeof(s4)<<endl; //结果为18字节 struct
{
char a[]; //
int x; //偏移量 14字节
char b; //偏移量 18字节
}s5; //结果为19字节,按2字节对齐,补充到20字节
cout<<sizeof(s5)<<endl; //结果为20字节 struct
{
char a[];
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个字节,从0开始偏移,所以下面的int是从15开始偏移
int x;//偏移量 0x15+1=16
}s1;
cout<<sizeof(s1)<<endl; //结果为20字节 struct ss1
{
char a[]; //
int x; //偏移量 16字节
char b; //偏移量 21字节
}s2; //结果为21字节,按最大基本类型对齐,补充到24字节
cout<<sizeof(s2)<<endl; //结果为24字节 struct ss2
{
char a[];
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个字节,从0开始偏移,所以下面的int是从15开始偏移
int x;//偏移量 0x15+1=16
}s1;
cout<<sizeof(s1)<<endl; //结果为20字节 struct ss1
{
char a[]; //
int x; //偏移量 16字节
char b; //偏移量 21字节
}s2; //结果为21字节,按最大基本类型对齐,补充到24字节
cout<<sizeof(s2)<<endl; //结果为24字节 struct ss2
{
char a[];
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[];
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[];
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[];
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字节补齐。

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

关于结构体占用空间大小总结(#pragma pack的使用)的更多相关文章

  1. 结构体(struct)大小

    结构体(struct)大小 本文参考链接:C语言结构体(struct)常见使用方法,链接中的实例代码经实践有几处不准确,本文在引用时已做更改 注意:在结构体定义时不能申请空间(除非是结构体变量),不可 ...

  2. sizeof()计算结构体的大小

    简要说明:结构体成员按照定义时的顺序依次存储在连续的内存空间,但是结构体的大小并不是简单的把所有成员大小相加,而是遵循一定的规则,需要考虑到系统在存储结构体变量时的地址对齐问题. 一.没有成员的结构体 ...

  3. C语言结构体占用空间内存大小解析

    结构体的数据类型的有点我们就不啰嗦了,直接来看相同数据结构体的几种书写的格式吧. 格式一: 01.struct tagPhone 02.{ 03.     char   A; 04.     int  ...

  4. 结构体的数据对齐 #pragma浅谈

    之前若是有人拿个结构体或者联合体问我这个结构占用了多少字节的内存,我一定觉得这个人有点low, 直到某某公司的一个实习招聘模拟题的出现,让我不得不重新审视这个问题, 该问题大致如下: typedef ...

  5. struct的成员对齐问题-结构体实际大小问题

    struct的成员对齐 注意:为了方便说明,等号左边是每个数据单独所占长度,右边是最终空间大小,以字节为单位. 一.什么时间存在对其问题:(32位机对齐方式是按照4字节对其的,以下所有试验都是在32位 ...

  6. 【2017-07-01】Linux应用开发工程师面试问题记录之二:关于结构体的大小及内存对齐问题

    Tencent后台服务器开发有一道题是计算一个结构体的sizeof的大小: struct strData { int m_Int; char m_Char; short m_Short; char m ...

  7. 【C语言】这种求结构体成员大小的方法,你可能需要了解一下~

    在C语言编程中,有时候需要知道某结构体中某成员的大小,比如使用堆内存来存储结构体中的某成员时,需要知道该成员的大小,才好确定所需申请的空间大小.求某结构体中某成员的大小,你会怎么做? 例子: type ...

  8. C语言结构体的内存对齐问题

    在C语言开发当中会遇到这样的情况: #include <stdio.h> struct test { int a; char b; }; int main(int argc, const ...

  9. #pragma pack(n)对齐格式

    #pragma pack(n)对齐格式 #pragma pack(n) 是预处理器用来指定对齐格式的指令,表示n对齐.当元素字节小于n时,要扩展到n:若元素字节大于n则占用其实际大小. struct ...

随机推荐

  1. java设计模式之工厂模式学习

    上周安排的写两篇设计模式的文章,结果一篇也没写,今天都给写了.回顾+反思.In this world he who stops ,won't get anything he wants! 工厂方法模式 ...

  2. [转]Shared——React Native与原生关系理解与对比

    零.关系理解 这个是我对RN和原生关系的理解.简单解释下这个图. RN js编写完业务代码后,通过react-native bundle命令,将代码分别编译成一个index.ios.bundle和in ...

  3. JavaScript AMD 与CMD的代码区别

    1:CMD 依赖就近 define(function(require,export) { var b =1; var a = require("../a"); a.dosometh ...

  4. 【转】46 个非常有用的 PHP 代码片段

    1. 发送 SMS 在开发 Web 或者移动应用的时候,经常会遇到需要发送 SMS 给用户,或者因为登录原因,或者是为了发送信息.下面的 PHP 代码就实现了发送 SMS 的功能. 为了使用任何的语言 ...

  5. 原生ajax与封装的ajax使用方法

    当我们不会写后端接口来测试ajax时,我们可以使用node环境创建一个本地服务器. 1.创建一个本地服务器可参考http://www.cnblogs.com/heyujun-/p/6793900.ht ...

  6. 02_zookeeper配置

    [zoo.cfg] * tickTime:用于计算的时间单元.比如session超时:N*tickTime * initLimit:用于集群,允许从节点连接并且同步到master节点的初始化连接时间, ...

  7. SQL Server ->> 高可用与灾难恢复(HADR)技术 -- AlwaysOn可用性组(理论篇)

    因为篇幅原因,AlwaysOn可用性组被拆成了两部分:理论部分和实战部分.而实战部分又被拆成了准备工作和AlwaysOn可用性组搭建. 三篇文章各自的链接: SQL Server ->> ...

  8. Oracle案例04——ORA-39700: database must be opened with UPGRADE option

    Oracle11.2.0.3数据库通过rman备份到Oracle11.2.0.4上做还原,报需要升级的错误,具体处理步骤如下: 一.错误信息 SQL> alter database open r ...

  9. Linux->ZooKeeper开机启动的俩种方式

    两种方式可以实现开机自启动 第一种:直接修改/etc/rc.d/rc.local文件 在/etc/rc.d/rc.local文件中需要输入两行, 其中export JAVA_HOME=/usr/jav ...

  10. Linux入门-2 VIM基础

    启动与退出 模式 进入插入模式 命令 删除.复制.粘贴 光标控制 查找与替换 EX模式 启动与退出 vim只启动vim vim <filename>打开文件,如果不存在则新建 模式 Nor ...