这是一个在嵌入式系统开发中非常常见且重要的关键字,我会从以下几个方面为你解释:

- 核心概念:
__packed是什么? - 为什么需要
__packed?(解决的问题) - 如何使用
__packed? - 使用
__packed的注意事项和潜在风险 - 与其他语言的对比
核心概念:__packed 是什么?
__packed(或 _packed,具体取决于编译器)是一个 编译器指令(或称为属性/attribute),而不是 C 语言标准的一部分,它的作用是告诉编译器:
“请将这个结构体(或联合体)中的所有成员紧密地排列在一起,不要在它们之间插入任何用于内存对齐的填充字节(padding bytes)。”
就是取消结构体的内存对齐,使得结构体的大小等于其所有成员大小之和。
为什么需要 __packed?(解决的问题)
要理解 __packed 的必要性,我们首先要明白什么是 内存对齐。

内存对齐
为了提高 CPU 访问内存的效率,现代计算机体系结构通常要求特定类型的数据存储在特定地址的内存位置。
- 4 字节的
int类型数据最好存储在能被 4 整除的内存地址上。 - 8 字节的
double类型数据最好存储在能被 8 整除的内存地址上。
编译器在编译结构体时,会自动在成员之间插入“填充字节”(Padding Bytes),以满足这些对齐要求。
示例:没有 __packed 的情况
假设我们有以下结构体,在大多数 32 位或 64 位系统上:

struct example {
char a; // 1 字节
int b; // 4 字节
char c; // 1 字节
};
内存布局可能是这样的:
地址: 0 1 2 3 4 5 6
+--+--+--+--+--+--+--+
|a| | | |b|b|b|b| |c| <-- 实际内存
+--+--+--+--+--+--+--+
|a|pad|pad|pad|b| |c| <-- 逻辑视图
+--+--+--+--+--+--+--+
char a占用地址 0。int b需要从能被 4 整除的地址开始,所以编译器在a后面插入了 3 个填充字节(pad),b从地址 4 开始。char c放在b后面的地址 8。- 整个结构体的大小是 9 字节。
__packed 的作用
我们给结构体加上 __packed 属性(这里以 GCC/Clang 的语法为例):
struct __packed example_packed {
char a; // 1 字节
int b; // 4 字节
char c; // 1 字节
};
内存布局会变成这样:
地址: 0 1 2 3 4 5 6
+--+--+--+--+--+--+--+
|a|b|b|b|b|c| | | | <-- 实际内存
+--+--+--+--+--+--+--+
|a| b |c| <-- 逻辑视图
+--+--+--+--+--+--+--+
char a占用地址 0。int b紧跟在a后面,从地址 1 开始(虽然这不符合对齐要求,但__packed强制允许)。char c紧跟在b后面,从地址 5 开始。- 整个结构体的大小是 6 字节(1 + 4 + 1),没有填充字节。
使用场景
__packed 主要用于以下场景:
- 硬件寄存器操作:在与硬件设备(如 MCU、外设芯片)通信时,硬件寄存器的映射地址是固定的,其内存布局是固定的,不能有填充字节,你必须使用
__packed结构体来精确匹配硬件的寄存器定义。 - 网络协议和数据包:网络数据包(如 TCP/IP、UDP)的格式是严格定义的,每个字段都有固定的偏移量,为了方便地解析和构造数据包,通常会使用
__packed结构体。 - 文件格式和二进制数据:读写二进制文件(如图片、自定义文件格式)时,需要确保内存中的数据结构与文件中的布局完全一致。
- 节省内存:在某些内存极其受限的嵌入式系统中,如果结构体成员很多,取消对齐可以节省一些填充字节占用的内存。
如何使用 __packed?
__packed 的语法因编译器而异,以下是几种常见的编译器实现:
GCC / Clang (Linux, macOS, 嵌入式 Linux)
使用 __attribute__((packed))。
struct my_struct {
char c;
int i;
} __attribute__((packed));
// 或者直接作用于结构体定义
struct __attribute__((packed)) my_struct_2 {
char c;
int i;
};
MSVC (Visual Studio)
使用 #pragma pack 指令。
// 设置打包对齐为 1 字节
#pragma pack(push, 1)
struct my_struct {
char c;
int i;
};
// 恢复之前的打包设置
#pragma pack(pop)
#pragma pack(push, 1) 中的 1 表示按 1 字节对齐(即紧密排列)。push 和 pop 用于保存和恢复之前的编译器设置,避免影响其他代码。
IAR / Keil (ARM 嵌入式开发器)
通常使用 __packed 关键字。
struct __packed my_struct {
char c;
int i;
};
使用 __packed 的注意事项和潜在风险
虽然 __packed 很有用,但它是一把“双刃剑”,使用不当会带来严重问题。
主要风险:性能下降
这是 __packed 最主要的缺点,CPU 访问未对齐的内存可能会需要更多的时钟周期,甚至触发硬件异常(在某些架构上,如 ARM)。
示例:访问未对齐的 int
假设我们有一个 __packed 结构体:
struct __packed data {
char a;
int b;
};
当 CPU 试图读取 data.b 时,它可能需要执行两次内存访问:
- 从地址 X 读取低 2 字节。
- 从地址 X+2 读取高 2 字节。
CPU 在内部将这两个部分组合成一个完整的
int。
b 是正确对齐的(例如从地址 4 开始),CPU 可能只需要一次内存访问就能读取整个 4 字节的 int。
不要为了节省一点点内存而在通用的、性能敏感的代码中使用 __packed。 它只应在与硬件或外部协议交互等特定场景下使用。
其他风险
- 可移植性差:
__packed不是 C 标准的一部分,不同编译器的语法和实现细节可能不同,跨平台代码需要处理这些差异。 - 代码可读性降低:取消对齐的内存布局在调试和阅读时会更困难。
与其他语言的对比
- C++:C++ 同样支持
__attribute__((packed))(GCC/Clang)和#pragma pack(MSVC),C++11 引入了标准化的对齐控制机制,如alignas和alignof,但__packed仍然因其简单性而被广泛使用。 - Rust:Rust 有一个更现代和安全的系统,它使用
#[repr(packed)]属性来实现类似的功能,Rust 的类型系统在编译时会尽力防止对未对齐指针的直接解引用,以避免运行时错误,这比 C 语言更安全。
| 特性 | 描述 |
|---|---|
| 作用 | 强制结构体成员紧密排列,取消内存填充字节。 |
| 语法 | 非标准,因编译器而异(如 __attribute__((packed)), #pragma pack, __packed)。 |
| 优点 | 精确匹配硬件寄存器布局。 方便解析网络协议和二进制文件。 在特定情况下节省内存。 |
| 缺点 | 可能导致显著的性能下降(未对齐访问)。 可移植性差。 代码可读性降低。 |
| 使用原则 | 仅在必要时使用,例如与硬件交互、解析外部协议等。避免在通用代码中滥用。 |
希望这个详细的解释能帮助你完全理解 C 语言中的 __packed!
