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

249次阅读
一条评论

共计 2240 个字符,预计需要花费 6 分钟才能阅读完成。

为什么要字节对齐和填充

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

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

  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;

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

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

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 字节。

正文完
 0
评论(一条评论)
Blayd
2023-09-28 10:15:54 回复

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

 Windows  Chrome  美国加利福尼亚层峰网络