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

第八章讲的是"怎么把数据组织成有意义的一块"。三件工具:结构体(把不同类型并排打包成一条记录)、共用体(让几个成员重叠用同一块内存)、枚举(给一组整数起名字)。它们共同的关键词是"打包"与"命名"——这正是从"写代码"走向"建模型"的第一步:先能描述"一个学生由什么组成",才谈得上管理"一群学生"。

📝 本节和前面几节的关系

这一章把前七章的"零件"组装成了"模型":类型(第二章)、表达式与转换(第三章)、分支与循环(第四、五章)、函数(第六章)、数组与指针(第七章)——全部用上了。而下一章要在这个地基上盖更高一层:类。结构体就是"类的雏形"——它已经有"把数据打包"的能力,只差"把操作数据的方法也打包进去"和"把数据藏起来"(封装),那就是 class。

一、本章知识地图

块 小节 一句话要点
结构体基础 8.1 / 8.1.1 / 8.1.2 / 8.1.3 / 8.1.4 / 8.1.5 struct 名 { 成员 };——末尾分号别漏;定义只是画图纸、声明变量才占内存;成员访问用 .;同类型可整体赋值 s2 = s1;;嵌套要一层层点(s.birth.y);大小 ≠ 成员相加(对齐填充)
重命名类型 8.2 typedef 原类型 新名字;——新名在最后;它是编译器处理的类型别名,和 #define 的文本替换完全不同(PChar a, b; 两个都是指针)
结构体与函数 8.3 / 8.3.1 / 8.3.2 三种交法:传值(复制、改不动)/ 传指针(p->x + &stu,改原件、省拷贝)/ 传引用(s.x + stu,最干净)。只读大对象首选 const T &
结构体数组 8.4 / 8.4.1 / 8.4.2 一张表格,cls[i].成员 两段式访问;下标天然对齐;排序要交换整条记录;数组名=首地址,p++ 跳 sizeof(Student);传参长度必须另传
共用体 8.5 / 8.5.1 / 8.5.2 / 8.5.3 成员共用一块内存(地址相同);大小取最大(再对齐);一次只有一个有效——要安全就配 tag 标签
枚举 8.6 / 8.6.1 / 8.6.2 / 8.6.3 给整数起名字,默认 0 起自动递增;不能用 int 直接赋值、不能 cin;枚举值共享命名空间(不同枚举别重名);配 switch 最漂亮

二、必须记住的七个结论

  1. 结构体定义只是"画图纸",声明变量才分配内存。定义末尾的分号是硬伤级别的坑(8.1.1)。
  2. 访问成员:变量用 .,指针用 ->。p->x ≡ (*p).x;不能写 *p.x(. 优先级高于 *)。
  3. sizeof(结构体) ≠ 成员大小相加。两条对齐规则:① 成员起点是自身大小的整数倍;② 总大小是最宽成员的整数倍。按大小从大到小排最省空间(8.1.5)。
  4. 三种交法各有位置:只读小对象用传值;要改原件用指针或引用;只读大对象用 const T &。结构体越大,"复制"越贵。
  5. 结构体数组就是一张表:cls[i].成员;排序交换的是整条记录(不是某个字段);p++ 在结构体指针上是跳 sizeof(结构体) 字节;传函数时长度必须另传(数组退化成指针)。
  6. 共用体取最大、只留一个:成员地址相同,后写的覆盖先写的;读之前必须知道当前有效的是谁(配 tag)。和结构体的区别是"重叠 vs 并排"。
  7. 枚举是"起了名字的整数":默认 0 起、可自定义;能比较、能算术(结果转 int)、能当数组下标、最适合配 switch;不能直接赋 int、不能 cin。

三、动手 Demo:两关自测

第一关(写程序):一个简易成绩管理系统

要求:用结构体数组存 5 个学生(姓名、年龄、成绩),实现四个功能——

  1. 用指针遍历打印全班(const Student *p 做形参);
  2. 算出平均分和最高分(最高分要能说出是谁);
  3. 按成绩从高到低排序(交换整条记录);
  4. 用指针修改某个学生的成绩(写 void setScore(Student *p, double v))。
