← C++ 教材目录(共 13 章)

第 3 章 函数与引用

red wenzi · 2026-09-15 · 编程语言 · C++ · 📖 预计阅读 26 分钟 · 共 7 道练习
🎯 本章你会学到:把程序拆成函数,搞懂值/引用/指针三种传参。建议边读边敲代码,每节的练习先自己做,再展开答案对照。

本章目标:把程序拆成函数;搞懂值/引用/指针三种传参;会组织多文件工程。引用是 C++ 相对 C 最大的语法增量,也是“避免拷贝”的技术基础。

3.1 函数:声明、定义与重载

先学会把一段逻辑装进函数,再学会把函数摆到正确的文件里。

声明 vs 定义

声明(原型)告诉编译器“有这么个函数”,可以放在头文件里;定义是函数体,放在 .cpp 里。int add(int a, int b); 是声明,int add(int a, int b) { return a + b; } 是定义。调用函数时只需要声明可见;链接时必须有定义。

⚠️ 易错点 函数必须保证所有路径都有返回值,否则是未定义行为。

重载:同名不同参数

int maxOf(int, int) 和 double maxOf(double, double) 可以同时存在,编译器根据实参类型选择版本,和 Java 的重载一样。注意两点:返回类型不能区分重载;默认参数只能从右往左连续写(int f(int a = 1, int b); 是错的)。

示例代码
#include <iostream>
int maxOf(int a, int b) { return a > b ? a : b; }
double maxOf(double a, double b) { return a > b ? a : b; }
int main() { std::cout << maxOf(3, 5) << ' ' << maxOf(1.5, 2.5); }
运行结果
5 2.5
⚠️ 易错点 重载(overload)是编译期决定;覆写(override)是运行期决定,两个词别混。

✍️ 本节练习

3.1.1必做我的第一个函数

写一个函数 int maxOf(int a, int b) 返回较大值,再写一个重载 double maxOf(double a, double b)。main 里读两个整数和一个浮点数对,分别调用并输出结果。

输入 一行两个整数;一行两个小数。

输出 两行,每行一个最大值。

样例输入
3 5
2.5 1.5
样例输出
5
2.5

💡 提示 重载靠参数类型区分,返回类型不能区分重载。

✅ 查看参考答案与解析
#include <iostream>
int maxOf(int a, int b) { return a > b ? a : b; }
double maxOf(double a, double b) { return a > b ? a : b; }
int main() {
    int x, y;
    double p, q;
    std::cin >> x >> y;
    std::cin >> p >> q;
    std::cout << maxOf(x, y) << '\n';
    std::cout << maxOf(p, q) << '\n';
    return 0;
}

解析 同名函数靠参数类型不同区分,编译器自动选对版本。

3.2 参数传递:值、引用、指针

这是全书最需要想清楚的一节。Java 只有值传递一种;C++ 有四种选择,你必须自己决定用哪种。

四种传参方式

byValue(int x):改的是副本,原变量不变;byRef(int& x):改的是原变量(引用=原变量的别名);byPointer(int* p):可能为空,用前先判空;byConstRef(const string& s):不拷贝也不能改——这是 C++ 里出现频率最高的参数写法。

示例代码
#include <iostream>
#include <string>
void byValue(int x)    { x = 100; (void)x; }           // 改副本
void byRef(int& x)     { x = 100; }           // 改原变量
void byConstRef(const std::string& s) { (void)s; }     // 零拷贝、只读
int main() {
    int a = 1;
    byValue(a);   std::cout << a << '\n';     // 1
    byRef(a);     std::cout << a << '\n';     // 100
}
运行结果
1
100
⚠️ 易错点 const T& 既避免拷贝(性能)又承诺不改(安全),还能绑定临时对象。看到函数签名 const std::vector<int>& 要立刻明白为什么这么写。

不要返回局部变量的引用或指针

局部变量在函数返回时就被销毁,返回它的地址得到一个悬垂引用,用它=未定义行为。编译器通常会给 -Wreturn-local-addr 警告。这是 C++ 独有的风险(Java 的 GC 没有这个问题)。

⚠️ 易错点 按值返回对象不用担心拷贝开销——编译器会做返回值优化(RVO),通常零拷贝。

引用的三条铁律

