C语言中sizeof struct内存对齐如何计算?

99ANYc3cd6
预计阅读时长 18 分钟
位置: 首页 C语言 正文

sizeof 用于计算一个类型或变量在内存中所占的字节数,对于结构体,sizeof 返回的是该结构体类型实例所占用的总内存大小

c语言 sizeof struct
(图片来源网络,侵删)

这个大小不等于其所有成员大小的简单相加,编译器为了提高内存访问效率,会在结构体成员之间插入填充字节,并在结构体末尾进行对齐填充,这个过程被称为内存对齐


为什么需要内存对齐?

内存对齐不是 C 语言语法的强制要求,而是大多数计算机体系结构(特别是 x86/x64 架构)的硬件特性决定的,主要原因有两点:

  1. 性能提升

    • 现代CPU在访问内存时,通常不是按字节访问,而是按字长进行访问,在 64 位系统上,CPU 一次会读取 64 位(8 字节)的数据。
    • 如果一个 int(4 字节)跨越了两个 8 字节的内存块,CPU 需要进行两次内存访问,然后将结果合并,效率极低。
    • 如果数据正确对齐,CPU 只需一次内存访问就能完整读入数据,速度更快。
  2. 硬件要求

    c语言 sizeof struct
    (图片来源网络,侵删)

    某些硬件架构(如某些 ARM 处理器)要求数据必须按特定方式对齐,否则直接导致硬件异常(程序崩溃)。


内存对齐规则(以 GCC/Clang 为例)

不同编译器和平台的具体对齐规则可能略有差异,但以下规则是普遍适用的:

  • 规则 1:结构体每个成员的起始地址必须能被其自身大小整除。

    • char (1 字节) 可以在任何地址。
    • short (2 字节) 必须从偶数地址开始(地址 % 2 == 0)。
    • int (4 字节) 必须能被 4 整除的地址开始(地址 % 4 == 0)。
    • double (8 字节) 必须能被 8 整除的地址开始(地址 % 8 == 0)。
    • 结构体指针(struct *)的对齐值等于其指向的结构体的对齐值。
  • 规则 2:结构体的总大小必须是其中“最大对齐要求”成员的整数倍。

    • “最大对齐要求”是指所有成员中,其自身大小最大的那个值。
    • 这个值也被称为该结构体的“对齐值”。

实例分析

让我们通过几个例子来直观地理解这个过程。

示例 1:简单的结构体

#include <stdio.h>
struct Simple {
    char c;      // 1 byte
    int i;       // 4 bytes
    short s;     // 2 bytes
};
int main() {
    printf("sizeof(char) = %zu\n", sizeof(char));   // 1
    printf("sizeof(int) = %zu\n", sizeof(int));     // 4
    printf("sizeof(short) = %zu\n", sizeof(short)); // 2
    printf("sizeof(struct Simple) = %zu\n", sizeof(struct Simple)); // ?
    return 0;
}

内存布局分析:

  1. 成员 char c; (1 字节)

    • 放在偏移量 0 处。
    • 当前占用:[0]
  2. 成员 int i; (4 字节)

    • 根据规则 1,int 必须从能被 4 整除的地址开始。
    • 上一个成员 c 占用了 0,下一个可用地址是 11 % 4 != 0,所以不能放。
    • 编译器会插入 3 个填充字节,让 i 从偏移量 4 开始。
    • 当前占用:[0] c, [1-3] 填充, [4-7] i
  3. 成员 short s; (2 字节)

    • 根据规则 1,short 必须从偶数地址开始。
    • 上一个成员 i 结束于 7,下一个可用地址是 88 % 2 == 0,满足条件。
    • s 可以直接放在偏移量 8 处。
    • 当前占用:... [8-9] s
  4. 计算总大小

    • 最后一个成员 s 结束于偏移量 9
    • 根据规则 2,结构体总大小必须是最大对齐要求(max(1, 4, 2) = 4)的整数倍。
    • 当前总大小为 10 字节(偏移量 0 到 9)。10 % 4 = 2,不是 4 的倍数。
    • 编译器在结构体末尾插入 2 个填充字节,使总大小变为 12 字节。12 % 4 == 0,满足条件。

最终内存布局:

偏移量 大小
0 c 1
1 (填充) 1
2 (填充) 1
3 (填充) 1
4 i (低地址) 4
8 s (低地址) 2
10 (填充) 2
12

运行结果:

sizeof(char) = 1
sizeof(int) = 4
sizeof(short) = 2
sizeof(struct Simple) = 12

验证: 1 + 3 + 4 + 2 + 2 = 12,可以看到,sizeof(struct Simple) (12) 并不等于 sizeof(c) + sizeof(i) + sizeof(s) (1 + 4 + 2 = 7)。