#include <iostream>
#include <cstring>
using namespace std;

struct Student {
    char   name[20];
    int    age;
    double score;
};

// ① 指针遍历打印(只读 → const 指针)
void printAll(const Student *p, int n) {
    for (int i = 0; i < n; i++)
        cout << "  " << (p + i)->name << "\t" << (p + i)->age
             << "\t" << (p + i)->score << endl;
}

// ② 平均分
double average(const Student *p, int n) {
    double sum = 0;
    for (int i = 0; i < n; i++) sum += (p + i)->score;
    return sum / n;
}

// ③ 返回最高分那条记录的地址(安全:指向调用方的数组)
const Student *best(const Student *p, int n) {
    const Student *m = p;
    for (int i = 1; i < n; i++)
        if ((p + i)->score > m->score) m = p + i;
    return m;
}

// ④ 用指针改成绩
void setScore(Student *p, double v) {
    p->score = v;
}

int main() {
    Student cls[5] = {
        {"张三", 18, 92.5},
        {"李四", 19, 85.0},
        {"王五", 20, 78.5},
        {"赵六", 18, 58.0},
        {"孙七", 19, 66.5}
    };
    int n = sizeof(cls) / sizeof(cls[0]);

    cout << "=== 原始名单 ===" << endl;
    printAll(cls, n);

    cout << "平均分 = " << average(cls, n) << endl;

    const Student *top = best(cls, n);
    cout << "最高分:" << top->name << " " << top->score
         << "(在第 " << (top - cls) << " 格)" << endl;      // 指针相减算下标

    cout << "=== 给赵六改成 70 ===" << endl;
    setScore(&cls[3], 70);                                    // 传地址
    printAll(cls, n);

    cout << "=== 按成绩从高到低排序 ===" << endl;
    for (int i = 0; i < n - 1; i++)
        for (int j = 0; j < n - 1 - i; j++)
            if (cls[j].score < cls[j+1].score) {
                Student t = cls[j]; cls[j] = cls[j+1]; cls[j+1] = t;
            }
    printAll(cls, n);

    cout << "sizeof(Student) = " << sizeof(Student)
         << ",整排 = " << sizeof(cls) << " 字节" << endl;
    return 0;
}

看到什么算成功:

=== 原始名单 ===
  张三    18  92.5
  李四    19  85
  王五    20  78.5
  赵六    18  58
  孙七    19  66.5
平均分 = 76.1
最高分:张三 92.5(在第 0 格)
=== 给赵六改成 70 ===
  张三    18  92.5
  李四    19  85
  王五    20  78.5
  赵六    18  70
  孙七    19  66.5
=== 按成绩从高到低排序 ===
  张三    18  92.5
  李四    19  85
  王五    20  78.5
  赵六    18  70
  孙七    19  66.5
sizeof(Student) = 32,整排 = 160 字节

检查清单: - ① 只读函数形参用了 const Student *(7.8.2 的好习惯); - ② 返回的是数组内部的地址(安全),调用方用 top - cls 算下标(7.4.3); - ③ 排序交换的是整条记录(Student t = cls[j];),不是只换分数; - ④ setScore(&cls[3], 70) 传的是地址,所以原件真的改了。

第二关(找错):八处问题

下面这段有八处问题,找出来并说明原因:

struct Point { int x; int y }                    // ①

struct Student {
    char name[20];
    int  age;
};

void init(Student s) {                           // ②
    s.name = "张三";                              // ③
    s.age  = 18;
}

void print(Student *p) {
    cout << p.name << endl;                      // ④
}

int main() {
    Student a = {"张三", 18};
    Student b = a;
    init(a);
    print(&a);

    Student cls[3] = {"张三", 18, "李四", 19, "王五", 20};   // ⑤
    cout << cls[3].name << endl;                              // ⑥

    cout << sizeof(Student) << endl;              // ⑦:以为等于 20 + 4

    union U { int i; double d; };
    U u;
    u.i = 100;
    u.d = 2.5;
    cout << u.i << endl;                          // ⑧
    return 0;
}