int& r = a; 引用必须初始化(int& r; 编译不过);一旦绑定就不能改绑(r = b 是把 b 的值赋给 a,不是让 r 改指 b);不存在空引用(所以用引用的函数不需要判空)。

⚠️ 易错点 心智模型:指针是“存放地址的变量”(可以改指向、做算术);引用是“另一个名字”(只是别名,没有自己的身份)。

✍️ 本节练习

3.2.1挑战偶数求和

写一个函数 bool isEven(int n),然后读入 n,输出 1 到 n 之间所有偶数的和。

输入 一个整数 n(1 ≤ n ≤ 100000)。

输出 1 到 n 的偶数和。

样例输入
10
样例输出
30

💡 提示 函数只负责判断,求和写在 main 的循环里。

✅ 查看参考答案与解析
#include <iostream>
bool isEven(int n) { return n % 2 == 0; }
int main() {
    int n;
    std::cin >> n;
    long long sum = 0;
    for (int i = 1; i <= n; ++i) {
        if (isEven(i)) { sum += i; }
    }
    std::cout << sum << '\n';
    return 0;
}

解析 小函数让 main 更清晰;累加可能超 int,用 long long。

3.2.2必做引用版交换

写一个函数 void swapRef(int& a, int& b) 交换两个整数,main 里读两个数调用它并输出交换后的结果。

输入 一行两个整数 a b。

输出 交换后的 a b(空格分隔)。

样例输入
3 5
样例输出
5 3

💡 提示 int t = a; a = b; b = t;

✅ 查看参考答案与解析
#include <iostream>
void swapRef(int& a, int& b) {
    int t = a;
    a = b;
    b = t;
}
int main() {
    int x, y;
    std::cin >> x >> y;
    swapRef(x, y);
    std::cout << x << ' ' << y << '\n';
    return 0;
}

解析 引用就是原变量的别名,函数里改的就是调用方的变量。

3.2.3挑战值传递 vs 引用

写两个函数:void byValue(int x) 把参数改成 100;void byRef(int& x) 把参数改成 100。main 里把 a=1 分别传进去,输出 a 两次,观察区别。

输入 无

输出 两行,第一行 byValue 后 a 的值,第二行 byRef 后 a 的值。

样例输入
(本题无输入)
样例输出
1
100

💡 提示 值传递改的是副本,引用传递改的是原变量。

✅ 查看参考答案与解析
#include <iostream>
void byValue(int x) { x = 100; (void)x; }
void byRef(int& x)  { x = 100; }
int main() {
    int a = 1;
    byValue(a);
    std::cout << a << '\n';   // 1:副本被改,a 没变
    byRef(a);
    std::cout << a << '\n';   // 100:引用改的是原变量
    return 0;
}

解析 这个实验值得亲手跑一遍,是理解引用意义的最小例子。

3.3 命名空间与多文件组织

为什么别人的 C++ 工程有 .h 和 .cpp 两种文件?什么时候放哪边?这一节解决这两个实际问题。

命名空间:给名字划分地盘

namespace math { ... } 包起来,外面用 math::square 访问(完全限定名,最安全)。using math::PI 只引入一个名字;using namespace std 引入整个命名空间,写在 .cpp 顶部可以接受,但绝不要写进头文件——头文件会被复制到很多源文件,等于强迫所有人接受 std 的全部名字。

⚠️ 易错点 匿名命名空间 namespace { ... } 里的名字只在当前文件可见,是现代 C++ 的“私有工具函数”写法。

头文件放什么、源文件放什么

规则:头文件放声明、类定义、constexpr 常量、inline 函数、模板(后面讲);源文件放普通函数的定义。头文件用 #pragma once 防止重复包含。编译时把 .cpp 一起交给 g++。

示例代码
// ---------- util.h ----------
#pragma once
double square(double x);          // 声明放头文件

// ---------- util.cpp ----------
#include "util.h"
double square(double x) { return x * x; }   // 定义放源文件

// ---------- main.cpp ----------
#include "util.h"
#include <iostream>
int main() { std::cout << square(3.0); }
运行结果
9
⚠️ 易错点 编译命令:g++ -std=c++17 -Wall -Wextra -o app main.cpp util.cpp。工程大时分开编译:先 -c 各出 .o,再链接。

ODR:为什么有的东西能放头文件

