第 27 章 学生档案袋——结构体 struct
🏗️ 前情回顾:递归打开了新世界——函数可以自己调用自己解决复杂问题。但数据和变量还是散的:一个学生有名字、年龄、分数三个信息,难道要声明三个数组分别存?
names[i]、ages[i]、scores[i]——明明是同一个人,信息却分在三处,一不小心就对错了行。能不能像档案袋一样,把同一个人的信息打包在一起?这一章的主角——结构体 struct,就是你要的档案袋。
🎯 本章目标
学完这一章,你能:
- 用
struct定义自己的复合数据类型 - 创建结构体变量并用
.访问成员 - 使用结构体数组管理多条记录
- 用结构体指针和
->运算符间接访问成员 - 把结构体作为函数参数传递
- 了解构造函数的基本写法
📖 故事引入
班主任王老师要给全班 45 个学生建一个电子档案。每个学生有三个信息:姓名(string)、年龄(int)、期末分数(double)。
如果用之前的知识,她需要这样写:
string name1, name2, ..., name45; // 45 个名字变量
int age1, age2, ..., age45; // 45 个年龄变量
double score1, score2, ..., score45; // 45 个分数变量光是声明就要写 135 行!而且 name3、age3、score3 明明是同一个人的信息,却分在三处,一不小心就搞混——把 name3 和 age5 配对了都不知道。
王老师想:教务处不是给每个学生一个档案袋吗?姓名、年龄、分数全塞进一个袋子,袋子上写学生名字。每个学生一个袋子,清清爽爽,不会搞混。
C++ 的 struct(结构体)就是这个"档案袋"——把属于同一个对象的信息打包成一个整体。
🧱 知识讲解
27.1 定义结构体:创造你自己的类型
struct 让你把多个不同类型的数据"打包"成一个新的类型:
❌ 结构体定义常见错误——你对号入座了吗?
cppstruct Student { // ❌ 忘了写成员类型 name; // ❌ 缺少类型声明 int age double score; // ❌ 上一行少了分号 } // ❌ 结尾少了分号!三个错误:①成员必须有类型 ②每个成员声明后要加分号 ③结构体定义的
}后必须加分号
✅ 正确的结构体定义:
struct Student {
string name; // 成员:姓名
int age; // 成员:年龄
double score; // 成员:分数
}; // ← 注意这里有分号!拆开看:
| 部分 | 含义 |
|---|---|
struct | 关键字:我要定义一个结构体 |
Student | 结构体的名字(你起的,以后就是一种新类型) |
{ ... } | 花括号里列出所有成员 |
; | 结构体定义结束必须有分号 |
现在 Student 就和 int、double 一样,是一种类型了!你可以这样声明变量:
Student s1; // s1 是一个 Student 类型的变量
Student s2; // s2 是另一个27.2 访问成员:用 .
创建了结构体变量后,用 . 运算符访问它的成员:
Student s1;
s1.name = "小明";
s1.age = 13;
s1.score = 96.5;
cout << s1.name << " 今年 " << s1.age << " 岁,考了 " << s1.score << " 分" << endl;也可以声明时直接初始化(按成员顺序):
Student s2 = {"小红", 14, 92.0}; // 依次是 name, age, score
// 或者用 C++11 的指定初始化(更清晰):
Student s3 = {.name = "小刚", .age = 13, .score = 88.5};27.3 结构体数组:管理多条记录
回到王老师的场景——现在只要一个数组就够了:
Student class1[45]; // 45 个学生的档案,每个是一个 Student
class1[0] = {"小明", 13, 96.5};
class1[1] = {"小红", 14, 92.0};
class1[2] = {"小刚", 13, 88.5};
// ...
// 遍历全班,输出每个人的信息
for (int i = 0; i < 45; i++) {
cout << class1[i].name << ": " << class1[i].score << " 分" << endl;
}对比之前要写 135 个变量——现在只用一个结构体定义 + 一个数组,干净利落!
27.4 结构体指针:用 -> 运算符
既然结构体是一种类型,那自然可以有指向结构体的指针:
Student s = {"小明", 13, 96.5};
Student* p = &s; // p 指向 s
// 访问成员的方式一:先解引用再用 . ——有点啰嗦
cout << (*p).name << endl;
// 访问成员的方式二:用 -> 运算符 ——简洁!
cout << p->name << endl; // 等价于 (*p).name
cout << p->age << endl; // 等价于 (*p).age
cout << p->score << endl; // 等价于 (*p).score
// 通过指针修改成员
p->score = 99.0; // s.score 变成了 99.0-> 读作"箭头运算符"或"指向运算符"。p->name 的意思是"p 指向的那个结构体的 name 成员"。
💡 口诀:
.是"的"(s.name = s 的 name),->是"指向的"(p->name = p 指向的结构体的 name)。
27.5 结构体作函数参数
你可以把结构体传给函数,就像传 int 一样:
传递结构体本身(值传递——会复制一份):
void printStudent(Student s) {
cout << s.name << " " << s.age << " " << s.score << endl;
}但如果结构体很大,复制一份开销也大。更好的做法是用指针(或引用):
传递指针(只传地址,不复制数据):
void printStudent(Student* p) {
cout << p->name << " " << p->age << " " << p->score << endl;
}
// 调用:
Student s = {"小明", 13, 96.5};
printStudent(&s); // 传地址传指针 + 修改原数据:
void addScore(Student* p, double bonus) {
p->score += bonus; // 直接修改原来的分数
}
Student s = {"小明", 13, 90};
addScore(&s, 5.5);
cout << s.score; // 输出 95.527.6 构造函数入门
有时候你想在创建结构体变量时自动做一些初始化工作——比如把分数默认设为 0、年龄默认设为 0。这时候可以写一个构造函数:
struct Student {
string name;
int age;
double score;
// 构造函数:名字和结构体名相同,没有返回类型
Student() {
name = "未命名";
age = 0;
score = 0.0;
}
// 带参数的构造函数
Student(string n, int a, double s) {
name = n;
age = a;
score = s;
}
};
// 使用:
Student s1; // 调用无参构造函数 → 默认值
Student s2("小明", 13, 96.5); // 调用带参构造函数构造函数的名字必须和结构体名相同,没有返回类型(连 void 都不写)。有了构造函数,创建对象时就能自动完成初始化——安全又方便。
27.7 综合示例:学生成绩管理系统
#include <iostream>
#include <string>
using namespace std;
struct Student {
string name;
int age;
double score;
Student() { name = ""; age = 0; score = 0; }
Student(string n, int a, double s) : name(n), age(a), score(s) {}
};
double averageScore(Student arr[], int n) {
double sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i].score;
}
return sum / n;
}
void printTopStudent(Student arr[], int n) {
Student* top = &arr[0];
for (int i = 1; i < n; i++) {
if (arr[i].score > top->score) {
top = &arr[i];
}
}
cout << "最高分:" << top->name << " " << top->score << " 分" << endl;
}
int main() {
Student class1[3] = {
{"小明", 13, 96.5},
{"小红", 14, 92.0},
{"小刚", 13, 99.0}
};
cout << "平均分:" << averageScore(class1, 3) << endl;
printTopStudent(class1, 3);
return 0;
}✋ 动手试试
试试 1:定义一个 Book 结构体,包含书名(string)、价格(double)、页数(int)。创建 3 本书的结构体变量,分别用 . 赋值,然后输出。
试试 2:创建一个 Student 结构体数组,存入 5 个学生的信息。写函数 findByName(Student arr[], int n, string target),用指针遍历数组找到目标学生并输出他的信息。
试试 3:写一个 Point 结构体(含 x, y 两个 int 成员),写函数 double distance(Point* a, Point* b),计算两点之间的距离。
试试 4:给 Student 结构体添加构造函数,使得不传参数时默认名字为"无名"、年龄为 0、分数为 0。测试两种构造方式。
🦶 你踩过这些坑吗
逐一核对,看你是不是也中过招:
- [ ] 忘记结构体定义末尾的分号:
struct Student { ... }← 少了分号,编译错误 → ✅struct Student { ... }; - [ ] 用
.操作结构体指针:Student* p = &s; p.name = "小明";← p 是指针,不能用.→ ✅ 指针用->:p->name = "小明"; - [ ] 结构体数组传参时忘了类型:
void func(Student arr)← 只传一个 Student,不是数组 → ✅void func(Student arr[], int n) - [ ] 构造函数写了返回类型:
void Student() { ... }← 构造函数不能有返回类型 → ✅Student() { ... }
📝 练习
基础题
1. 填空题
(1)定义结构体的关键字是 ____。
(2)访问结构体变量的成员用 ____ 运算符,访问结构体指针的成员用 ____ 运算符。
(3)结构体定义末尾必须加 ____。
(4)构造函数的名字必须与 ____ 相同,且不能有 ____ 类型。
2. 读代码写结果
struct Point { int x; int y; };
Point p1 = {3, 4};
Point* ptr = &p1;
ptr->x = 10;
cout << p1.x << " " << p1.y << endl;输出是什么?
3. 选择题
(1)以下关于结构体的说法,正确的是?
A. 结构体的成员必须是同一类型 B. 结构体变量不能作为函数参数
C. 结构体是一种自定义数据类型 D. 结构体只能包含数据,不能包含函数
(2)以下代码的输出是?
struct S { int a; int b; };
S s = {1, 2};
S* p = &s;
p->a = 5;
cout << s.a + s.b;A. 3 B. 7 C. 8 D. 编译错误
4. 判断题
(1)结构体变量可以在声明时用 {} 初始化成员。( )
(2)构造函数可以有返回类型 void。( )
提高题
5. 编程题 — 矩形结构体
定义 Rect 结构体(左上角 x, y 和宽 w, 高 h),写函数 int area(Rect r) 计算面积,写函数 bool contains(Rect r, int px, int py) 判断点 (px, py) 是否在矩形内部。
6. 编程题 — 分数统计
输入 N 个学生的姓名和三科分数,用结构体数组存储。输出每个学生的总分,以及全班的总分排名(从高到低)。用结构体指针完成比较和交换。
7. 编程题 — 日期结构体
定义 Date 结构体(年、月、日),写函数 bool isEarlier(Date a, Date b) 判断 a 是否早于 b,写函数 int daysInMonth(int year, int month) 返回该月天数(注意闰年 2 月)。
挑战题
8. 编程题 — 链表节点
定义结构体 Node,包含一个 int data 和一个 Node* next。创建三个节点,让它们首尾相连(node1.next = &node2; node2.next = &node3; node3.next = nullptr;),然后写一个循环"沿着 next 指针走",依次输出每个节点的 data。
(提示:这就是数据结构中"链表"的雏形——用结构体里的指针把数据串成一条链。)
9. 编程题 — 学生成绩排序
定义 Student 结构体(姓名、总分),输入 N 个学生的姓名和三科成绩,计算总分,然后按总分从高到低排序输出。要求用结构体数组和冒泡排序。
示例:
输入:
3
小明 85 90 88
小红 92 95 91
小刚 78 82 80
输出:
小红 278
小明 263
小刚 240🧠 本章小结
struct 结构体名 {
类型1 成员1;
类型2 成员2;
...
构造函数() { ... } // 可选:自动初始化
}; // ← 别忘了分号!
访问成员:
变量名.成员 → s.name
指针->成员 → p->name (等价于 (*p).name)
结构体数组: struct名 数组名[大小];
结构体传参: 可以传值、传指针(效率更高)📝 配套练习
共7题。struct从定义→访问→比较→排序,为M6的sort+cmp打基础。。★核心(课堂必做) ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | J0106 | https://hydro.ac/d/srqc/p/J0106 | struct定义、成员访问、遍历 |
| ◆ 拓展 | J0107 | https://hydro.ac/d/srqc/p/J0107 | struct、嵌套比较、分数差 |
| ◆ 拓展 | J0108 | https://hydro.ac/d/srqc/p/J0108 | struct、条件判断、等级评定 |
| ◆ 拓展 | luogu-P5740 | https://hydro.ac/p/luogu-P5740 | struct、多成员、最值 |
| ◆ 拓展 | luogu-P5741 | https://hydro.ac/p/luogu-P5741 | struct、多关键字比较 |
| ◆ 拓展 | luogu-P5742 | https://hydro.ac/p/luogu-P5742 | struct、排序启蒙 |
| ◆ 拓展 | J0114 | https://hydro.ac/d/srqc/p/J0114 | struct数组、排序、分组 |
💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 6 道◆拓展题,覆盖不同变式和细节。
配套练习
共7题。struct从定义→访问→比较→排序,为M6的sort+cmp打基础。★核心(课堂必做) · ◆拓展(课后练习)
| 级别 | 题号 | 链接 | 覆盖知识点 |
|---|---|---|---|
| ★ 核心 | J0106 | https://hydro.ac/d/srqc/p/J0106 | struct定义、成员访问、遍历 |
| ◆ 拓展 | J0107 | https://hydro.ac/d/srqc/p/J0107 | struct、嵌套比较、分数差 |
| ◆ 拓展 | J0108 | https://hydro.ac/d/srqc/p/J0108 | struct、条件判断、等级评定 |
| ◆ 拓展 | luogu-P5740 | https://hydro.ac/p/luogu-P5740 | struct、多成员、最值 |
| ◆ 拓展 | luogu-P5741 | https://hydro.ac/p/luogu-P5741 | struct、多关键字比较 |
| ◆ 拓展 | luogu-P5742 | https://hydro.ac/p/luogu-P5742 | struct、排序启蒙 |
| ◆ 拓展 | J0114 | https://hydro.ac/d/srqc/p/J0114 | struct数组、排序、分组 |
练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 6 道◆拓展题,覆盖不同变式和细节。
自查清单:
- [ ] 我能用
struct定义自己的复合数据类型 - [ ] 我会用
.访问结构体变量的成员 - [ ] 我会用
->通过指针访问结构体成员 - [ ] 我能创建和使用结构体数组
- [ ] 我知道如何把结构体传给函数(值传递 vs 指针传递)
- [ ] 我了解构造函数的基本写法
- [ ] 我记得结构体定义末尾要加分号
🚀 下章预告:结构体把数据打包得整整齐齐——一个学生的姓名、年龄、分数全在一个袋子里。但它在内存的哪个位置?能不能直接找到地址绕过去操作?第 28 章,我们用指针这门"GPS 导航",直接定位内存中的任意数据。