答案与理由:

# 问题 原因与修法
① 结构体定义末尾漏分号 struct Point { int x; int y };(8.1.1)。漏了会连累后面一整段报错
② init(Student s) 是传值 函数里怎么改都改不到原件(8.3.1)。改成 void init(Student &s) 或 Student *p 配 &a
③ s.name = "张三"; name 是字符数组,不能用 =(7.3、8.1.3)。改成 strcpy(s.name, "张三");(并且要 #include <cstring>)
④ p.name p 是指针,必须用 ->:p->name(8.3.2)。. 是给变量用的
⑤ 平铺初始化,没写内层花括号 应该写 {{"张三",18},{"李四",19},{"王五",20}}(8.4.1)。平铺写法位置全靠数,极易错
⑥ cls[3] 越界 3 个元素的合法下标是 0~2。越界不报错,会踩坏相邻内存(7.1.2、8.4.1)
⑦ 以为 sizeof(Student) = 20 + 4 = 24 错。要考虑对齐填充(8.1.5)。实际值以机器实测为准(本例常见为 24,但不能靠相加去猜)
⑧ 读错共用体成员 写了 u.d 之后 u.i 已经失效(8.5.2)。要么只读"最后写的那个",要么配 tag 标签

30 秒实验:把上面八处一一改对,跑一遍。特别试一下 ⑥:把 cls[3].name 改成 cls[2].name——程序从"可能崩"变成"正常",但你不会看到任何警告。"不报错的错"才是真危险,这是第八章最该记住的一句话。

四、常见误区(本章汇总)

  1. "结构体就是几个变量放一起"——它的价值在于"一条记录变成一个可整体搬运的东西":能整体赋值、能进函数、能进数组、能被指针指着。这是"数据建模"的起点(下一章的类就长在它身上)。
  2. "结构体大小就是成员相加"——不是,还有对齐填充(8.1.5)。这也是笔试高频题。
  3. "-> 和 . 差不多,随便用"——看左边是什么:变量用 .、指针用 ->。这是本章一半编译错误的来源。
  4. "共用体只是省内存的结构体"——它是"一次只能放一个"的容器,成员互斥。当"省空间的结构体"用,必然读到垃圾。
  5. "枚举是字符串"——枚举值是整数,名字只活在源码里(8.6)。所以 cout 出来是数字,也没法 cin。
  6. "结构体和指针/引用无关"——恰恰相反:本章所有"能改原件"的场景(8.3.2、8.4.2)都靠第七章的指针和引用。第七章是地基,第八章是盖在上面的第一层楼。
自测八问(点开对答案) 1. 定义结构体时最容易漏什么?——末尾的**分号**。 2. `p->x` 展开成"解引用 + 点"怎么写?为什么不能写 `*p.x`?——`(*p).x`;因为 `.` 优先级高于 `*`,`*p.x` 会被当成 `*(p.x)`。 3. `sizeof(Student)` 为什么可能大于成员相加?——**内存对齐**:成员起点要满足自身对齐要求,总大小要是最宽成员的整数倍,所以中间和末尾都可能垫空白。 4. 三种传参各适合什么场合?——传值(只读、小结构体)、传指针(要改、或可能为空)、传引用(要改、写法最干净);**只读大对象首选 `const T &`**。 5. 结构体数组按成绩排序时,`Student t = cls[j];` 这句为什么必要?——因为姓名、年龄、成绩是**同一条记录**,必须整条交换,否则名字和分数会错配。 6. 共用体的三条特点是什么?——① 成员共用同一块内存(地址相同);② 一次只有一个有效(后写覆盖先写);③ 大小取最大成员(再对齐)。 7. 什么时候用 `union` 而不是 `struct`?——数据**互斥、同一时刻只用其中一个**时(协议字段、类型可变的记录、省内存场合)。 8. 枚举值能不能直接参与比较和算术?有什么限制?——能比较(六个关系运算符都行);算术的结果是 **`int`**,要存回枚举变量必须**强转**,而且**强转可能越界**,转之前先判断范围。

相关笔记