C语言union竟能“榨干”内存?这3个骚操作程序员必看!

你是不是也觉得C语言的自定义类型里,struct(结构体)就是“顶流”?写个学生信息、设备参数,随手就用struct,舒服又顺手~ 但你知道吗?struct有个“低调孪生兄弟”union(共用体/联合体),明明是内存优化的“隐藏王者”,却被90%的初学者当成“无用废柴”——觉得它“逻辑怪”“容易出错”,直接打入冷宫!

其实啊,union的核心套路就一个:同一块内存轮流用,不浪费一寸空间!在嵌入式、单片机这种“内存按字节抠”的场景里,它能让你的代码从“臃肿胖子”变身“灵活瘦子”,效率直接翻倍~ 今天就用最接地气的话,把union的原理、技巧、坑点扒得明明白白,看完你绝对会喊:“原来这玩意儿这么香!”

一、先搞懂:union到底是个啥?

union的核心规则就两条,记牢这俩,直接避开80%的坑,比背单词还简单:

  1. 所有成员“共享同一块内存”,就像几个人挤一个房间,起始位置完全一样;
  2. union的大小 = 最大成员的大小(还要满足内存对齐,这点后面细说)。

光说不练假把式,咱们拿struct和union做个直观对比,代码跑一遍就懂:

#include <stdio.h>

// 结构体:每个成员占独立“房间”
struct S {
    char c;  // 1字节的小房间
    int i;   // 4字节的大房间
};

// 共用体:所有成员挤一个“大房间”
union U {
    char c;  // 1字节的“小家具”
    int i;   // 4字节的“大家具”
};

int main() {
    printf("struct S大小:%zd\n", sizeof(struct S));  // 输出8(含3字节“空床位”填充)
    printf("union U大小:%zd\n", sizeof(union U));    // 输出4(只留最大“家具”的空间)
    return 0;
}

简单说:struct是“每人一间房,哪怕空着也不让用”,总空间是所有房间+空床位;union是“大家挤一间房,谁用谁摆家具”,总空间只够放最大的那件家具~

这里提一嘴内存对齐:不是光看最大成员就行哦!比如成员是char[5](5字节)和int(4字节,对齐数4),5不够4的2倍(8字节),就会补3字节“空位置”,所以union大小是8字节——就像房间要摆下5米的柜子,但房子得按4米的倍数建,只能建8米的,多出来的3米暂时空着~

二、3个核心技巧:union的“神仙用法”

union的价值不是“存多个数据”,而是“灵活复用内存”——这3个场景直接套用,效率拉满!

技巧1:内存优化——互斥数据的“空间共享”

当多个数据“不同时上班”(互斥关系),用union替代struct,能省出一大半内存!嵌入式开发里,这招能让设备多跑好几个功能~

比如传感器数据:传感器是个“专一选手”,同一时间只输出温度(int类型)或电压(float类型),绝不会两边同时干活。

  • 反例(浪费内存):用struct存储,不管哪种数据有效,都要占int+float=8字节(32位系统)——相当于给两个不同时上班的人,各租一间房,太浪费!
  • 正例(优化内存):用union存储,只占4字节(最大成员大小)——相当于让两人共用一间房,谁上班谁用,完美利用空间!

代码演示:

#include <stdio.h>

// 传感器数据:温度和电压“轮班制”
union SensorData {
    int temp;    // 温度(℃)—— 早班
    float voltage;  // 电压(V)—— 晚班
};

int main() {
    union SensorData data;

    // 早班:读取温度,用temp成员
    data.temp = 25;
    printf("当前温度:%d℃\n", data.temp);  // 输出:25℃

    // 晚班:读取电压,复用同一块内存
    data.voltage = 3.3f;
    printf("当前电压:%.1fV\n", data.voltage);  // 输出:3.3V

    return 0;
}

核心逻辑:用“时间上的先后使用”,替代“空间上的同时占用”——就像共享充电宝,你用完我用,不浪费资源~