示例 2:优化结构体(减少内存占用)

成员的声明顺序会严重影响结构体的内存占用,将小成员放在一起,可以减少填充。

#include <stdio.h>
struct Optimized {
    char c1;     // 1 byte
    char c2;     // 1 byte
    short s;     // 2 bytes
    int i;       // 4 bytes
};
int main() {
    printf("sizeof(struct Optimized) = %zu\n", sizeof(struct Optimized));
    return 0;
}

内存布局分析:

  1. c10
  2. c21
  3. s 需要从偶数地址开始,放在 2
  4. i 需要从能被 4 整除的地址开始。s 结束于 3,下一个地址是 44 % 4 == 0,满足。i 放在 4
  5. 总大小:最后一个成员 i 结束于 7,最大对齐要求是 48 是 4 的倍数,所以总大小为 8

最终内存布局:

偏移量 大小
0 c1 1
1 c2 1
2 s (低地址) 2
4 i (低地址) 4
8

运行结果:

sizeof(struct Optimized) = 8

通过简单地重新排列成员顺序,我们将内存占用从 12 字节减少到了 8 字节。


如何手动控制对齐?

#pragma pack(n)

可以使用 #pragma pack 指令来告诉编译器使用指定的对齐值 n(通常是 1, 2, 4, 8, 16)。

  • #pragma pack(1):取消对齐,所有成员紧密排列,没有填充字节,这会牺牲性能,但能节省内存,常用于网络通信、文件格式等需要精确控制内存布局的场景。
  • #pragma pack()#pragma pack(2)#pragma pack(4) 等:恢复或设置默认对齐。

示例:

#include <stdio.h>
#pragma pack(1) // 设置对齐值为 1
struct Packed {
    char c;
    int i;
    short s;
};
#pragma pack() // 恢复默认对齐
int main() {
    printf("sizeof(struct Packed) = %zu\n", sizeof(struct Packed)); // 7
    printf("sizeof(struct Simple) = %zu\n", sizeof(struct Simple)); // 12 (不受影响)
    return 0;
}

运行结果:

sizeof(struct Packed) = 7
sizeof(struct Simple) = 12

可以看到,struct Packed 的大小就是其所有成员大小的简单相加:1 + 4 + 2 = 7,没有任何填充。


sizeof 用于结构体指针

sizeof 作用于一个结构体类型时,返回的是该类型实例的大小。 sizeof 作用于一个结构体指针时,返回的是指针本身的大小(在 64 位系统上是 8 字节),而不是它指向的结构体的大小。

struct Simple {
    char c;
    int i;
};
int main() {
    struct Simple s;
    struct Simple *p = &s;
    printf("sizeof(s) = %zu\n", sizeof(s));   // 12 (结构体大小)
    printf("sizeof(p) = %zu\n", sizeof(p));   // 8 (指针大小,64位系统)
    printf("sizeof(*p) = %zu\n", sizeof(*p)); // 12 (解引用指针,得到结构体实例)
    return 0;
}

结构体自引用问题

结构体不能直接包含自身类型的成员,因为这会导致无限大小的定义,但可以通过指针来实现自引用。

错误示例:

struct Node {
    int data;
    struct Node next; // 错误!sizeof(struct Node) 会无限大
};

正确示例(使用指针):

struct Node {
    int data;
    struct Node *next; // 正确!指针大小是固定的(如8字节)
};

sizeof(struct Node) 是一个确定的值:sizeof(int) + sizeof(struct Node*)


概念 描述
sizeof(struct) 计算结构体实例在内存中占用的总字节数。
内存对齐 由 CPU 硬件特性决定,用于提高内存访问效率或避免硬件异常。
填充字节 编译器在结构体成员之间和末尾插入的字节,以满足对齐规则。
对齐规则 成员起始地址必须能被其大小整除。
结构体总大小必须是其最大成员大小的整数倍。
成员顺序 成员声明顺序影响 sizeof 结果,将类型相近、大小相似的成员放在一起可以减少填充,节省内存。
#pragma pack 用于手动控制内存对齐,#pragma pack(1) 可取消对齐,紧密排列。
sizeof 指针 sizeof(struct_pointer) 返回指针大小,而不是结构体大小,使用 sizeof(*pointer) 才能得到结构体大小。
自引用 结构体通过包含自身类型的指针成员来实现自引用,而不是直接包含自身类型。

理解 sizeof struct 的原理对于 C 语言程序的性能优化和跨平台开发至关重要。

-- 展开阅读全文 --
头像
C语言如何创建Socket窗口?
« 上一篇 前天
Window下C语言如何用OpenGL?
下一篇 » 昨天

相关文章

取消
微信二维码
支付宝二维码

目录[+]