结构体的成员本身可以是另一个结构体——这叫嵌套。访问时就像进套间一样,一层一层往里点: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
};
能用,但有三个不爽:
- 语义散了——
year/month/day是"生日"的三件套,现在和学生姓名平级,看不出从属关系; - 没法复用——以后"商品上架日期"、"订单创建时间"又要重写一遍这三个
int; - 没法整体搬运——想把"生日"传给一个"判断是否成年"的函数,得传三个参数。
只要有"这三个数据是一伙的"这种感觉,就该给它单独定义一个结构体。
二、定义:先内后外
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) 变不变。如果变了,就说明成员的先后顺序真的会影响结构体大小。
七、常见误区
- "内层结构体要写在后面也行"——必须先内后外。编译器从头往下读,用到
Date时它必须已经存在。实在要互相引用,就得用 6.1.2 的"先声明"——但结构体的互相嵌套多靠指针解决(如上面的Node *next)。 - "
s.birth.month可以简写成s.month"——不行。每一层都必须点到位,编译器不会帮你"穿透"。 - "
Date d = s.birth;之后再改d,s.birth也会变"——不会。Date d是复制一份出来;要改原件得用引用Date &d = s.birth;(7.8.1 的规则)。 - "结构体可以包含自己"——值成员不行(大小无解),指针成员可以(大小固定)。这是链表、树等数据结构的入口。
- "嵌套越深越好"——过深的嵌套(
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)`,无解);指针大小固定,所以可以,这也是链表的基础。- ☐ 8.1.5 结构体的大小(下一节)
相关笔记
- 结构体成员及初始化 —— 上一节:初始化与赋值
- 结构体的大小 —— 下一节:为什么大小不等于成员相加
- 引用概述 —— 用引用取内层,避免复制
- 指针 —— "自己包含自己"为什么要靠指针
- 🔷 C++基础 · 索引 —— 全课路线图