sizeof(共用体) = 最大的那个成员的大小(再按 8.1.5 的对齐规则向上取整)。因为所有成员共享同一块内存,这块内存必须装得下最大的那一个。推论很重要:同一时刻只有一个成员是"有效"的——最后写谁,就只有谁有意义,其他成员读出来是"被搅乱的字节"。教材实例05就在这里。
这是"内存布局"这条线索的最后一个落点:8.1.5 讲了结构体的"对齐填充"(成员并排,还要垫空白),本节讲共用体的"重叠复用"(成员叠着放,取最大的)。两节的规则合起来,就能算出任何复合类型的大小。 实例05 把"同一块内存的多种解读"做成一个能跑起来的实用小工具(也顺手给 8.6 的枚举做了铺垫)。
一、规则:取最大,再对齐
union Data { int i; float f; char c; };
cout << sizeof(Data); // 4 —— 取最大的 int(4 字节)
演算逻辑:
| 成员 | 大小 |
|---|---|
int i |
4 |
float f |
4 |
char c |
1 |
| 取最大 | 4 |
为什么必须取最大?因为三个成员共用同一个起始地址。如果这块内存只有 1 字节(按 char 算),那么写 int 的时候就会写出去——越界写,后果比 7.1.2 的数组越界更严重。
再来一个要"向上取整"的例子:
union U {
char c[10]; // 10 字节
int i; // 4 字节
};
cout << sizeof(U); // 12,不是 10
为什么是 12?
1. 取最大成员:char[10] 是 10 字节 → 这块空间要 10 字节;
2. 但还要满足整体对齐(8.1.5 规则 2):最宽成员 int 要求对齐 4 → 10 向上取到 12。
所以完整规则是:
共用体大小 = max(各成员大小),再向上补齐到"最大对齐要求"的整数倍。
二、同一时刻只有一个成员"有效"
这是共用体最需要建立的概念。看这段代码:
union Data d;
d.i = 100; // 这时"d 是个 int"
d.f = 3.14f; // 现在"d 变成了 float"——刚才的 100 没了
d.c = 'A'; // 现在"d 变成了 char"——3.14 也没了
内存里发生的事(假设 d 的地址是 0x1000):
| 语句 | 0x1000 | 0x1001 | 0x1002 | 0x1003 | 当前"有效"的成员 |
|---|---|---|---|---|---|
d.i = 100 |
100 的 4 个字节 | ← | ← | ← | i |
d.f = 3.14f |
3.14 的 4 个字节(全部重写) | ← | ← | ← | f |
d.c = 'A' |
'A' | 3.14 剩下的 3 个字节 | ← | ← | c(而 f 已被破坏) |
注意第三行的细节:d.c = 'A' 只写了 1 个字节,剩下的 3 个字节还是 3.14f 的残留——所以这时读 d.f 得到的是一个"半新半旧"的垃圾值。
结论:"最后写的是谁,谁就有效;其他成员处于被破坏的状态,绝对不能读。"
三、用一个"类型标签"来管住它
既然共用体自己不知道当前有效的是哪个成员,那就在外面加一个标签:
struct Variant {
char tag; // 'i' 表示存的是整数,'f' 是小数,'c' 是字符
union {
int i;
double d;
char c;
} value; // 共用体作为成员(8.1.4 的嵌套)
};
用的时候"先看标签再取数据":
Variant v1, v2, v3;
v1.tag = 'i'; v1.value.i = 42;
v2.tag = 'f'; v2.value.d = 3.14;
v3.tag = 'c'; v3.value.c = 'A';
// 读的时候按 tag 决定看哪个成员(4.4 的 switch 正合适)
switch (v1.tag) {
case 'i': cout << v1.value.i << endl; break;
case 'f': cout << v1.value.d << endl; break;
case 'c': cout << v1.value.c << endl; break;
}
这个"标签 + 共用体"的组合,就是 C 语言里表示"任意类型值"的标准做法(后来的 std::variant 就是它的升级版)。记住这个模式——它是共用体在实际代码中最常见的形态。
四、动手 Demo(实例05 使用共用体变量)
教材实例05就是"使用共用体变量"。这里做成一个能同时算大小、能实测覆盖、还能演示标签模式的完整程序:
#include <iostream>
#include <cstring>
using namespace std;
// ① 三种数据类型互斥的共用体
union Data {
int i;
float f;
char c;
};
// ② 带标签的变体(共用体最实用的形态)
struct Variant {
char tag; // 'i' / 'f' / 'c'
union {
int i;
double d;
char c;
} value;
};
void showVariant(const Variant &v) {
switch (v.tag) {
case 'i': cout << " 整数:" << v.value.i << endl; break;
case 'f': cout << " 小数:" << v.value.d << endl; break;
case 'c': cout << " 字符:" << v.value.c << endl; break;
default: cout << " 未知类型" << endl;
}
}
int main() {
// === ① 大小观察 ===
cout << "=== 大小 === " << endl;
cout << "sizeof(union Data) = " << sizeof(Data) << endl; // 4
cout << "sizeof(Variant) = " << sizeof(Variant) << endl; // 1 + 8 → 16(含对齐)
union Big { char c[10]; int i; };
cout << "union Big{char[10]; int;} = " << sizeof(Big) << "(10 取整到 12)" << endl;
// === ② 覆盖实测 ===
cout << "=== 覆盖 ===" << endl;
Data d;
d.i = 100;
cout << "写 i = 100 → d.i = " << d.i << endl;
d.f = 3.14f;
cout << "写 f = 3.14 → d.f = " << d.f << endl;
cout << " d.i = " << d.i << "(被覆盖,无意义)" << endl;
d.c = 'A';
cout << "写 c = 'A' → d.c = " << d.c << endl;
cout << " d.f = " << d.f << "(也被破坏了)" << endl;
// === ③ 标签模式 ===
cout << "=== 标签模式 ===" << endl;
Variant v1, v2, v3;
v1.tag = 'i'; v1.value.i = 42;
v2.tag = 'f'; v2.value.d = 3.14159;
v3.tag = 'c'; v3.value.c = 'A';
cout << "v1:"; showVariant(v1);
cout << "v2:"; showVariant(v2);
cout << "v3:"; showVariant(v3);
// === ④ 同一块内存的两种解读 ===
cout << "=== 同一块内存,两种看法 ===" << endl;
union View {
int n;
char bytes[4];
};
View v;
v.n = 65;
cout << "v.n = " << v.n << ",按字符看第一个字节 = '" << v.bytes[0] << "'" << endl;
return 0;
}
看到什么算成功:
=== 大小 ===
sizeof(union Data) = 4
sizeof(Variant) = 16
union Big{char[10]; int;} = 12(10 取整到 12)
=== 覆盖 ===
写 i = 100 → d.i = 100
写 f = 3.14 → d.f = 3.14
d.i = (无意义的数)
写 c = 'A' → d.c = A
d.f = (被破坏)
=== 标签模式 ===
v1: 整数:42
v2: 小数:3.14159
v3: 字符:A
=== 同一块内存,两种看法 ===
v.n = 65,按字符看第一个字节 = 'A'
四个看点:
- sizeof(union Data) = 4(不是 1+4+4=9)——取最大;
- union Big = 12——10 字节取整到 12(对齐规则 8.1.5 在共用体里同样生效);
- 覆盖实测:写 f 之后 i 失效,写 c 之后 f 失效;
- 最后一行最妙:v.n = 65,而按字符看第一个字节得到 'A'——因为 65 就是 'A' 的 ASCII 码(2.2.4 的老知识)。同一块内存,用两种类型解读就得到两个"真相"。
sizeof(Variant)的 16 在老编译器上也可能是别的值,取决于double的对齐。重点不是这个数字,而是"它等于 max(1, 8) 再对齐",绝不是 1+8=9。
30 秒实验:把 union View 里的 v.n = 65; 改成 v.n = 66;,重新运行——最后一行变成 'B'。每次加 1,字符就往后挪一位,这就是 ASCII 表的"活的"演示。
五、常见误区
- "共用体大小 = 成员大小相加"——错,那是结构体。共用体取最大(再对齐)。
- "写完
d.f还能读d.i"——不能。读出来的是"被搅乱的字节"。这是共用体最核心的纪律:只在你知道当前有效成员时才读它。 - "
d.c = 'A'只影响 1 个字节,其他成员还好好的"——错。那一格内存被改了,以它为基础的整个int/float都不再是原来的值(int由 4 个字节拼成,改一个就变了)。 - "共用体没有实际用处,只是课本知识"——它在协议解析、二进制文件读写、嵌入式里是主力工具。本节第三部分的"标签 + 共用体"模式,就是 C 语言里表示"任意类型值"的标准做法。
自测三问(点开对答案)
1. `union U { char c[10]; int i; };` 的 `sizeof` 是多少?——**12**。取最大成员 10 字节,再按最宽成员(`int`,对齐 4)向上补齐到 12。 2. 写了 `d.f = 3.14f;` 之后,`d.i` 还等于之前赋的值吗?——不等。**共用体成员共享内存,后写的会覆盖先写的。** 3. 怎样让共用体"安全"地表示不同类型的值?——配一个**类型标签**(`tag`),写的时候记下类型,读的时候**先看标签再取对应成员**。- ☐ 8.5.3 共用体的特点(下一节)
相关笔记
- 共用体的定义与声明 —— 上一节:语法
- 共用体的特点 —— 下一节:三条特点汇总
- 结构体的大小 —— 对齐规则(共用体也要遵守)
- switch语句 —— 标签模式里按 tag 分支
- 🔷 C++基础 · 索引 —— 全课路线图