🔷 第八章 结构体与共用体
📄 一句话概括

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 表的"活的"演示。

五、常见误区

  1. "共用体大小 = 成员大小相加"——错,那是结构体。共用体取最大(再对齐)。
  2. "写完 d.f 还能读 d.i"——不能。读出来的是"被搅乱的字节"。这是共用体最核心的纪律:只在你知道当前有效成员时才读它。
  3. "d.c = 'A' 只影响 1 个字节,其他成员还好好的"——错。那一格内存被改了,以它为基础的整个 int/float 都不再是原来的值(int 由 4 个字节拼成,改一个就变了)。
  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`),写的时候记下类型,读的时候**先看标签再取对应成员**。

相关笔记