← 课程目录
第 20 讲 模块六:进阶与综合

函数 + 数组 / vector

20 / 24
💡
本节课目标
把模块四的函数和模块五的容器接到一起: 学会把 vector 传给函数,理解传值会整份拷贝这个坑, 再学一个新工具——引用 &,让函数能真正改动外面的容器。
开场

main 又变长了

📖 理论讲解

上节课末尾的问题

上节课把学生信息打包进了 struct,数据是整齐了, 但录入、打印、统计的代码还是一股脑堆在 main 里:

messy_main.cpp CPP
int main() {
    std::vector<Student> students;

    // 录入……十几行
    // 打印……十几行
    // 求平均分……十几行
    // 找最高分……十几行

    return 0;   // main 已经长到要滚屏才能看完了
}
⚠️
这正是第 11 讲抱怨过的毛病
一个 main 什么都干,就很难读、很难改、更难复用。 第 11 讲的解决办法今天照样管用:把每件事拆成一个函数。 只是这次要传给函数的,不再是一个 int,而是一整个 vector——这里有个新坑要踩。
第一段

把 vector 传给函数

📖 理论讲解

写法和传普通变量一样

参数类型写成 std::vector<Student>,调用时把容器名传进去, 函数里就能像用普通 vector 一样遍历它:

print_all.cpp CPP
void printAll(std::vector<Student> students) {
    for (int i = 0; i < students.size(); i++) {
        std::cout << students[i].name << ":" << students[i].score << std::endl;
    }
}

int main() {
    std::vector<Student> students = {{"小明", 90}, {"小红", 85}};
    printAll(students);   // 一行调用,代替一整个循环
    return 0;
}
只读的操作,这样写就够用
printAll 只是数据打印出来,不修改任何东西, 所以这样写完全正确,能跑出正确结果。但如果函数需要改动容器, 问题就来了——下面就是这个坑。
第二段

坑:函数改不动外面的 vector

📖 理论讲解

第 11 讲的规矩,对容器同样成立

第 11 讲学过:函数拿到的是实参的一份拷贝, 在函数里改形参,不影响外面。当时的例子是 int, 现在换成 vector,规矩一模一样

copy_problem.cpp CPP
void addOne(std::vector<int> v) {
    v.push_back(999);   // 改的是函数内部那份拷贝
}

int main() {
    std::vector<int> nums = {1, 2, 3};
    addOne(nums);
    std::cout << nums.size() << std::endl;   // 还是 3,不是 4!
    return 0;
}
🚫
push_back 明明执行了,为什么没效果
因为 addOne 拿到的 vnums复制品—— push_back 加进去的 999,加在了这份复制品上。 函数一结束,复制品就被丢掉了,main 里的 nums 从头到尾没被碰过。
⚠️
而且,拷贝一整个容器很浪费
int 拷贝一下无所谓,但如果 vector 里装着几千个学生, 每次调用函数都要把这几千个学生完整复制一遍,既慢又占内存。 所以哪怕只是只读的 printAll,也有改进的余地。
第三段

引用 &:让函数直接操作原件

📖 理论讲解

类型后面加一个 &

在参数类型后面加上 &,这个参数就变成了引用—— 不再复制一份,而是直接给原来那个变量起了个别名。 函数里对它做的任何改动,都实实在在落在 main 的那个容器上:

ref_version.cpp CPP
void addOne(std::vector<int>& v) {   // 注意类型后面多了一个 &
    v.push_back(999);   // 现在改的就是 main 里的那个 nums
}

int main() {
    std::vector<int> nums = {1, 2, 3};
    addOne(nums);
    std::cout << nums.size() << std::endl;   // 4,改动生效了
    return 0;
}
一个字符的差别,行为天差地别
std::vector<int> v拷贝,函数里改了不算数。
std::vector<int>& v引用,函数里改了就是真改。
差别只有一个 &,但这是本节课最需要盯住的细节
⚡ 动手实操 约 10 分钟

亲手对比两种写法