技巧2:类型双关——无拷贝的“数据格式转换”

利用“成员共享内存”的特性,union能实现“零拷贝类型转换”(也叫“类型双关”)——不用复制数据,直接把同一块内存当成另一种类型解析,效率超高!

场景1:拆分int的4个字节(网络协议常用)

比如网络传输时,大整数(4字节int)不能直接传,得拆成4个单字节再发——用union不用写复杂的移位运算,直接“拆分”:

#include <stdio.h>

// 把int拆成4个char字节,像拆大礼包一样
union Int2Bytes {
    int num;        // 4字节的“大礼包”
    char bytes[4];  // 4个1字节的“小零食”
};

int main() {
    union Int2Bytes conv;
    conv.num = 0x12345678;  // 一个16进制大整数

    // 输出每个字节,还能判断系统是“小端”还是“大端”
    printf("字节0:%x\n", conv.bytes[0]);
    printf("字节1:%x\n", conv.bytes[1]);
    printf("字节2:%x\n", conv.bytes[2]);
    printf("字节3:%x\n", conv.bytes[3]);

    // 小端系统输出:78 56 34 12(低地址存低位,像把礼包倒着放)
    // 大端系统输出:12 34 56 78(低地址存高位,像把礼包正着放)
    return 0;
}
场景2:int与float的二进制互转

不用强制类型转换,直接复用内存,把int的二进制当成float解析——比如把一个16进制int,转成IEEE754标准的float:

#include <stdio.h>

union TypePun {
    int i;    // 整数类型
    float f;  // 浮点类型
};

int main() {
    union TypePun pun;
    pun.i = 0x41424344;  // 16进制值,对应float的二进制格式
    printf("float值:%f\n", pun.f);  // 输出:6.928346e-34(按IEEE754规则解析)
    return 0;
}

注意:这种转换依赖二进制存储规则(比如IEEE754浮点数标准),跨平台要谨慎,但同一架构下(比如都是x86),绝对安全又高效~

技巧3:嵌套结构体——多类型数据的“安全封装”

单独用union有个坑:不知道当前哪个成员有效,容易读错数据。实际开发中,大家都用“struct嵌套union”的组合拳——加一个“类型标记”,告诉程序“现在该用哪个成员”,安全又好懂!

场景:处理不同类型的消息(文本/图片),用标记区分:

#include <stdio.h>
#include <string.h>

// 消息类型标记:像快递单上的“物品类型”
typedef enum {
    TEXT_MSG,  // 文本消息
    IMG_MSG    // 图片消息
} MsgType;

// 消息结构体:外层struct包着“类型标记+union”
typedef struct {
    MsgType type;  // 先说明“是什么类型”
    union {
        // 文本消息:长度+内容
        struct {
            int len;
            char content[100];
        } text;
        // 图片消息:宽+高+像素数据
        struct {
            int width;
            int height;
            char pixels[1024];
        } img;
    } data;  // 共用体存储具体内容
} Message;

// 处理消息的函数:按标记“对症下药”
void process_msg(Message* msg) {
    switch (msg->type) {
        case TEXT_MSG:
            printf("文本消息(长度:%d):%s\n", msg->data.text.len, msg->data.text.content);
            break;
        case IMG_MSG:
            printf("图片消息(%d×%d像素)\n", msg->data.img.width, msg->data.img.height);
            break;
        default:
            printf("未知消息类型,跳过~\n");
    }
}

int main() {
    // 1. 处理文本消息
    Message text_msg;
    text_msg.type = TEXT_MSG;
    text_msg.data.text.len = 11;
    strcpy(text_msg.data.text.content, "Hello Union!");
    process_msg(&text_msg);

    // 2. 处理图片消息
    Message img_msg;
    img_msg.type = IMG_MSG;
    img_msg.data.img.width = 640;
    img_msg.data.img.height = 480;
    process_msg(&img_msg);

    return 0;
}

