结构体与联合体优化:内存对齐、位域使用、联合体的妙用

结构体和联合体,是C语言里最常用的复合数据类型。但说实话,很多人用了好几年,还是没搞明白怎么用才高效。我见过不少项目,结构体定义得随心所欲,结果内存浪费严重,性能也受影响。今天咱们就聊聊怎么把这些东西用好。

一、内存对齐:看不见的浪费

先问个问题:下面这个结构体占多少字节?

struct Example {
    char a;    // 1字节
    int b;     // 4字节
    char c;    // 1字节
};

很多人会算:1 + 4 + 1 = 6字节。但实际呢?在我的机器上,sizeof(struct Example) 返回的是12字节。为什么?

这就是内存对齐在作怪。CPU读取内存时,不是按字节读的,而是按字(4字节或8字节)读的。如果int变量没有对齐到4字节边界,CPU就得读两次,再拼起来。这太慢了。

对齐规则其实很简单:

  • 每个成员的对齐要求,等于它自身的大小(比如int是4字节对齐)
  • 结构体的总大小,必须是最大成员对齐值的整数倍
  • 编译器会在成员之间和末尾插入填充字节

我在项目中遇到过这样一个坑:一个网络协议的结构体,因为没注意对齐,在x86上跑得好好的,换到ARM上就崩了。查了两天才发现是对齐问题。嗯,从那以后我写结构体都会加一句:

#pragma pack(push, 1)  // 取消对齐
struct PacketHeader {
    uint8_t type;
    uint32_t length;
    uint16_t checksum;
};
#pragma pack(pop)
注意:取消对齐虽然省内存,但会降低访问速度。网络协议、文件格式这类需要精确控制内存布局的场景才用。普通场景别乱用。

二、优化内存布局:重新排列成员

其实有个更优雅的办法——调整成员顺序。把大的成员放前面,小的放后面,就能减少填充。

// 不好的布局:12字节
struct Bad {
    char a;
    int b;
    char c;
};

// 好的布局:8字节
struct Good {
    int b;      // 4字节
    char a;     // 1字节
    char c;     // 1字节
    // 填充2字节
};

你想想看,只是换了个顺序,就省了4个字节。如果结构体数组有1000个元素,那就是4KB的差距。嵌入式系统里,这点空间可能就决定了你的程序能不能跑起来。

我的习惯:写结构体时,先把所有成员列出来,按大小从大到小排序。这样既省内存,又不会破坏对齐规则。

三、位域:精确控制比特

位域这东西,说白了就是让你能操作一个字节里的某几个比特。我最早接触它是在做通信协议解析的时候——一个字节里塞了好几个标志位,不用位域就得写一堆移位和掩码操作。

struct Flags {
    unsigned int ready : 1;    // 1比特
    unsigned int error : 1;    // 1比特
    unsigned int mode : 2;     // 2比特
    unsigned int count : 4;    // 4比特
};

这个结构体只占1个字节,却存了4个字段。如果用普通成员,至少得4个字节。

位域的使用要点:

  • 位域的类型必须是int、unsigned int或signed int
  • 位域不能取地址(不能用&操作符)
  • 位域数组不存在
  • 不同编译器的位域布局可能不同——这是个大坑
我曾经踩过的坑:在项目里用位域定义了一个硬件寄存器映射结构体,在GCC上跑得好好的,换到IAR编译器上就全乱了。后来查文档才发现,两个编译器对位域的分配顺序是相反的。所以,跨平台项目慎用位域,或者用宏和移位操作代替。

四、联合体:同一块内存的不同解读

联合体是我最喜欢的C语言特性之一。它让同一块内存地址,在不同时刻可以当作不同类型来用。说白了,就是内存复用。

union Data {
    int i;
    float f;
    char str[4];
};

这个联合体只占4个字节(最大成员的大小)。你可以存一个int,然后读成一个float——虽然结果可能很奇怪,但语法上是合法的。

联合体的经典用法:

  1. 协议解析:一个数据包可能有多种类型,用联合体可以共用解析缓冲区
  2. 类型转换:用联合体实现reinterpret_cast的效果(C语言没有这个关键字)
  3. 节省内存:多个字段不会同时使用,共用一块内存
// 协议解析的典型用法
struct Packet {
    uint8_t type;
    union {
        struct { uint32_t addr; } connect;
        struct { uint8_t data[64]; } send;
        struct { uint8_t code; } error;
    } payload;
};

我在做物联网网关项目时,就用这种结构处理了十几种不同类型的消息。每个包只占固定大小的内存,解析起来也特别清晰。

五、结构体与联合体的组合技巧

把结构体和联合体组合起来,能玩出很多花样。比如实现一个变体类型:

typedef struct {
    int type;  // 0: int, 1: float, 2: string
    union {
        int i;
        float f;
        char* s;
    } value;
} Variant;

这种模式在脚本引擎、配置系统里特别常见。一个变量可以存不同类型的数据,运行时根据type字段决定怎么解读value。

核心思路:结构体负责组织数据,联合体负责复用内存。两者结合,既能保持代码清晰,又能节省空间。

六、知识体系总览

下面这张图总结了结构体与联合体优化的核心知识点:

结构体与联合体优化知识体系 内存对齐 位域使用 联合体妙用 对齐规则:成员按自身大小对齐 优化技巧:大成员在前,小成员在后 pragma pack:取消对齐(慎用) 精确控制比特:节省内存 协议解析:标志位打包 跨平台陷阱:编译器差异 内存复用:同一块内存不同解读 协议解析:多种消息类型 变体类型:结构体+联合体组合 核心原则:理解内存布局,善用复用技巧,注意跨平台差异

七、实战建议

说了这么多,总结几条我个人的实战经验:

  • 先想清楚数据的使用场景——哪些字段经常一起访问?哪些字段互斥?这决定了你用结构体还是联合体
  • 能用普通成员就别用位域——位域的性能和可移植性都不如普通成员
  • 联合体配合枚举类型使用——用枚举标记当前联合体里存的是什么类型,避免读错
  • 写代码时就把对齐考虑进去——别等出问题了再回来改,那时候改起来成本高得多

嗯,结构体和联合体优化,说白了就是理解内存布局,然后根据实际需求做取舍。没有银弹,但掌握了这些技巧,你的C代码至少能省30%的内存,性能也能提升不少。


公众号:蓝海资料掘金营,微信deep3321