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

这个大小不等于其所有成员大小的简单相加,编译器为了提高内存访问效率,会在结构体成员之间插入填充字节,并在结构体末尾进行对齐填充,这个过程被称为内存对齐。
为什么需要内存对齐?
内存对齐不是 C 语言语法的强制要求,而是大多数计算机体系结构(特别是 x86/x64 架构)的硬件特性决定的,主要原因有两点:
-
性能提升:
- 现代CPU在访问内存时,通常不是按字节访问,而是按字长进行访问,在 64 位系统上,CPU 一次会读取 64 位(8 字节)的数据。
- 如果一个
int(4 字节)跨越了两个 8 字节的内存块,CPU 需要进行两次内存访问,然后将结果合并,效率极低。 - 如果数据正确对齐,CPU 只需一次内存访问就能完整读入数据,速度更快。
-
硬件要求:
(图片来源网络,侵删)某些硬件架构(如某些 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;
}
内存布局分析:
-
成员
char c;(1 字节)- 放在偏移量
0处。 - 当前占用:
[0]
- 放在偏移量
-
成员
int i;(4 字节)- 根据规则 1,
int必须从能被 4 整除的地址开始。 - 上一个成员
c占用了0,下一个可用地址是1。1 % 4 != 0,所以不能放。 - 编译器会插入 3 个填充字节,让
i从偏移量4开始。 - 当前占用:
[0] c, [1-3] 填充, [4-7] i
- 根据规则 1,
-
成员
short s;(2 字节)- 根据规则 1,
short必须从偶数地址开始。 - 上一个成员
i结束于7,下一个可用地址是8。8 % 2 == 0,满足条件。 s可以直接放在偏移量8处。- 当前占用:
... [8-9] s
- 根据规则 1,
-
计算总大小
- 最后一个成员
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;
}
内存布局分析:
c1在0。c2在1。s需要从偶数地址开始,放在2。i需要从能被 4 整除的地址开始。s结束于3,下一个地址是4,4 % 4 == 0,满足。i放在4。- 总大小:最后一个成员
i结束于7,最大对齐要求是4。8是 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 语言程序的性能优化和跨平台开发至关重要。
