第四章 C++ 函数专题讲义
本讲义系统地梳理了 C++ 中函数的基本概念、参数传递机制、生命周期安全、现代 C++ 高级特性(如 Lambda 表达式、可调用对象等),旨在帮助你构建完整的函数知识体系。
1. 函数基础
函数是 C++ 程序的基本构建模块,用于将复杂的逻辑拆分为可复用的代码块。
1.1 函数的定义与声明
在 C++ 中,函数必须先声明后调用。
- 定义(Definition):提供函数的具体实现。
- 声明(Declaration / 原型):告知编译器函数的名称、返回类型及参数列表。通常放在头文件或调用处的上方。
#include <iostream>
// 1. 函数声明(Forward Declaration)
double add(double a, double b);
int main() {
// 2. 函数调用
double result = add(3.5, 4.5);
std::cout << "Result: " << result << std::endl;
return 0;
}
// 3. 函数定义
double add(double a, double b) {
return a + b;
}
1.2 返回值类型
- 普通类型:如
int,double, 自定义结构体等。 void类型:不返回任何值。若要提前结束函数,可直接使用return;。- 注意:不能直接对函数名赋值。
2. 参数传递机制
C++ 支持多种参数传递方式。选择合适的传递方式对程序性能和正确性至关重要。
2.1 值传递 (Pass by Value)
将实参的副本复制给形参。函数内对形参的修改不会影响外部实参。
* 适用场景:内置基本类型(如 int, double),占用内存小的对象。
* 缺点:对于大型对象(如 std::vector、大结构体),复制会带来显著的性能开销。
void increment(int x) {
x++; // 仅修改了副本
}
2.2 指针传递 (Pass by Pointer)
将实参的地址复制给形参指针。可以通过解引用(*)修改外部实参的值。
* 特点:传递的是地址(通常为 8 字节),效率高。
* 注意:指针可能为 nullptr,在函数内使用前通常需要进行空指针检查。
void increment(int* p) {
if (p != nullptr) {
(*p)++; // 修改了指针所指向的值
}
}
2.3 引用传递 (Pass by Reference)
形参是实参的别名,不发生任何复制。对形参的修改会直接反映在实参上。
* 优点:既拥有指针的高效率(无复制开销),又避免了繁琐的解引用操作,且无需担心空指针问题。
* 局限性:无法直接传入字面量常数(如 5)或临时表达式。
void increment(int& r) {
r++; // 直接修改外部实参
}
2.4 常量引用传递 (Pass by Const Reference)
通过 const Type& 传递参数。
* 优点:既避免了复制开销,又保护了数据不被函数内部意外修改,并且允许传入字面量常数和临时表达式。
* 适用场景:标准库容器、复杂类对象等大对象的“只读”传递。
// 既能避免大对象复制,又能防止内部修改,且支持传入常量
void printVector(const std::vector<int>& vec) {
for (int x : vec) {
std::cout << x << " ";
}
}
参数传递方式对比
| 传递方式 | 是否发生复制 | 能否修改实参 | 是否支持传入常量/字面量 | 推荐使用场景 |
|---|---|---|---|---|
| 值传递 | 是 | 否 | 是 | 基础类型(int, char, double 等) |
| 指针传递 | 否(仅复制地址) | 是 | 否(需取地址 &) |
需要兼容 C 语言接口,或允许参数为 nullptr |
| 引用传递 | 否 | 是 | 否 | 需要修改实参的大对象 |
| 常量引用传递 | 否 | 否 | 是 | 不需要修改的复杂类对象、标准库容器 |
3. 返回值与生命周期安全
3.1 悬挂指针与悬挂引用 (Dangling Pointer / Reference)
当函数返回指针或引用时,绝对不能返回指向函数内部局部变量的指针或引用。局部变量在函数生命周期结束时会被销毁,返回它们的地址会导致未定义行为(Undefined Behavior)。
❌ 错误示例:
int& getLocalReference() {
int val = 42;
return val; // 警告:返回了局部变量 val 的引用!
} // val 占用的内存在此处被销毁
3.2 安全的解决方案
- 值返回:直接返回值,依靠编译器优化(RVO/NRVO)可以避免复制开销。
- 静态局部变量:使用
static延长生命周期。 - 全局变量或堆内存(需注意内存释放)。
// 正确做法:利用 static 使变量在程序运行期间一直存在
const int& getStaticReference() {
static int val = 42;
return val;
}
4. 函数的高级特性
4.1 函数重载 (Function Overloading)
在同一作用域内,允许存在多个同名但参数列表不同(参数个数、类型或顺序不同)的函数。 * 注意:仅返回值类型不同不能构成重载。
int add(int a, int b);
double add(double a, double b); // OK,重载
// void add(int a, int b); // 错误,仅返回值类型不同,无法重载
4.2 默认参数 (Default Arguments)
可以为函数的参数指定默认值。如果调用时未传入该参数,则使用默认值。 * 规则:默认参数必须从右向左连续排列。
// 正确:默认参数在最右侧
void displayInfo(std::string name, int age = 18, double score = 100.0);
// 错误:默认参数后面出现了无默认值的参数
// void displayInfo(std::string name = "User", int age);
4.3 内联函数 (Inline Functions)
对于代码量少、调用频繁的函数,可以使用 inline 关键字修饰。
* 机制:通知编译器在调用处将函数体直接展开,避免函数调用栈的开销(如压栈、跳转、出栈)。
* 现代编译器:inline 仅仅是向编译器提出的建议,现代编译器会根据实际情况自动决定是否内联。
// 温度转换:先乘后除以保证整型计算的精度
inline int FtoC(int f) {
return (f - 32) * 5 / 9;
}
4.4 常量表达式函数 (constexpr) (C++11 新增)
使用 constexpr 修饰的函数如果传入的参数是编译期常量,则该函数会在编译期进行计算,进一步提升运行效率。
constexpr int square(int x) {
return x * x;
}
int main() {
constexpr int res = square(5); // 编译期直接计算出 25
int arr[res]; // 可以作为数组大小
}
5. 函数指针与可调用对象
5.1 函数指针 (Function Pointer)
函数在内存中也有地址,函数指针用于存放函数的入口地址。
int add(int a, int b) { return a + b; }
int main() {
// 声明一个函数指针 pFunc,指向“接收两个 int 并返回 int”的函数
int (*pFunc)(int, int) = add;
// 调用
int res = pFunc(3, 4); // 等价于 (*pFunc)(3, 4)
}
5.2 std::function (C++11 包装器)
std::function 是 C++11 引入的通用多态函数包装器,可以存储、复制和调用任何可调用目标(普通函数、Lambda、函数对象等)。
#include <functional>
int multiply(int a, int b) { return a * b; }
int main() {
std::function<int(int, int)> func = multiply;
int res = func(3, 4); // 12
}
6. Lambda 表达式 (C++11 新增匿名函数)
Lambda 表达式提供了一种在需要的地方直接定义匿名函数对象的方法,非常适合与 STL 算法配合。
6.1 基本语法
[捕获列表](参数列表) mutable(可选) -> 返回类型(可选) { 函数体 }
6.2 常用捕获方式
| 捕获写法 | 解释 |
|---|---|
[] |
不捕获任何外部变量。 |
[x] |
按值捕获变量 x(复制一份,内部默认不可修改,除非加 mutable)。 |
[&x] |
按引用捕获变量 x(可直接修改外部原变量)。 |
[=] |
按值捕获所有使用到的外部局部变量。 |
[&] |
按引用捕获所有使用到的外部局部变量。 |
[=, &x] |
默认按值捕获,但 x 按引用捕获。 |
[&, x] |
默认按引用捕获,但 x 按值捕获。 |
[this] |
捕获当前类的 this 指针,允许在 Lambda 内访问数成员。 |
6.3 典型应用示例
示例 1:自定义排序 (STL std::sort)
#include <vector>
#include <algorithm>
std::vector<int> v = {3, 1, 4, 1, 5};
// 降序排序:通过 Lambda 表达式定义比较规则
std::sort(v.begin(), v.end(), [](int a, int b) {
return a > b;
});
示例 2:按条件计数 (STL std::count_if)
#include <vector>
#include <algorithm>
int threshold = 3;
std::vector<int> v = {1, 2, 3, 4, 5};
// 捕获外部变量 threshold
auto cnt = std::count_if(v.begin(), v.end(), [threshold](int x) {
return x > threshold;
}); // cnt = 2
6.4 递归 Lambda
由于匿名函数没有固定的函数名,实现自我调用(递归)需要一些特殊技巧。
方案 A:C++14 通用泛型引用方案
利用 C++14 的泛型 Lambda,通过将自身作为参数传递来实现递归。
#include <vector>
std::vector<std::vector<int>> g; // 邻接表结构
// 使用 [&] 捕获外部邻接表 g
// 使用 auto&& self 接收 Lambda 自身
auto dfs = [&](auto&& self, int u, int fa) -> void {
for (int v : g[u]) {
if (v != fa) {
self(self, v, u); // 递归调用时,必须将自身作为第一个参数传入
}
}
};
// 启动调用
dfs(dfs, 1, 0);
方案 B:C++23 显式对象形参(Deducing This)
C++23 引入了显式对象形参,允许在 Lambda 中直接使用 this 关键字指代当前 Lambda 闭包,让递归 Lambda 的写法变得非常自然。
// C++23 标准写法
auto dfs = [&](this auto&& self, int u, int fa) -> void {
for (int v : g[u]) {
if (v != fa) {
self(v, u); // 像普通函数一样直接递归,无需重复传递 self
}
}
};
// 启动调用
dfs(1, 0);
—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com