单一定义规则(ODR):同一个名字在整个程序里可以有多个声明,但只能有一个定义,而且声明必须完全一致。inline 函数、constexpr 常量、类内定义的成员函数、模板,这些“可以写在头文件里”的东西,本质上都是标准给出的 ODR 豁免——它们允许在多处出现且保持一份定义。

⚠️ 易错点 看到“重复定义(multiple definition)”链接错误,就是违反了 ODR:通常是普通函数定义被放进了头文件。

✍️ 本节练习

3.3.1必做自己的命名空间

定义命名空间 math,里面放 constexpr double PI 和函数 double square(double x)。main 里用 math:: 前缀调用,输出 PI 和 square(3.0)。

输入 无

输出 两行:PI 的值、9。

样例输入
(本题无输入)
样例输出
3.14159265
9

💡 提示 namespace math { ... } 包起来,外面用 math::square 访问。double 默认只显示 6 位有效数字,输出 PI 前用 std::setprecision(9)(9 位有效数字),不要用 fixed(会把 square 也变成 9.00000000)。

✅ 查看参考答案与解析
#include <iomanip>
#include <iostream>
namespace math {
    constexpr double PI = 3.14159265;
    double square(double x) { return x * x; }
}
int main() {
    std::cout << std::setprecision(9) << math::PI << '\n';
    std::cout << math::square(3.0) << '\n';
    return 0;
}

解析 完全限定名 math:: 最安全;命名空间解决名字冲突。

3.3.2挑战拆成 .h 和 .cpp

把一个求平方的函数拆成三个文件:util.h(声明 + #pragma once)、util.cpp(定义)、main.cpp(调用),编译运行。这道题先不写进一个文件,按下面参考代码建三个文件,再用 g++ main.cpp util.cpp 编译。

输入 无

输出 9

样例输入
(本题无输入)
样例输出
9

💡 提示 编译命令:g++ -std=c++17 -Wall -Wextra -o app main.cpp util.cpp

✅ 查看参考答案与解析
util.h
#pragma once
double square(double x);
util.cpp
#include "util.h"
double square(double x) { return x * x; }
main.cpp
#include "util.h"
#include <iostream>
int main() {
    std::cout << square(3.0) << '\n';
    return 0;
}

解析 头文件放声明、源文件放定义;多个 .cpp 一起交给 g++,链接器把函数定义拼起来。

本章小结

🧩 本章综合练习

这几道题把本章多个知识点串起来,建议合上资料独立完成,再展开答案对照。

3.A综合统计函数:const 引用入参 + 引用输出参数

写一个函数 void analyze(const std::vector<int>& v, int& minv, int& maxv, double& avg),把最小值、最大值、平均值写回调用方。main 读入数据后调用并输出。这是 C++ 参数设计最典型的组合:只读的用 const 引用(零拷贝),要写回的用引用。

输入 第一行整数 n;第二行 n 个整数。

输出 三行:min=max=avg=(2 位小数)。

样例输入
6
5 3 9 1 7 4
样例输出
min=1
max=9
avg=4.83

💡 提示 返回多个值就用引用输出参数;传入的容器不要按值传,否则整份拷贝。

✅ 查看参考答案与解析
#include <iomanip>
#include <iostream>
#include <vector>

void analyze(const std::vector<int>& v, int& minv, int& maxv, double& avg) {
    minv = v[0];
    maxv = v[0];
    long long sum = 0;
    for (int x : v) {                  // 只读遍历
        if (x < minv) { minv = x; }
        if (x > maxv) { maxv = x; }
        sum += x;
    }
    avg = static_cast<double>(sum) / static_cast<double>(v.size());
}

int main() {
    int n{};
    std::cin >> n;
    std::vector<int> v(n);
    for (int i = 0; i < n; ++i) { std::cin >> v[i]; }
    int lo{}, hi{};
    double avg{};
    analyze(v, lo, hi, avg);
    std::cout << "min=" << lo << '\n';
    std::cout << "max=" << hi << '\n';
    std::cout << "avg=" << std::fixed << std::setprecision(2) << avg << '\n';
    return 0;
}

解析 const 引用既避免拷贝又承诺不改;引用输出参数在写回前要先赋初值(这里从 v[0] 起步)。

📚 本文概念都在知识大全:

引用 · 命名空间 · 头文件 · ODR 单一定义规则