copy_problem.cpp 补成完整程序(记得 #include <vector>) 并运行,确认打印出的是 3

  1. 然后只加一个 &,别的一个字都不改,再运行一次。
  2. 确认这次打印的是 4——一个字符改变了整个程序的行为。
第四段

只读就加 const,一个好习惯

📖 理论讲解

既想省掉拷贝,又不想手滑改坏数据

printAll 这种只读函数,我们希望:用引用省掉拷贝, 但又明确表示不会修改。这时在类型前面加 const

const_ref.cpp CPP
// 只读:加 const,表示"我保证不改它"
void printAll(const std::vector<Student>& students) {
    for (int i = 0; i < students.size(); i++) {
        std::cout << students[i].name << std::endl;
    }
}

// 要改:只加 &,不加 const
void addStudent(std::vector<Student>& students, Student s) {
    students.push_back(s);
}
💡
const 是写给编译器看的承诺
加了 const 之后,万一你在函数里手滑写了 students.push_back(…)编译器会直接报错,而不是等程序跑起来才发现数据被改坏了。 这是让编译器帮你挡错误的好习惯。
📖 理论讲解

三种写法,怎么选

写法会拷贝吗什么时候用
vector<T> v会,慢基本不用(除非真想要一份独立副本)
vector<T>& v不会函数需要修改容器时
const vector<T>& v不会函数只读容器时

记一句话:传容器,默认就加 &;不改它,就再加 const

实现

重新组织:一个函数干一件事

📖 理论讲解

把 main 拆干净

把录入、打印、算平均分各封装成一个函数,main 就只剩下一条清晰的主线,一眼能看完:

organized.cpp CPP
#include <iostream>
#include <string>
#include <vector>

struct Student {
    std::string name;
    int score;
};

// 录入 n 个学生,装进 students
void inputStudents(std::vector<Student>& students, int n) {
    for (int i = 0; i < n; i++) {
        Student s;
        std::cout << "第 " << (i + 1) << " 个学生姓名:";
        std::cin >> s.name;
        std::cout << "分数:";
        std::cin >> s.score;
        students.push_back(s);
    }
}

// 打印全部学生
void printAll(const std::vector<Student>& students) {
    for (int i = 0; i < students.size(); i++) {
        std::cout << students[i].name << ":" << students[i].score << std::endl;
    }
}

// 算平均分并返回
double averageScore(const std::vector<Student>& students) {
    int sum = 0;
    for (int i = 0; i < students.size(); i++) {
        sum += students[i].score;
    }
    return sum / (double)students.size();
}

int main() {
    std::vector<Student> students;

    int n;
    std::cout << "有多少个学生?";
    if (!(std::cin >> n) || n <= 0) {
        std::cout << "人数必须是正整数。" << std::endl;
        return 1;
    }

    inputStudents(students, n);
    printAll(students);
    std::cout << "平均分:" << averageScore(students) << std::endl;

    return 0;
}
逐段看

inputStudents往容器里加东西,所以用 &(不加就白忙一场);printAllaverageScore 只读,所以用 const &——三个函数的参数写法差别,正好对应它们各自要干的事。

现在的 main 只有六七行,读一遍就知道整个程序在干什么。 想加新功能(比如找最高分),只要再写一个函数、在 main 里加一行调用。

实战

动手写四个函数

练习 20-1

求总分函数

写一个 int totalScore(const std::vector<Student>& students), 返回所有学生分数的总和,在 main 里调用并打印。

练习 20-2

统计及格人数

写一个 int countPassed(const std::vector<Student>& students), 返回分数大于等于 60 的学生人数。

提示:遍历 + 计数器,只是这次把它包进了函数里。

练习 20-3

全班加分

写一个 void addBonus(std::vector<Student>& students, int bonus), 给每个学生的分数都加上 bonus 分。 调用前后各打印一次全班成绩,确认真的改动了。

💡
想一想
这个函数该不该加 const?如果加了会怎样?
练习 20-4

返回最高分的学生

写一个 Student findTop(const std::vector<Student>& students), 用打擂台算法找出分数最高的学生,把整个 Student 返回main 里拿到后打印他的姓名和分数。

提示:函数的返回类型可以是 struct—— 和返回 intdouble 没有本质区别。

收尾

小结

📖 理论讲解

这节课你掌握了

能力用到的工具
把容器交给函数处理参数写 std::vector<T>
让函数真能改动容器引用 std::vector<T>&
只读时省掉拷贝又防手滑const std::vector<T>&
让 main 保持简短清晰一个函数只干一件事
💡
下节课预告
到这里,第一阶段的语法就全部学完了——变量、判断、循环、函数、 数组 / vectorstruct、引用。下节课做一次系统复习, 把这些串成一张完整的知识地图,为后面两节的综合项目做准备。