这种写法是工业级常用操作——既省内存,又不会“读错成员”,可读性拉满,再也不用担心别人看你代码时骂“这写的啥玩意儿”~

三、避坑指南:这4个雷区千万别踩!

union虽香,但“内存共享”的特性也藏着陷阱——就像共享房间,不遵守规则就会乱成一锅粥!这4点一定要记牢:

1. 别访问被覆盖的成员!

给union一个成员赋值后,其他成员会被“覆盖”(值变成随机无效数),再访问就会出问题——就像你刚在杯子里倒了可乐,又倒了牛奶,再喝“可乐”不仅味道怪,还可能拉肚子(程序崩溃)~

反例(错误示范):

union U {
    int i;
    float f;
};

union U u;
u.i = 100;       // 先装“可乐”
u.f = 3.14f;     // 再倒“牛奶”,覆盖可乐
printf("%d\n", u.i);  // 错误:现在读的是“可乐味牛奶”,值随机

2. 初始化只能“盯准一个成员”

C语言规定:union只能初始化第一个成员;想初始化其他成员,得用C99及以上的“指定初始化器”(.成员名 = 值)——不能贪心同时初始化多个!

正确vs错误示范:

union U {
    int i;
    float f;
};

union U u1 = {10};        // 正确:初始化第一个成员i
union U u2 = {.f = 3.14f};// 正确:C99指定初始化f(推荐这么写)
union U u3 = {10, 3.14f}; // 错误:想同时装可乐和牛奶,不可能!

3. 别忽略内存对齐的影响

union的大小不仅看最大成员,还要满足内存对齐——比如成员是char[3](3字节)和int(4字节),默认对齐下union大小是4字节,不是7字节~ 忽略对齐可能导致内存浪费,或跨平台跑不通。

如果是特殊场景(比如访问硬件寄存器),想强制取消对齐,可用编译器指令(GCC的#pragma pack(1)):

#pragma pack(push, 1)  // 强制对齐1字节,不留空位置
union PackedUnion {
    char c[3];  // 3字节
    int i;      // 4字节
};
#pragma pack(pop)  // 恢复默认对齐

printf("大小:%zd\n", sizeof(union PackedUnion));  // 输出4(不是7)

4. 别在union里嵌套动态内存指针

避免在union里放指针(尤其是指向动态内存的指针)——成员覆盖时,指针可能被破坏,导致内存泄漏或野指针错误~ 就像你把充电宝借给别人,别人用的时候给你搞坏了,你再用就充不了电(指针失效)。如果实在要⽤,一定要严格管理指针的“生命周期”,用完及时回收~

四、struct vs union:该怎么选?一张表看明白

很多人分不清啥时候用struct,啥时候用union——直接看这张表,秒懂!

特性 struct(结构体) union(共用体)
内存分配 成员独立,总大小=成员和+填充 成员共享,总大小=最大成员(含对齐)
数据共存 所有成员同时有效(每人一间房) 同一时刻仅一个成员有效(挤一间房)
核心用途 封装对象属性(如学生信息、设备参数) 内存优化、类型转换、协议解析

简单总结:

  • 想同时存储多个相关数据(比如学生的姓名、年龄、成绩)——用struct;
  • 内存紧张、数据互斥,或需要高效类型转换(比如网络协议解析)——用union!

最后:来个小挑战,学以致用~

看完这么多干货,动手练一练才记得牢!挑战任务:用union实现一个“既能存储整数,又能存储字符串”的通用数据结构,还要加类型标记保证访问安全~

比如:用枚举标记“整数类型”“字符串类型”,struct嵌套union,存储int或char数组——评论区晒出你的代码,看看谁的写法最简洁优雅!

union的核心价值,就是用“时间复用”换“空间高效”——在内存紧张的场景里,它就是“救命神器”!掌握这3个技巧+4个避坑点,不仅能让你的代码更高效,还能加深对C语言内存布局的理解~ 毕竟,能把内存玩明白的程序员,才是真正的C语言高手呀!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