/ 其他 / 156浏览

【C】如何计算结构体占用内存大小

为什么要字节对齐和填充

结构体中的变量在内存中的地址在大多数情况下并不会是连续的,而是在变量首地址中间有一些空余。这是编译器为了提高对数据访问的效率而刻意为之的。

这个问题可以被分解成两个子问题:

  1. 为什么要字节对齐?
  2. 为什么结构体要在变量首地址之间“填充“空隙?

首先来回答第一个问题,CPU从内存中取数据的时候是以4字节一组来取的(不同的硬件可能有差异,也可能是8字节,为了简单这里就按4字节解释),假设有一个变量存在地址为1的地址,那么CPU拿到地址为0的4个字节后还需要把地址0存放的数据扔掉,这样一来就增加了使用的CPU周期,当然也就比较低效了。

至于第二个问题,这还跟缓存机制有关,正确填充的结构体有利于减少cache miss和cache flush、降低总线传输时间、降低数据竞争风险等。

总之,如果一个结构体没有正确对齐和填充,对它的访问和操作会变得比较低效。所以编译器会填充空隙,以空间来换取时间。当然,如果就是要省空间,那也有对应的办法,这个后边会介绍。另外,这也是在嵌入式编程中通常禁止或者不推荐使用malloc函数的原因之一,因为大多数嵌入式MCU并没有内存管理单元。由于字节对齐的现象,很容易产生内存碎片,从而导致可利用的内存越来越少,最后系统崩溃。

结构体到底在内存中如何存放

开门见山,其存放规则如下:

  1. 结构体成员的首地址要是其所占空间的整数倍
  2. 结构体的总体大小要是占用空间最大成员所占地址的整数倍
  3. 结构体的首地址要是占用空间最大成员所占地址的整数倍

下面对这几条规则做个解释,第一条很好理解,字节对齐。比如一个int变量在32位系统上占4字节,double占8字节,那么它们的存放地址就要是4/8的整倍数。第二条,主要计算完最后一个成员地址后,要看计算的大小是不是占用地址最大的成员所占空间的整倍数。例如:

struct Example1 {
    char a;
    int b;
    double c;
};

计算步骤如下:

  1. char a占1个字节,首地址0,0%1==0 不需要填充,已分配大小: 1字节
  2. int b 占4个字节,首地址1,1%4!=0 需要填充3,已分配大小: 8字节
  3. double c占8个字节,首地址8,8%8==0 不需要填充,已分配大小 :16字节
  4. 最后一个成员计算完毕,已分配大小16字节,是最大成员(double)的整倍数,不用填充, 总大小:16字节

另外需要注意的是,成员声明的顺序也会影响最后的大小:

struct Example2 {
    char a;
    int b;
    char c;
    double d;
};
struct Example3 {
    char a;
    char b;
    int c;
    double d;
};

Example2Example3分别占24字节和16字节,这里给出Example3的计算过程。

Example3:

  1. char a占1个字节,首地址0,0%1==0 不需要填充,已分配大小: 1字节
  2. char b占1个字节,首地址1,1%1==0 不需要填充,已分配大小:2字节
  3. int c 占4个字节,首地址2,2%4!=0 需要填充2,已分配大小: 8字节
  4. double d占8个字节,首地址8,8%8==0 不需要填充,已分配大小 :16字节
  5. 最后一个成员计算完毕,已分配大小16字节,是最大成员(double)的整倍数,不用填充, 总大小:16字节

再来看一个稍复杂的例子:

struct Example6 {
    int a;
    char * b;
    short c;
    char d[2];
    short e[4];
}sE6;

其中char *b是指向字符类型的指针,在32位系统上占4个字节。而计算数组成员的起始地址时要按照数组存放的类型来定,而不是数组所占的整体大小,比如这里在计算完d后的大小是12字节,这里并不用填充至8的倍数(e这个数组占连续的8字节)。来使用VS来验证下:

sE6.a = 0xaaaaaaaa;
sE6.b = (char*)0xbbbbbbbb;
sE6.c = 0xcccc;
sE6.d[0] = 0xd0;
sE6.d[1] = 0xd1;
sE6.e[0] = 0xe0e0;
sE6.e[3] = 0xe3e3;

关于开头提到的存放规则的第三点,结构体的首地址要是占用空间最大成员所占地址的整数倍。这个当结构体存在嵌套时会用到。计算时候不能把先嵌套的结构体当成一个成员变量计算,而是要使用第三条规则。

typedef Example2 sE2;
struct Example4 {
    char a;
    sE2 b;
    int c;
    double d;
};

Example4:

  1. char a占1个字节,首地址0,0%1==0 不需要填充,已分配大小: 1字节
  2. struct Example2占24字节,首地址1,struct Example2 的最大变量占8字节,1%8!=0 需要填充7字节(而不是23字节),已分配大小:32字节
  3. int b 占4个字节,首地址32,32%4==0 不需要填充,已分配大小:36字节
  4. double c占8个字节,首地址36,36%8!=0 需要填充4字节,已分配大小 :48字节
  5. 最后一个成员计算完毕,已分配大小48字节,是最大成员(sizeof(double)=8不是sizeof(struct Example2)=24)的整倍数,无需填充, 总大小:48字节

宏 字节对齐

你可以使用#pragma pack()来自定义字节对齐数。括号内填入你期望的数字,不填即为默认方式。

例如当你使用了#pragma pack(1)时,此时完全没有填充和对齐,Example4占用空间为28字节。

当使用#pragma pack(4)时,Example4占用空间为40字节。

Eysent
修复WordPress错误 – 打开错误日志方法并排查问题
修复WordPress错误 – 打开错误日志方法并排查问题
【C#】解决因Windows缩放导致的截图错位和鼠标位置错误
【C#】解决因Windows缩放导致的截图错位和鼠标位置错误
【Python】回溯递归法一键解决数独谜题
【Python】回溯递归法一键解决数独谜题
【C#】C#实现单片机上位机,串口通信示例
【C#】C#实现单片机上位机,串口通信示例
【CE】某网盘加速下载方法,无需破解或其他下载器
【CE】某网盘加速下载方法,无需破解或其他下载器
【HTML/CSS/JS】网页实现猫猫眼睛跟随鼠标转动,可用Wallpaer Engine当作壁纸
【HTML/CSS/JS】网页实现猫猫眼睛跟随鼠标转动,可用Wallpaer Engine当作壁纸

1

  1. Blayd

    博主讲的很好,速更,夜不能寐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注