函数基础
建议先阅读:09_数组基础
原理
函数调用栈帧
每次函数调用在调用栈上压入一个栈帧(stack frame),包含:
- 返回地址(调用方下一条指令的地址)
- 保存的基指针(调用方的 ebp/rbp)
- 局部变量
- 参数(部分可能通过寄存器传递)
函数返回时弹出栈帧,程序计数器跳回返回地址继续执行。这就是为什么递归过深会导致栈溢出——每个递归调用都分配新栈帧,超出操作系统限制(通常 8MB)。
值传递 vs 引用传递
void byValue(int x) { x++; } // 栈帧上复制一份 x,修改不影响实参
void byRef(int& x) { x++; } // x 是实参的别名,修改直接作用于实参引用在底层实现为指针——编译器自动解引用,语法层无需 *。对大型对象(如 string),const 引用避免昂贵的内存拷贝。
名称修饰 (Name Mangling)
C++ 的函数重载依赖名称修饰——编译器将函数名、参数类型信息编码为唯一的链接符号。例如 add(int, double) 在不同编译器下可能编为 _Z3addid(GCC)或 ?add@@YAHHN@Z(MSVC)。这就是为什么 C++ 函数不能被 C 代码直接调用(需要 extern "C" 禁止修饰)。
内联与调用开销
函数调用有固定开销:压栈参数、跳转、分配栈帧、返回。inline 将函数体在调用处展开,消除调用开销。但内联增加代码体积(code bloat),影响指令缓存。编译器会根据启发式规则自主决定是否内联——inline 关键字只是建议。
语法
声明与定义
int add(int a, int b); // 声明(原型)
int add(int a, int b) { // 定义
return a + b;
}声明可多次,定义只能一次(单一定义规则 ODR)。函数必须先声明后调用。
值传递与引用传递
void swap(int& a, int& b) { // 引用传递,修改生效
int temp = a;
a = b;
b = temp;
}
void print(const std::string& s) { // const 引用:不拷贝,不修改
std::cout << s << '\n';
}基本类型 (int, char, double) 值传递即可;大对象使用 const 引用。
函数重载
同名函数,参数列表不同(类型/数量/顺序):
int add(int a, int b);
double add(double a, double b);
int add(int a, int b, int c);仅返回类型不同不构成重载——编译器无法仅凭调用上下文区分。
默认参数
void print(std::string msg, int times = 1, std::string end = "\n") {
for (int i = 0; i < times; i++)
std::cout << msg << end;
}
// 调用: print("Hi"); print("Hi", 3); print("Hi", 2, " ");默认参数必须从右向左连续设置,不能跳跃。
递归
int factorial(int n) {
if (n <= 1) return 1; // 基准条件 (终止)
return n * factorial(n - 1);
}每个递归必须有无条件到达的基准条件。尾递归(最后一步是自身调用)可被编译器优化为循环。
头文件组织
// utils.h
#ifndef UTILS_H
#define UTILS_H
int add(int a, int b);
#endif
// utils.cpp
#include "utils.h"
int add(int a, int b) { return a + b; }
// main.cpp
#include "utils.h"详解:函数重载决议
编译器按以下步骤选择重载函数:
- 名称查找:在作用域内找到所有同名函数
- 实参推导:将实参类型与每个重载的形参类型匹配
- 最佳匹配:选择”最匹配”的重载
匹配优先级:
- 精确匹配(无需转换)
- 提升匹配(
char→int) - 标准转换匹配(
int→double) - 用户自定义转换匹配(构造函数/转换运算符)
- 无匹配 → 编译错误
void f(int);
void f(double);
void f(int, int);
f(42); // 精确匹配: f(int)
f(3.14); // 精确匹配: f(double)
f('a'); // 提升匹配: f(int) (char → int)
f(1, 2); // 精确匹配: f(int, int)
// f(1, 2.0); // 歧义错误: f(int,int) 和 f(double) 均可匹配重载 vs 函数模板
// 重载: 为每种类型写一份
int max(int a, int b) { return a > b ? a : b; }
double max(double a, double b) { return a > b ? a : b; }
// 模板: 自动生成 (C++98)
template<typename T>
T max_val(T a, T b) { return a > b ? a : b; }
// max_val(1, 2) -> T=int, max_val(1.5, 2.5) -> T=doubleextern “C”
// C++ 函数被 C 调用
extern "C" void c_style_func(int x) {
// 无名称修饰,可被 C 链接器找到
}
// C 函数被 C++ 调用
extern "C" {
#include <math.h> // C 头文件
}详解:默认参数深入
声明与定义中的默认参数
// 声明中提供默认值
void func(int a, int b = 10, int c = 20);
// 定义中不能重复提供
void func(int a, int b, int c) {
std::cout << a << b << c;
}
// 错误: 重复默认值
// void func(int a, int b = 10, int c = 20) { ... }默认参数与重载的陷阱
void f(int a, int b = 10) { ... }
void f(int a) { ... }
f(5); // 歧义! 两个 f 都可以匹配建议:默认参数和函数重载不要混用,容易产生歧义。
详解:constexpr 函数
C++11: 基本 constexpr
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
// 编译期计算
constexpr int f5 = factorial(5); // 120, 编译时求值
int x = 10;
int f10 = factorial(x); // 运行时求值(x 不是 constexpr)C++14: 放宽限制
// C++14: constexpr 函数可以有循环、局部变量
constexpr int fibonacci(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
int temp = a + b;
a = b;
b = temp;
}
return b;
}
constexpr int fib10 = fibonacci(10); // 55, 编译期C++17: if constexpr
template<typename T>
constexpr auto convert(T val) {
if constexpr (std::is_integral_v<T>) {
return static_cast<double>(val);
} else {
return val;
}
}
// 编译期根据类型选择分支,无运行时开销constexpr 的价值
// 编译期验证: 错误在编译时暴露
constexpr int sqrt_int(int n) {
if (n < 0) throw "negative"; // C++14: constexpr 函数可以 throw
int root = 0;
while ((root + 1) * (root + 1) <= n) root++;
return root;
}
// constexpr int bad = sqrt_int(-1); // 编译错误!
// 用于模板参数
template<int N>
struct Array {
int data[N];
};
Array<sqrt_int(100)> arr; // 编译期: N=10详解:Lambda 表达式 (C++11)
基本语法
// [捕获列表](参数列表) -> 返回类型 { 函数体 }
auto greet = []() { std::cout << "Hello!\n"; };
greet(); // Hello!
// 带参数
auto add = [](int a, int b) { return a + b; };
add(3, 4); // 7
// 带返回类型
auto divide = [](double a, double b) -> double {
if (b == 0) return 0;
return a / b;
};捕获方式
int x = 10, y = 20;
// 值捕获 (只读)
auto f1 = [x, y]() { return x + y; };
// 引用捕获 (可修改)
auto f2 = [&x, &y]() { x++; y++; };
// 隐式值捕获
auto f3 = [=]() { return x + y; }; // 捕获所有外部变量(值)
// 隐式引用捕获
auto f4 = [&]() { x++; y++; }; // 捕获所有外部变量(引用)
// 混合捕获
auto f5 = [=, &x]() { return x + y; }; // y值捕获, x引用捕获Lambda vs 函数指针 vs std::function
// Lambda: 零开销,类型唯一
auto lambda = [](int x) { return x * 2; };
// 函数指针: 只能捕获全局函数
int global_add(int a, int b) { return a + b; }
int (*fptr)(int, int) = global_add; // OK
// fptr = lambda; // 错误: lambda 不能隐式转为函数指针(无捕获时可以)
// std::function: 通用包装,有运行时开销
std::function<int(int)> func = lambda; // OK
func = global_add; // OK
func = [](int x) { return x + 100; }; // OK| 特性 | Lambda | 函数指针 | std::function |
|---|---|---|---|
| 捕获外部变量 | 可以 | 不能 | 可以 |
| 运行时开销 | 无 | 无 | 有(堆分配) |
| 类型 | 唯一类型 | int(*)(int) | std::function<int(int)> |
| 作为模板参数 | 推荐 | 可以 | 不推荐 |
| 可赋值性 | 有限 | 可互换 | 完全通用 |
Lambda 实用示例
#include <algorithm>
#include <vector>
std::vector<int> v = {3, 1, 4, 1, 5, 9};
// 排序: 传入 lambda 比较器
std::sort(v.begin(), v.end(), [](int a, int b) {
return a > b; // 降序
});
// 查找: 第一个大于 3 的元素
auto it = std::find_if(v.begin(), v.end(), [](int x) {
return x > 3;
});
// 计数: 统计偶数个数
int count = std::count_if(v.begin(), v.end(), [](int x) {
return x % 2 == 0;
});详解:函数指针 vs std::function
函数指针
// 声明
int (*fptr)(int, int) = add; // 指向全局函数
int (*fptr2)(int, int) = &add; // & 可省略
// 调用
fptr(3, 4); // 7
(*fptr)(3, 4); // 7, 两种写法等价
// 作为参数 (回调)
void execute(int (*func)(int, int), int a, int b) {
std::cout << func(a, b);
}
execute(add, 3, 4); // 7std::function
#include <functional>
std::function<int(int, int)> f = add; // 普通函数
f = [](int a, int b) { return a + b; }; // Lambda
f = std::bind(add, std::placeholders::_1, std::placeholders::_2); // bind
f(3, 4); // 7
// 检查是否为空
if (f) { f(1, 2); } // 有值时调用何时使用哪个
// 1. 高性能场景: 函数指针或模板
// 编译期确定,零开销
void sort(int* begin, int* end, bool (*comp)(int, int));
// 2. 需要存储状态: Lambda + std::function
int counter = 0;
std::function<int()> gen = [counter]() mutable { return ++counter; };
// 3. STL 算法: Lambda (推荐)
std::sort(v.begin(), v.end(), [](int a, int b) { return a < b; });
// 4. 回调接口: std::function (通用性)
using Callback = std::function<void(int)>;
void register_callback(Callback cb);详解:内联函数
inline 的本质
// inline: 建议编译器在调用处展开
inline int square(int x) { return x * x; }
// 编译器可能展开为:
int y = square(5); // int y = 5 * 5;
// 或者保留函数调用 (编译器自主决定)何时内联有效
// 小函数: 内联有收益
inline int max_val(int a, int b) { return a > b ? a : b; }
// 大函数: 内联增加代码体积,可能降低性能
inline void huge_function() {
// 几十行代码...
// 不建议内联
}编译器决策
现代编译器(GCC/Clang)会自主决定是否内联,无视 inline 关键字。inline 的真正作用是允许函数在多个翻译单元中定义(消除链接器的 ODR 违规)。
实践
计算整数幂(快速幂):
double power(double base, int exp) {
if (exp == 0) return 1;
double half = power(base, exp / 2);
if (exp % 2 == 0)
return half * half;
else if (exp > 0)
return half * half * base;
else
return half * half / base; // 负指数
}尾递归优化
// 普通递归: O(n) 栈空间
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // 最后一步是乘法
}
// 尾递归: O(1) 栈空间 (编译器优化为循环)
int factorial_tail(int n, int acc = 1) {
if (n <= 1) return acc;
return factorial_tail(n - 1, n * acc); // 最后一步是自身调用
}回调函数模式
#include <functional>
#include <vector>
// 通用遍历
void for_each(const std::vector<int>& v, std::function<void(int)> func) {
for (int x : v) func(x);
}
// 使用
std::vector<int> nums = {1, 2, 3, 4, 5};
for_each(nums, [](int x) { std::cout << x * x << ' '; });
// 输出: 1 4 9 16 25练习
| 题号 | 题目 | 链接 | 知识点 |
|---|---|---|---|
| 50 | Pow(x, n) | https://www.luogu.com.cn/problem/P1001 | 快速幂、递归 |
| 70 | 爬楼梯 | https://www.luogu.com.cn/problem/P1001 | 递归、记忆化 |
| 172 | 阶乘后的零 | https://www.luogu.com.cn/problem/P1001 | 数学、函数封装 |
| 206 | 反转链表 | https://www.luogu.com.cn/problem/P1001 | 递归、迭代 |
| 231 | 2的幂 | https://www.luogu.com.cn/problem/P1001 | 位运算、递归 |
| 342 | 4的幂 | https://www.luogu.com.cn/problem/P1001 | 位运算、数学 |
| 367 | 有效的完全平方数 | https://www.luogu.com.cn/problem/P1001 | 二分查找、数学 |
| 392 | 判断子序列 | https://www.luogu.com.cn/problem/P1001 | 双指针、函数抽象 |
| 509 | 斐波那契数 | https://www.luogu.com.cn/problem/P1001 | 递归、迭代 |
| 704 | 二分查找 | https://www.luogu.com.cn/problem/P1001 | 二分、函数封装 |