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

结构体的成员本身可以是另一个结构体——这叫嵌套。访问时就像进套间一样,一层一层往里点:s.birth.year(s 的 birth 的 year)。初始化时也要再套一层花括号:{"张三", 18, 92.5, {2008, 5, 12}}。嵌套的价值是把"一条记录"再拆成有意义的小块——"生日"本身就是年、月、日三件套,硬塞三个 int 进学生结构体就散了。

📝 本节和前面几节的关系

8.1.1~8.1.3 让"一条记录"成为整体。这一节把"整体"递归地用起来:既然结构体是"一条记录",那么"一条记录里的某一项如果本身也是一条记录",就自然地嵌进去。这个思路和 7.2 的二维数组有点像(格子里还能有结构),但结构体的嵌套自由度更高——每一层可以装不同的东西。8.1.5 会算它的总大小,8.3/8.4 会让它进函数、进数组。

一、什么时候需要嵌套

假设要记学生的出生日期。最直接的做法是塞三个成员:

struct Student {
    char name[20];
    int  age;
    double score;
    int  year, month, day;      // ← 三个平铺的 int
};

能用,但有三个不爽:

  1. 语义散了——year/month/day 是"生日"的三件套,现在和学生姓名平级,看不出从属关系;
  2. 没法复用——以后"商品上架日期"、"订单创建时间"又要重写一遍这三个 int;
  3. 没法整体搬运——想把"生日"传给一个"判断是否成年"的函数,得传三个参数。

只要有"这三个数据是一伙的"这种感觉,就该给它单独定义一个结构体。

二、定义:先内后外

struct Date {                 // ① 先定义"日期"这个小结构体
    int year;
    int month;
    int day;
};

struct Student {              // ② 再定义"学生",把 Date 当成员
    char   name[20];
    int    age;
    double score;
    Date   birth;             // ← 成员类型是 Date(8.1.1 里"先定义类型再用"的用法)
};

顺序不能颠倒:Date 必须写在 Student 前面,否则编译器不认识这个词。

类型名可以直接当成员类型用:这里的 Date birth; 和我们之前写 int age; 是同一种语法——结构体名一旦定义好,就等同于一个内置类型名。这是结构体"可以嵌套"的根本原因。

三、访问:一层一层点进去

Student s;
strcpy(s.name, "张三");
s.age   = 18;
s.score = 92.5;

s.birth.year  = 2008;         // 进两层:s → birth → year
s.birth.month = 5;
s.birth.day   = 12;

读作"s 的 birth 的 year",从左往右一层层剥。

两种写法其实等价(把中间结果拿出来也能用):

s.birth.month = 5;              // 写法 A:一串点到底
Date &d = s.birth;              // 写法 B:先取出内层(引用,7.8.1)
d.month = 5;                    //        再点它

写法 B 的好处:当嵌套很深(a.b.c.d.e)或者要反复访问内层时,代码短且不容易写错。注意要用引用 Date &d,否则复制一份出来、改的是副本(6.2.2 的老教训)。

四、初始化:花括号套花括号

Student s = {"张三", 18, 92.5, {2008, 5, 12}};
//                              ↑ 内层结构体自己一对花括号

规则和数组嵌套一致:遇到"成员是结构体",就给它再开一对花括号,里面的值按内层成员顺序排。

可以只填外层、内层留空:

Student s = {"张三", 18};       // birth 三个成员自动补 0 → 0年0月0日
Student t = {};                 // 全清零(含 birth)

输入时要进到最里层:

cout << "请输入出生年 月 日:";
cin >> s.birth.year >> s.birth.month >> s.birth.day;

五、嵌套能有多深?以及一条死路

层级没有硬性限制:

struct Time   { int h, m, s; };
struct Log    { Time  when;  int code; };
struct Order  { Log   record; char item[20]; };
// 访问: order.record.when.h

但有一条死路:

