导读:本期聚焦于小伙伴创作的《联合体是什么概念?union关键字基本用法与内存布局解析》,敬请观看详情。把同一段内存解释为不同类型数据,这种看似取巧的做法其实来自C语言联合体。与结构体每个成员独占空间不同,union所有成员共享起始地址,尺寸由最大成员决定。理解这点能解释很多底层通信、寄存器映射中的代码写法。不少嵌入式程序用联合体做类型双关,既避免指针强转的未定义行为,又让协议解析更直观。本文从内存模型切入,对比结构体差异,给出定义语法、访问规则与典型应用场景,并提醒大小端和对齐可能带来的隐蔽错误,帮助写出安全可移植的代码。

在C和C++里,联合体(union)是一种特殊的数据类型,它允许在相同的内存位置存储不同的数据类型。和结构体把成员依次排列、各自占用独立空间不同,联合体的所有成员从同一个地址开始,任意时刻只能安全地保存其中一个成员的值。编译器为联合体分配的空间大小,等于其最宽成员所占的字节数,再按对齐要求补齐。这种特性让联合体成为做类型双关、硬件寄存器映射以及协议数据解析时的常用工具。

联合体是什么概念?union关键字基本用法与内存布局解析

一、union关键字的基本定义与语法

使用union关键字可以定义一个联合体类型。语法形式上它和struct非常接近,只是把关键字换成了union。下面给出一个最简单的例子,定义一个能够用不同方式解释同一段内存的联合体。

#include <stdio.h>

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

int main() {
    union Data d;
    d.i = 0x3f800000;
    printf("as int: %dn", d.i);
    printf("as float: %fn", d.f);
    printf("as char[0]: %02xn", (unsigned char)d.c[0]);
    return 0;
}

在上面代码中,d.i、d.f和d.c共享同一块4字节内存。给d.i写入一个十六进制值后,再用d.f读取,就相当于把这块内存按照float的编码规则解释,得到1.0。这种方式比用指针强制转换更不容易触发严格的别名规则问题,但依然要小心平台字节序。

联合体也可以定义匿名联合体,或者作为结构体的成员嵌套使用。匿名联合体在C11和C++中都得到支持,访问其成员时不需要写外层联合体变量名,看起来就像结构体的直接成员一样,适合做寄存器字段拆分。

struct Packet {
    unsigned int raw;
    union {
        unsigned char bytes[4];
        struct {
            unsigned int a : 8;
            unsigned int b : 8;
            unsigned int c : 8;
            unsigned int d : 8;
        } fields;
    };
};

这段定义里,Packet同时拥有原始整型raw和内部匿名联合体。程序可以通过bytes数组按字节访问,也能通过fields位域字段访问,二者背后都是同一段内存。这样的写法在驱动开发和网络协议处理中非常普遍。

二、联合体与结构体内存布局的差异

很多人初学时会把联合体和结构体搞混。结构体struct的每个成员都有自己独立的偏移地址,总大小是各成员大小之和再加上填充字节。联合体union则所有成员偏移都为0,总大小是最宽成员大小并做对齐补齐。可以用一段代码直观对比。

#include <stdio.h>

struct S {
    int a;
    char b;
    double c;
};

union U {
    int a;
    char b;
    double c;
};

int main() {
    printf("sizeof struct S: %zun", sizeof(struct S));
    printf("sizeof union U: %zun", sizeof(union U));
    return 0;
}

在常见的64位Linux环境里,struct S因为对齐通常占用16字节,而union U只占用8字节,也就是double的大小。可见联合体在节省内存方面有明显优势,代价是同一时间只能可靠地使用其中一个成员。

需要强调的是,写入一个成员后再读取另一个成员,标准称之为类型双关。C99之后允许通过联合体做这种操作,行为是明确实现的;但反过来用指针强转类型去读,则可能违反严格别名规则,被编译器优化搞错。因此联合体比指针转换更安全可控。

对比项结构体 struct联合体 union
成员存储各自独立空间共享同一空间
总大小成员之和加填充最大成员加对齐
同时有效成员全部有效仅最后写入者有效
典型用途聚合不同数据类型双关、省内存

三、联合体的典型使用场景

联合体最常见的用法之一是协议解析。例如网络收到的4字节数据,既想当整型算长度,又想按字节拆开看头标志,用联合体就很自然。下面示例把一个32位状态字拆成两个16位字段。

#include <stdio.h>
#include <stdint.h>

union Status {
    uint32_t value;
    struct {
        uint16_t low;
        uint16_t high;
    } part;
};

int main() {
    union Status s;
    s.value = 0x12345678;
    printf("low: %x, high: %xn", s.part.low, s.part.high);
    return 0;
}

在小端机器上,s.part.low会得到0x5678,high得到0x1234。这种写法直观且不需要移位运算,但正因依赖字节序,在跨平台通信时要特别小心,必要时应显式转换字节序而不是依赖联合体。

另一个场景是硬件寄存器映射。很多MCU的手册会把一个32位寄存器分成不同位域,用联合体配合位域结构体,可以既用整型写整个寄存器,又用位域改其中几位。这种方式在裸机开发中大量存在,能减少繁琐的掩码操作。

volatile union Reg {
    uint32_t all;
    struct {
        uint32_t enable : 1;
        uint32_t mode   : 3;
        uint32_t reserved : 28;
    } bit;
} *const UART_CTRL = (union Reg *)0x40001000;

上述代码把地址0x40001000映射为寄存器联合体,写UART_CTRL->all可以整体配置,写UART_CTRL->bit.enable可以只改使能位。注意这里用了volatile避免编译器优化掉寄存器访问,且地址映射只适合确定硬件平台的裸机或驱动代码。

四、使用联合体时的注意事项

联合体的最大陷阱是类型双关带来的未初始化和字节序问题。如果先写int成员,再读float成员,得到的值依赖于int的位模式如何被解释为IEEE754浮点,这通常不是你想要的“转换”,而是重新解释。若真要做数值转换,应当用赋值而非联合体。

union Bad {
    int i;
    float f;
};

// 错误认知:以为这样能把int转成float
union Bad b;
b.i = 10;
// b.f 并不是 10.0,而是把10的位模式当浮点解释

此外,包含非平凡构造函数的C++类对象不能作为联合体成员,除非联合体本身也提供相应构造和析构,否则编译会报错。C++11起允许带非平凡成员的联合体,但必须手动管理活动成员的生命周期,否则容易资源泄漏。

最后要注意对齐。联合体大小由最宽且对齐要求最高的成员决定,如果里面放了SIMD向量类型,可能导致整个联合体按16或32字节对齐,在栈上或数组里会放大内存占用。在内存紧张的环境,应当用static_assert检查联合体大小是否符合预期。

#include <assert.h>
union Check {
    char a[8];
    long long b;
};
static_assert(sizeof(union Check) == 8, "unexpected union size");

通过静态断言可以在编译期发现联合体尺寸变化,避免后续代码基于错误假设访问内存。总体来看,联合体是贴近硬件、节省空间的好工具,但只在明确需要共享内存语义时使用,不要把它当普通类型转换的捷径。

union联合体内存布局修改时间:2026-08-04 15:54:37

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。