Skip to content

第 27 章 学生档案袋——结构体 struct


🏗️ 前情回顾:递归打开了新世界——函数可以自己调用自己解决复杂问题。但数据和变量还是散的:一个学生有名字、年龄、分数三个信息,难道要声明三个数组分别存?names[i]ages[i]scores[i]——明明是同一个人,信息却分在三处,一不小心就对错了行。能不能像档案袋一样,把同一个人的信息打包在一起?这一章的主角——结构体 struct,就是你要的档案袋。

🎯 本章目标

学完这一章,你能:

  • struct 定义自己的复合数据类型
  • 创建结构体变量并用 . 访问成员
  • 使用结构体数组管理多条记录
  • 用结构体指针和 -> 运算符间接访问成员
  • 把结构体作为函数参数传递
  • 了解构造函数的基本写法

📖 故事引入

班主任王老师要给全班 45 个学生建一个电子档案。每个学生有三个信息:姓名(string)、年龄(int)、期末分数(double)。

如果用之前的知识,她需要这样写:

cpp
string name1, name2, ..., name45;  // 45 个名字变量
int age1, age2, ..., age45;        // 45 个年龄变量
double score1, score2, ..., score45; // 45 个分数变量

光是声明就要写 135 行!而且 name3age3score3 明明是同一个人的信息,却分在三处,一不小心就搞混——把 name3age5 配对了都不知道。

王老师想:教务处不是给每个学生一个档案袋吗?姓名、年龄、分数全塞进一个袋子,袋子上写学生名字。每个学生一个袋子,清清爽爽,不会搞混。

C++ 的 struct(结构体)就是这个"档案袋"——把属于同一个对象的信息打包成一个整体。


🧱 知识讲解

27.1 定义结构体:创造你自己的类型

struct 让你把多个不同类型的数据"打包"成一个新的类型:

结构体定义常见错误——你对号入座了吗?

cpp
struct Student {           // ❌ 忘了写成员类型
    name;                  // ❌ 缺少类型声明
    int age
    double score;          // ❌ 上一行少了分号
}                           // ❌ 结尾少了分号!

三个错误:①成员必须有类型 ②每个成员声明后要加分号 ③结构体定义的 } 后必须加分号

✅ 正确的结构体定义:

cpp
struct Student {
    string name;    // 成员:姓名
    int age;        // 成员:年龄
    double score;   // 成员:分数
};  // ← 注意这里有分号!

拆开看:

部分含义
struct关键字:我要定义一个结构体
Student结构体的名字(你起的,以后就是一种新类型)
{ ... }花括号里列出所有成员
;结构体定义结束必须有分号

现在 Student 就和 intdouble 一样,是一种类型了!你可以这样声明变量:

cpp
Student s1;    // s1 是一个 Student 类型的变量
Student s2;    // s2 是另一个

27.2 访问成员:用 .

创建了结构体变量后,用 . 运算符访问它的成员:

cpp
Student s1;
s1.name = "小明";
s1.age = 13;
s1.score = 96.5;

cout << s1.name << " 今年 " << s1.age << " 岁,考了 " << s1.score << " 分" << endl;

也可以声明时直接初始化(按成员顺序):

cpp
Student s2 = {"小红", 14, 92.0};  // 依次是 name, age, score
// 或者用 C++11 的指定初始化(更清晰):
Student s3 = {.name = "小刚", .age = 13, .score = 88.5};

27.3 结构体数组:管理多条记录

回到王老师的场景——现在只要一个数组就够了:

cpp
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 结构体指针:用 -> 运算符

既然结构体是一种类型,那自然可以有指向结构体的指针:

cpp
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 一样:

传递结构体本身(值传递——会复制一份):

cpp
void printStudent(Student s) {
    cout << s.name << " " << s.age << " " << s.score << endl;
}

但如果结构体很大,复制一份开销也大。更好的做法是用指针(或引用):

传递指针(只传地址,不复制数据):

cpp
void printStudent(Student* p) {
    cout << p->name << " " << p->age << " " << p->score << endl;
}

// 调用:
Student s = {"小明", 13, 96.5};
printStudent(&s);  // 传地址

传指针 + 修改原数据:

cpp
void addScore(Student* p, double bonus) {
    p->score += bonus;   // 直接修改原来的分数
}

Student s = {"小明", 13, 90};
addScore(&s, 5.5);
cout << s.score;  // 输出 95.5

27.6 构造函数入门

有时候你想在创建结构体变量时自动做一些初始化工作——比如把分数默认设为 0、年龄默认设为 0。这时候可以写一个构造函数

cpp
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 综合示例:学生成绩管理系统

cpp
#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. 读代码写结果

cpp
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)以下代码的输出是?

cpp
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打基础。。★核心(课堂必做) ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心J0106https://hydro.ac/d/srqc/p/J0106struct定义、成员访问、遍历
◆ 拓展J0107https://hydro.ac/d/srqc/p/J0107struct、嵌套比较、分数差
◆ 拓展J0108https://hydro.ac/d/srqc/p/J0108struct、条件判断、等级评定
◆ 拓展luogu-P5740https://hydro.ac/p/luogu-P5740struct、多成员、最值
◆ 拓展luogu-P5741https://hydro.ac/p/luogu-P5741struct、多关键字比较
◆ 拓展luogu-P5742https://hydro.ac/p/luogu-P5742struct、排序启蒙
◆ 拓展J0114https://hydro.ac/d/srqc/p/J0114struct数组、排序、分组

💡 练习建议:先完成 1 道★核心题,确保掌握本章基本方法;再完成 6 道◆拓展题,覆盖不同变式和细节。


配套练习

共7题。struct从定义→访问→比较→排序,为M6的sort+cmp打基础。★核心(课堂必做) · ◆拓展(课后练习)

级别题号链接覆盖知识点
★ 核心J0106https://hydro.ac/d/srqc/p/J0106struct定义、成员访问、遍历
◆ 拓展J0107https://hydro.ac/d/srqc/p/J0107struct、嵌套比较、分数差
◆ 拓展J0108https://hydro.ac/d/srqc/p/J0108struct、条件判断、等级评定
◆ 拓展luogu-P5740https://hydro.ac/p/luogu-P5740struct、多成员、最值
◆ 拓展luogu-P5741https://hydro.ac/p/luogu-P5741struct、多关键字比较
◆ 拓展luogu-P5742https://hydro.ac/p/luogu-P5742struct、排序启蒙
◆ 拓展J0114https://hydro.ac/d/srqc/p/J0114struct数组、排序、分组

练习建议:先在课堂完成 1 道★核心题,掌握本章基本方法;课后完成 6 道◆拓展题,覆盖不同变式和细节。

自查清单

  • [ ] 我能用 struct 定义自己的复合数据类型
  • [ ] 我会用 . 访问结构体变量的成员
  • [ ] 我会用 -> 通过指针访问结构体成员
  • [ ] 我能创建和使用结构体数组
  • [ ] 我知道如何把结构体传给函数(值传递 vs 指针传递)
  • [ ] 我了解构造函数的基本写法
  • [ ] 我记得结构体定义末尾要加分号

🚀 下章预告:结构体把数据打包得整整齐齐——一个学生的姓名、年龄、分数全在一个袋子里。但它在内存的哪个位置?能不能直接找到地址绕过去操作?第 28 章,我们用指针这门"GPS 导航",直接定位内存中的任意数据。