struct Node {
    int  value;
    Node next;          // ❌ 错:自己包含自己 → 大小无穷大,编译器直接拒绝
};

为什么不行?sizeof(Node) = 4 + sizeof(Node),方程无解。

但"包含自己的指针"可以:

struct Node {
    int   value;
    Node *next;         // ✅ 指针大小固定(4 或 8 字节),没问题
};

这正是链表的基础——指针让"自己套自己"从死路变成通途(第七章的指针在这里第一次派上这种用场)。

六、动手 Demo:嵌套结构体的造、填、看

#include <iostream>
#include <cstring>
using namespace std;

struct Date {
    int year;
    int month;
    int day;
};

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

int main() {
    // ① 嵌套初始化:花括号套花括号
    Student s1 = {"张三", 18, 92.5, {2008, 5, 12}};
    cout << s1.name << " 生日:" << s1.birth.year << "-"
         << s1.birth.month << "-" << s1.birth.day << endl;

    // ② 逐层赋值
    Student s2;
    strcpy(s2.name, "李四");
    s2.age          = 19;
    s2.score        = 85.0;
    s2.birth.year   = 2007;
    s2.birth.month  = 11;
    s2.birth.day    = 3;
    cout << s2.name << " 生日:" << s2.birth.year << "-"
         << s2.birth.month << "-" << s2.birth.day << endl;

    // ③ 用引用简化深层访问
    Date &d = s1.birth;
    d.day = 20;                                       // 就等于 s1.birth.day = 20
    cout << "改过之后:" << s1.birth.year << "-"
         << s1.birth.month << "-" << s1.birth.day << endl;

    // ④ 内层结构体本身多大
    cout << "sizeof(Date) = " << sizeof(Date) << endl;
    return 0;
}

看到什么算成功:

张三 生日:2008-5-12
李四 生日:2007-11-3
改过之后:2008-5-20
sizeof(Date) = 12

读结果:Date 三个 int 是 12 字节——"生日"三件套被干净地打包成了一个 12 字节的小块,这就是嵌套最直接的价值。

至于 Student 一共多大,它并不等于 20+4+8+12 的简单相加,想知道答案得先把下一节的"对齐"搞清楚。

30 秒实验:把 Student 里的 Date birth; 挪到 char name[20]; 前面,重新跑一遍看 sizeof(Student) 变不变。如果变了,就说明成员的先后顺序真的会影响结构体大小。

七、常见误区

  1. "内层结构体要写在后面也行"——必须先内后外。编译器从头往下读,用到 Date 时它必须已经存在。实在要互相引用,就得用 6.1.2 的"先声明"——但结构体的互相嵌套多靠指针解决(如上面的 Node *next)。
  2. "s.birth.month 可以简写成 s.month"——不行。每一层都必须点到位,编译器不会帮你"穿透"。
  3. "Date d = s.birth; 之后再改 d,s.birth 也会变"——不会。Date d 是复制一份出来;要改原件得用引用 Date &d = s.birth;(7.8.1 的规则)。
  4. "结构体可以包含自己"——值成员不行(大小无解),指针成员可以(大小固定)。这是链表、树等数据结构的入口。
  5. "嵌套越深越好"——过深的嵌套(a.b.c.d.e)会让代码难读、访问啰嗦。一般超过三层就该考虑拆分或改用引用取中间层。
自测三问(点开对答案) 1. 结构体 `Student` 有一个 `Date birth;` 成员,怎么访问它的年份?——`s.birth.year`(一层层点进去)。 2. 嵌套结构体初始化怎么写?——花括号套花括号:`{"张三", 18, 92.5, {2008, 5, 12}}`。 3. 为什么结构体不能包含"自己的值",但能包含"自己的指针"?——值成员会让大小无穷大(`sizeof(Node) = 4 + sizeof(Node)`,无解);指针大小固定,所以可以,这也是链表的基础。

相关笔记