欢迎光临
我们一直在努力

一文详解:C共用体

共用体

共用体和结构体一样,都能把多个成员组织在一起,但两者的存储方式并不相同。结构体的各成员各有自己的空间,共用体的各成员则共享同一块存储空间。本文围绕 C 语言共用体,梳理声明、成员访问、读写覆盖、内存对齐、初始化等常见知识点,并通过对比与问答形式说明容易混淆的细节。 本系列还有更多其他作品:万字详解:C指针与动态内存分配、一文详解:C结构体、一文详解:C枚举

一、基本概念

共用体是一种特殊的数据类型,关键字是 union。同一个共用体变量的各成员共享存储空间,可以在不同时间存放不同类型的数据。各成员的起始地址相同,但成员占用的字节数不一定相同。例如,一个成员占 4 字节,另一个成员占 10 字节,它们从同一地址开始,共享起始的那部分空间,并不是每个成员都占满整个共用体。

共用体和结构体有什么区别?

比较内容结构体 struct共用体 union
成员的存储空间 各成员有各自的空间 各成员共享空间
能否同时保存各成员独立的值 可以 不可以
修改一个成员 不会因此改写其他成员 可能覆盖其他成员的数据
大小 需要容纳所有成员,并考虑对齐 需要容纳最大成员,并考虑对齐

例如,需要同时保存学生的学号和成绩,应使用结构体:

struct Student {
int id;
float score;
};

共享内存的是同一个共用体变量的成员,不是同一种共用体类型的所有变量。

union Data {
int i;
float f;
};

union Data a, b;

a.i = 10;
b.f = 3.5f;

a 和 b 是两个独立变量,修改 b.f 不影响 a.i。

二、声明与定义

声明共用体类型:

union Data {
int i;
float f;
char c;
};

用该类型定义变量:

union Data d;

在 C 中,如果没有使用 typedef 定义类型别名,不能直接写:

Data d; // 不正确

也可以在声明类型的同时定义变量:

union Data {
int i;
float f;
char c;
} d;

三、访问成员

共用体变量通过 . 访问成员:

d.i = 10;
printf("%d\\n", d.i);

指向共用体的指针通过 -> 访问成员:

union Data *p = &d;

p->i = 20;
printf("%d\\n", p->i);

. 和 & 放在一起,怎样结合?

. 的优先级高于一元运算符 &。

int *p = &d.i;

等价于:

int *p = &(d.i);

意思是取成员 d.i 的地址。

取地址与读取值是两种不同的操作。 即使成员尚未初始化,也可以取它的地址,但不能因此随意读取它的值。

优先级和求值顺序有什么区别?

优先级决定表达式怎样组合,即谁与谁组成一个整体。求值顺序决定运行时先计算谁、后计算谁。

例如:

f() + g() * h()

根据优先级,它相当于:

f() + (g() * h())

这决定了 g() 和 h() 的返回值相乘,再与 f() 的返回值相加。不代表一定先调用 g()、h(),最后才调用 f()。C 没有规定这里三个函数调用的先后次序。

优先级决定计算关系,与各部分的求值顺序无关。

四、读取与写入:什么操作会覆盖数据?

写入成员可能覆盖其他成员的数据,读取本身不会造成覆盖。

d.i = 10;

printf("%d\\n", d.i);
printf("%d\\n", d.i);

两次都输出 10,因为读取没有修改保存的内容。

d.i = 10;
d.f = 3.5f;

后一次赋值改写了共享内存,不能再指望 d.i 仍然保存整数 10。

后一次写入可能覆盖前一次保存的数据,如果需要先后使用不同成员,可以在覆盖之前使用当前值:

d.i = 10;
printf("%d\\n", d.i);

d.f = 3.5f;
printf("%f\\n", d.f);

如果需要同时保留多个值,应使用结构体或其他独立的存储空间。

五、读取另一个成员,不等于类型转换

d.f = 3.5f;
printf("%d\\n", d.i);

这里不是把浮点数 3.5f 转换为整数 3,而是按照 int 的方式解释对应的内存内容。

