文章目录
📚 本文是「C++ 全栈开发 30 周课程」阶段一第 4 周的详细讲义,也是阶段一的收官周。本周目标:掌握函数的声明/定义/调用、四种参数传递方式、默认参数、函数重载与编译期计算,并用函数模块化完成阶段一项目「学生成绩管理系统」。
📋 本周学习地图
1️⃣ 函数基础:声明、定义、调用
1.1 三段式结构
#include <iostream>
// ① 函数声明(原型):告诉编译器"有这么个函数"
int add(int a, int b);
int main() {
std::cout << add(3, 5) << "\n"; // ③ 调用
return 0;
}
// ② 函数定义:给出具体实现
int add(int a, int b) {
return a + b;
}
💡 为什么需要声明:C++ 是单遍编译,调用处必须先看到声明。多文件项目里声明放
.h,定义放.cpp。
1.2 声明与定义必须一致
// ❌ 声明和定义参数类型不一致 → 编译/链接错误
int add(int a, int b);
double add(int a, int b) { return a + b; } // error: 返回类型不匹配
1.3 函数名、参数、返回值三要素
| 要素 | 说明 | 示例 |
|---|---|---|
| 返回类型 | 可以是 void、任意类型、引用、指针 | int、void、int& |
| 函数名 | 标识符,遵循命名规范 | add、getScore |
| 参数列表 | 类型 + 名字,可为空 () 或 (void) | (int a, int b) |
2️⃣ 参数传递:四种方式,选错就是坑
2.1 四种传递方式总览
| 方式 | 写法 | 是否拷贝 | 能否修改实参 | 典型场景 |
|---|---|---|---|---|
| 值传递 | void f(int x) | ✅ 拷贝 | ❌ | 小对象、不需要改原值 |
| 引用传递 | void f(int& x) | ❌ 零拷贝 | ✅ | 需要修改实参 |
| const 引用 | void f(const int& x) | ❌ 零拷贝 | ❌ | 大对象只读(最常用) |
| 指针传递 | void f(int* x) | 拷贝指针 | ✅(解引用后) | C 风格 API、可空参数 |
2.2 值传递:改的是副本
#include <iostream>
void byValue(int x) {
x = 100; // 改的是副本
}
int main() {
int a = 10;
byValue(a);
std::cout << a << "\n"; // 10(没变)
}
2.3 引用传递:改的是原对象
void byRef(int& x) {
x = 100; // 改的是原对象
}
int main() {
int a = 10;
byRef(a);
std::cout << a << "\n"; // 100(变了)
}
2.4 const 引用:零拷贝 + 只读(大对象首选)
#include <string>
#include <vector>
// ❌ 按值传大对象:每次都拷贝整个 string/vector
void printBad(std::string s) {
std::cout << s << "\n";
}
// ✅ const 引用:零拷贝,且保证函数内不修改
void printGood(const std::string& s) {
std::cout << s << "\n";
}
int main() {
std::string big = "很长的字符串...";
printBad(big); // 拷贝一份
printGood(big); // 不拷贝
}
💡 口诀:要改就
T&,只读大对象const T&,小对象直接按值。
2.5 指针传递:可空 + 可改
void byPointer(int* x) {
if (x != nullptr) {
*x = 100; // 解引用后修改
}
}
int main() {
int a = 10;
byPointer(&a); // 传地址
byPointer(nullptr); // 合法,函数内判空
}
2.6 悬空引用陷阱
// ❌ 返回局部变量的引用 → 悬空引用,UB
int& badRef() {
int local = 10;
return local; // local 在函数结束就销毁了
}
// ✅ 返回值,或返回静态/成员变量
int goodValue() {
int local = 10;
return local; // 返回值拷贝,安全
}
3️⃣ 返回值与返回引用
3.1 返回值 vs 返回引用
#include <vector>
std::vector<int> g_data = {1, 2, 3};
// 返回值:调用者拿到副本
std::vector<int> getCopy() {
return g_data;
}
// 返回引用:调用者拿到原对象的别名(零拷贝)
std::vector<int>& getRef() {
return g_data;
}
int main() {
auto copy = getCopy(); // 拷贝
auto& ref = getRef(); // 零拷贝,改 ref 就是改 g_data
}
3.2 返回引用的三条铁律
| 规则 | 说明 |
|---|---|
| ✅ 可以返回 | 全局变量、静态变量、成员变量、参数引用 |
| ❌ 不能返回 | 局部变量的引用/指针 |
| ⚠️ 慎用 | 返回 const T& 时注意生命周期 |
// ❌ 局部变量
int& f1() { int x; return x; }
// ✅ 静态变量
int& f2() { static int x; return x; }
// ✅ 成员变量
class A { int x; public: int& get() { return x; } };
4️⃣ 默认参数
4.1 基本用法
#include <string>
#include <iostream>
void greet(const std::string& name,
const std::string& prefix = "你好") {
std::cout << prefix << ", " << name << "!\n";
}
int main() {
greet("Alice"); // 你好, Alice!
greet("Bob", "Hello"); // Hello, Bob!
}
4.2 两条硬规则
规则 1:默认参数必须从右往左连续指定
// ✅ 合法
void f(int a, int b = 2, int c = 3);
// ❌ 非法:b 没默认值,c 有
void g(int a, int b = 2, int c);
// ✅ 合法:跳过的必须靠右
void h(int a, int b, int c = 3);
规则 2:默认参数只能在声明或定义中出现一次
// ❌ 重复指定 → 编译错误
void f(int x = 1);
void f(int x = 1) { }
// ✅ 声明给默认值,定义不给
void f(int x = 1);
void f(int x) { }
4.3 默认参数 vs 重载:怎么选
| 场景 | 推荐 |
|---|---|
| 只是"可选参数",逻辑相同 | 默认参数 |
| 不同参数类型/数量,逻辑不同 | 函数重载 |
| 默认参数会导致重载歧义 | 改用重载 |
// ⚠️ 歧义示例
void f(int a);
void f(int a, int b = 0);
f(1); // ❌ 两个都能匹配,编译错误
5️⃣ 函数重载
5.1 基本用法:同名不同参
#include <iostream>
#include <string>
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
std::string add(const std::string& a, const std::string& b) {
return a + b;
}
int main() {
std::cout << add(1, 2) << "\n"; // 3(int 版本)
std::cout << add(1.5, 2.5) << "\n"; // 4(double 版本)
std::cout << add(std::string("A"), std::string("B")) << "\n"; // AB
}
5.2 重载的判定依据
| 能构成重载 | 不能构成重载 |
|---|---|
| ✅ 参数类型不同 | ❌ 仅返回值不同 |
| ✅ 参数个数不同 | ❌ 仅参数名字不同 |
| ✅ 参数顺序不同 | ❌ 仅默认参数不同 |
// ❌ 仅返回值不同 → 编译错误
int f(int x);
double f(int x);
// ❌ 仅参数名不同 → 重复定义
int f(int a);
int f(int b);
5.3 重载解析:最佳匹配
void f(int x) { std::cout << "int\n"; }
void f(double x) { std::cout << "double\n"; }
int main() {
f(1); // int(精确匹配)
f(1.5); // double(精确匹配)
f(1.0f); // ⚠️ float → 可能选 double(提升)也可能歧义
f('a'); // ⚠️ char → int(整型提升)
}
💡 原则:重载要语义一致。
add(int)和add(string)是合理的;print(int)和deleteFile(string)同名就是灾难。
5.4 重载 + 默认参数的歧义
void f(int a);
void f(int a, int b = 0);
f(1); // ❌ 两个都匹配 → 歧义
f(1, 2); // ✅ 只有第二个匹配
6️⃣ 内联函数 inline
6.1 作用与写法
// 建议编译器在调用处展开函数体,减少函数调用开销
inline int square(int x) {
return x * x;
}
int main() {
int a = square(5); // 可能被展开成 int a = 5 * 5;
}
6.2 三个关键认知
| 认知 | 说明 |
|---|---|
inline 是建议不是命令 | 编译器可以拒绝(函数太大、递归等) |
| 主要用途 | 头文件中定义函数,避免多重定义链接错误 |
| 现代编译器 | 小函数即使不写 inline 也会自动内联(-O2) |
// 头文件 utils.h
inline int max(int a, int b) { // ✅ 多个 .cpp 包含也不会重复定义
return a > b ? a : b;
}
💡 工程建议:小函数放头文件时加
inline;性能优化交给编译器-O2,不要为了内联牺牲可读性。
7️⃣ 编译期函数:constexpr / consteval
7.1 constexpr(C++11):能编译期算就编译期算
#include <iostream>
// 编译期可求值的函数
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
int main() {
constexpr int f5 = factorial(5); // ✅ 编译期算出 120
int runtime = 5;
int f_runtime = factorial(runtime); // ✅ 运行期也能调用
std::cout << f5 << "\n"; // 120
}
7.2 constexpr 的限制(C++11 → C++14 → C++20 逐步放宽)
| 标准 | 函数体允许内容 |
|---|---|
| C++11 | 只能一条 return(或三目) |
| C++14 | 允许局部变量、循环、多个 return |
| C++20 | 允许 try、constexpr 虚函数、动态分配(有约束) |
// C++14 起可以写循环
constexpr int sumTo(int n) {
int total = 0;
for (int i = 1; i <= n; ++i) {
total += i;
}
return total;
}
constexpr int s = sumTo(100); // 5050,编译期算出
7.3 consteval(C++20):强制编译期
// 必须编译期求值,运行期调用直接报错
consteval int mustBeCompileTime(int n) {
return n * n;
}
int main() {
constexpr int a = mustBeCompileTime(5); // ✅ 25
int x = 5;
// int b = mustBeCompileTime(x); // ❌ 编译错误:x 不是常量
}
7.4 三者对比
| 关键字 | 标准 | 语义 | 运行期能否调用 |
|---|---|---|---|
constexpr | C++11 | 能编译期算就算 | ✅ |
consteval | C++20 | 必须编译期算 | ❌ |
constinit | C++20 | 变量必须编译期初始化 | —(用于变量) |
💡 选择:普通工具函数写
constexpr;明确"只允许编译期"的接口写consteval。
8️⃣ 本周高频面试题(5 道)
| # | 题目 | 考察点 | 参考答案要点 |
|---|---|---|---|
| 1 | 值传递、引用传递、const 引用传递的区别? | 参数传递 | 值拷贝、引用零拷贝可改、const 引用零拷贝只读;大对象只读用 const 引用 |
| 2 | 函数重载的判定依据是什么? | 重载规则 | 参数类型/个数/顺序不同;仅返回值不同不能重载 |
| 3 | 默认参数有什么限制? | 默认参数 | 必须从右往左连续;声明和定义只能出现一次 |
| 4 | constexpr 和 consteval 的区别? | 编译期计算 | constexpr 能算就算,运行期也可调用;consteval 必须编译期,运行期调用报错 |
| 5 | 为什么不能返回局部变量的引用? | 生命周期 | 函数结束局部变量销毁,引用悬空,访问是 UB |
9️⃣ 阶段一项目验收:学生成绩管理系统(控制台版)
这是阶段一的收官项目,要求用本周学的函数知识把代码模块化,不再把所有逻辑堆在
main里。
9.1 需求清单
| 功能 | 要求 | 用到的本周知识 |
|---|---|---|
| 添加学生 | 输入姓名、成绩 | 值/引用传参 |
| 删除学生 | 按姓名删除 | const string& 传参 |
| 修改成绩 | 按姓名改分 | 引用传参修改 |
| 查询学生 | 按姓名/学号查 | 返回引用或副本 |
| 排序统计 | 按成绩排序、算平均分 | 函数返回 double |
| 文件读写 | 保存/加载 | 函数返回 bool 表示成功 |
9.2 模块化设计(函数划分)
9.3 参考代码(关键函数签名)
#include <string>
#include <vector>
#include <iostream>
#include <fstream>
#include <algorithm>
struct Student {
std::string name;
double score;
};
// ✅ 大对象用 const 引用,零拷贝
void addStudent(std::vector<Student>& students,
const std::string& name,
double score);
// ✅ 需要修改容器,用非 const 引用
bool removeStudent(std::vector<Student>& students,
const std::string& name);
// ✅ 只读查询,返回 const 引用避免拷贝
const Student* findStudent(const std::vector<Student>& students,
const std::string& name);
// ✅ 返回统计值
double calcAverage(const std::vector<Student>& students);
// ✅ 返回 bool 表示成败
bool saveToFile(const std::vector<Student>& students,
const std::string& filename);
bool loadFromFile(std::vector<Student>& students,
const std::string& filename);
// ✅ 编译期常量:最大学生数
constexpr int MAX_STUDENTS = 1000;
int main() {
std::vector<Student> students;
students.reserve(MAX_STUDENTS);
// 菜单循环:do-while + switch(第3周知识复用)
char choice;
do {
std::cout << "\n1.添加 2.删除 3.修改 4.查询 5.排序 6.统计 7.保存 8.加载 0.退出\n";
std::cin >> choice;
switch (choice) {
case '1': { /* 调用 addStudent */ break; }
case '2': { /* 调用 removeStudent */ break; }
// ...
case '0': break;
default: std::cout << "无效选项\n";
}
} while (choice != '0');
return 0;
}
9.4 验收标准
-
main函数不超过 50 行,只负责菜单调度 - 每个功能独立成函数,函数名见名知义
- 大对象参数一律用
const T&,需要修改用T& - 查询函数返回
const Student*(找不到返回nullptr) - 文件读写函数返回
bool,main里检查返回值 - 用
constexpr定义最大容量等常量 - 编译加
-Wall -Wextra无警告 - 所有
if/else、for都带{}
9.5 阶段一通关检查
🔟 本周避坑速查表(贴墙版)
🎯 一句话总结:参数传递看"改不改 + 大不大",返回值警惕悬空引用,默认参数靠右写一次,重载只看参数不看返回值,
constexpr能算就算、consteval必须算。 掌握这五条,加上把学生成绩管理系统写完,阶段一就正式通关,可以进入阶段二「指针与内存管理」了。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/qq_58662768/article/details/169951937