同一组 0 和 1,原本按一种类型的编码规则解释是 3.5,改用另一种类型的规则解释,就不能指望得到相同的数值。

在常见的 32 位 int、IEEE 754 单精度 float 平台上:

同一组位的十六进制表示解释方式得到的值
0x40600000 按 float 解释 3.5
0x40600000 按 int 解释 1080033280

这个具体结果依赖平台,不是 C 语言在所有平台上的保证。读取非最近写入的成员涉及数据表示等细节,可能遇到陷阱表示,不能普遍保证得到有意义的值。

初学时,通常应读取最近一次写入的成员。

真正的数值转换是什么样的?

d.f = 3.5f;
int n = (int)d.f;

先读取浮点数 3.5f,再转换为整数 3,用于初始化 n。

可以这样简化理解:类型转换相当于使用一个临时副本,得到转换后的值,不修改原值。

这里的“临时副本”是理解模型,不代表一定在内存中实际创建了一个副本。

n = 8;

不会影响 d.f,因为 n 是独立变量,有自己的存储空间。

如果把转换结果写回共用体呢?

d.f = 3.5f;
d.i = (int)d.f;

执行过程:

  • 读取 d.f,得到 3.5f。
  • 将其转换成整数 3,转换本身不修改共享内存。
  • 把整数 3 写入 d.i,这一步改写共享内存。
  • 因此,最后 d.i 为整数 3,不能再保证 d.f 是 3.5f。

    转换不修改原值,但将转换结果赋给共享内存中的成员,会改写内存。

    六、共用体与指针

    union Data d;
    int *p = &d.i;

    d.f = 3.5f;

    给 d.f 赋值不会让 p 变成悬空指针。

    只要 d 仍然存在,相关存储空间仍然有效。这里改变的是共享内存的内容,而不是让那块空间消失。

    接着执行:

    *p = 20;

    相当于:

    d.i = 20;

    它通过指针把整数 20 写入成员 i,不能再保证 d.f 保持原来的值。

    地址是否有效,与其中保存的内容是否改变,是两个不同的问题。

    七、共用体大小与内存对齐

    共用体必须足以容纳最大的成员,同时满足对齐要求:

    共用体大小 ≥ 最大成员大小

    实际大小使用 sizeof 获取:

    sizeof(union Data)
    sizeof d

    什么是内存对齐?

    内存对齐是指:某种类型的数据在内存中存放时,起始地址需要是某个数的倍数。这个数就是它的对齐要求。(这部分在[万字详解:C 指针与动态内存分配](万字详解:C 指针与动态内存分配-CSDN博客)简单提及过)

    假设某个平台的 int 占 4 字节,要求按 4 字节对齐:

    起始地址是否满足要求
    100
    101
    102
    104

    从地址 100 开始的 int 占用 100、101、102、103 四个字节。

    要求对齐的是起始地址,不是它占用的每个字节地址。

    对齐用于适应处理器访问内存的方式,通常由编译器安排。类型的大小与对齐要求不是同一个概念,不能认为占几字节就必然要求几字节对齐。

    为什么共用体大小可能超过最大成员?

    union Example {
    int i; // 假设占 4 字节,要求 4 字节对齐
    char s[10]; // 占 10 字节
    };

    假设这个共用体的对齐要求为 4 字节,那么它的大小通常是 12 字节。

    这是为了让它们组成数组时,每个元素都满足对齐要求:

    每个元素大小第一个元素起始地址第二个元素起始地址
    10 字节 100 110,不满足 4 字节对齐
    12 字节 100 112,满足 4 字节对齐

    补出来的字节称为填充字节,不代表增加了成员。

    填充字节存放的是“垃圾值”吗?

    填充字节也有实际的 0 和 1,但不能依赖它们具有某个固定内容,也不需要把它们作为成员数据管理。

    “垃圾内容”可以作为口语理解,但不意味着程序专门生成了随机数。

    同样,执行:

    union Example d;
    d.i = 10;

    只能保证 d.i 得到了整数 10,不能认为其余字节都会自动清零。

    八、共用体初始化

    1. 不指定成员名:初始化第一个成员

    union Data {
    int i;
    float f;
    };

    union Data a = {10}; // a.i 为整数 10
    union Data b = {3.5f}; // b.i 为整数 3

    不会根据初始化值的类型自动选择成员。

    第二句仍然初始化第一个成员 i,因此发生浮点数到整数的转换。

    如果调整成员顺序:

    union Other {
    float f;
    int i;
    };

    union Other x = {10};

    初始化的是 f,保存浮点数 10.0f。

    2. 指定成员名:初始化指定成员

    union Data c = {.f = 3.5f};

    .f 表示选择成员 f 进行初始化。

    3. 不能像结构体那样依次初始化多个成员

    union Data c = {10, 3.5f}; // 不符合初始化规则

    第二个初始化值是多余的,编译器必须给出诊断。即使某个编译器警告后继续编译,也不代表同时保存了两个值。

    4. 初始化与赋值不同

    union Data d = {.f = 3.5f}; // 定义变量时初始化
    d.i = 10; // 之后赋值

    共用体可以先后给不同成员赋值,但不能同时保存各成员独立的值。

    九、结构体省略初始化的成员会怎样?

    必须区分“整个变量没有初始化”和“初始化列表只写了部分成员”。

    struct Student {
    int id;
    float score;
    };

    情况结果
    函数内普通局部变量 struct Student a; 成员未初始化,不能直接读取使用
    struct Student b = {.score = 95.0f}; score 为 95.0f,省略的 id 为 0
    struct Student c = {0}; id 为 0,score 为 0.0f
    函数外定义,或使用 static,没有显式初始化 成员按规则默认初始化,这里分别为 0、0.0f

    默认初始化中的“零”按类型理解:

    • 整数成员得到整数零。
    • 浮点成员得到浮点零。
    • 指针成员得到空指针。

    不能简单把这种初始化等同于所有位机械地清零。

    这个规则不能理解成共用体未选择的成员也分别保存了零:

    union Data d = {.i = 10};

    这里只初始化了 i,不能说 f 也被初始化为 0.0f,因为它们共享存储空间。

    十、共用体不会自动记录当前成员

    共用体本身不会自动记录“当前存放的是 int 还是 float”。

    如果程序需要区分,可以另外保存标记:

    enum Type {
    INTEGER,
    FLOATING
    };

    struct Value {
    enum Type type;

    union {
    int i;
    float f;
    } data;
    };

    这里:

    • type 和 data 是结构体的不同成员,各有自己的空间。
    • data.i 和 data.f 是同一个共用体的成员,共享空间。

    写入时维护标记与数据的一致性:

    struct Value v;

    v.type = FLOATING;
    v.data.f = 3.5f;

    读取时根据标记选择成员:

    if (v.type == INTEGER) {
    printf("%d\\n", v.data.i);
    } else if (v.type == FLOATING) {
    printf("%f\\n", v.data.f);
    }

    标记本身有什么特殊功能吗?

    没有。标记只是普通变量,没有自动管理共用体的功能。

    标记能影响程序运行行为,是因为程序把它用于 if 等判断;如果不这样使用,它就不会自动影响共用体的读写。不能把程序员写出的判断逻辑,当成标记本身自带的功能。

    因此:

    v.type = INTEGER;

    不会自动把浮点数转换成整数。

    v.data.i = 10;

    也不会自动把 v.type 更新为 INTEGER。

    标记与数据之间的联系,由程序员通过代码建立,一致性也需要自己维护。

    标记并不是使用共用体的必要条件。如果程序通过其他方式知道应该读取哪个成员,也可以不设置标记。

    以上就是本篇全部内容,结构体、枚举、共用体让C能描述更复杂的数据和关系,从而支撑更复杂的功能。如有帮助,还请点赞关注,感谢支持。

    学习路上,我们常在。

    赞(0)
    未经允许不得转载:171主机测评 » 一文详解:C共用体
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址