函数进阶:递归函数、函数指针、lambda表达式、std::function与std::bind
函数这东西,写了几年代码后你会发现,它远不止「把代码包起来反复调用」那么简单。今天聊的这几个进阶特性,说白了就是让函数变得更灵活、更抽象、更像一个「值」来传递和操作。我个人觉得,这是从C风格走向现代C++的一道分水岭。
一、递归函数:函数调用自己
递归,就是函数直接或间接调用自身。听起来有点绕,但实际场景很常见。比如遍历文件夹、解析JSON树、计算阶乘——这些天然具有「嵌套结构」的问题,用递归写起来特别自然。
// 计算阶乘的递归实现
int factorial(int n) {
if (n <= 1) return 1; // 终止条件
return n * factorial(n - 1); // 递归调用
}
嗯,这里要注意:递归必须有两个要素——终止条件和递推关系。缺一个就是无限递归,栈溢出等着你。
我曾经在项目里写了一个递归遍历目录的函数,忘了处理符号链接导致的循环引用。结果线上服务跑了几个小时,栈直接爆了。从那以后,我写递归一定会问自己三个问题:终止条件明确吗?递归深度可控吗?有没有循环依赖?
递归深度太深时,可以考虑改用循环或尾递归优化。C++编译器对尾递归的优化支持有限,别太依赖。
二、函数指针:把函数当作变量
函数指针,说白了就是存函数的地址。你可以把它赋值、传参、放进数组里。这在回调机制、策略模式里非常常见。
// 定义一个函数指针类型
int (*op)(int, int);
// 两个普通函数
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
// 使用函数指针
op = add;
int result = op(3, 4); // 7
op = mul;
result = op(3, 4); // 12
函数指针的语法有点丑,尤其是返回函数指针的函数——那声明能让你怀疑人生。我个人习惯用 using 或 typedef 给它起个别名,代码可读性会好很多。
用
using FuncPtr = int(*)(int, int); 来定义函数指针类型,比直接写声明清晰多了。
三、lambda表达式(C++11):匿名函数的优雅写法
lambda 是 C++11 引入的语法糖,让你可以在需要函数的地方直接「就地」定义一个匿名函数。我刚开始接触时觉得这玩意儿花里胡哨的,后来用顺手了,真香。
// 基本语法:[捕获列表](参数列表) -> 返回类型 { 函数体 }
auto add = [](int a, int b) -> int { return a + b; };
std::cout << add(2, 3); // 5
// 捕获外部变量
int factor = 10;
auto multiply = [factor](int x) { return x * factor; };
std::cout << multiply(5); // 50
捕获列表有几种模式:[=] 值捕获、[&] 引用捕获、[this] 捕获当前对象。我建议优先用值捕获,除非你明确需要修改外部变量。引用捕获容易踩坑——lambda 生命周期比捕获的变量长时,引用就悬空了。
lambda 本质上是一个匿名函数对象(functor)。编译器会为每个 lambda 生成一个独一无二的闭包类型。所以
auto 是声明 lambda 变量的最佳方式。
四、std::function:万能函数包装器
std::function 是 C++11 标准库提供的多态函数包装器。它可以存储任何可调用对象——普通函数、lambda、函数对象、成员函数指针等。说白了,它就是一个「函数类型的容器」。
#include <functional>
std::function<int(int, int)> func;
func = add; // 普通函数
func = [](int a, int b) { // lambda
return a * b + 1;
};
int result = func(3, 4); // 13
我在项目中用 std::function 实现过事件回调系统、命令模式、策略模式。它的好处是类型擦除——你不需要关心具体是什么可调用对象,只要签名匹配就行。
std::function 有额外的开销(虚函数调用、堆分配)。在性能敏感的循环里,尽量用模板或裸函数指针。我一般只在需要类型擦除或运行时多态时才用它。
五、std::bind:参数绑定与占位符
std::bind 可以「预绑定」函数的部分参数,生成一个新的可调用对象。配合占位符 std::placeholders::_1 等,可以灵活调整参数顺序或固定某些参数。
#include <functional>
using namespace std::placeholders;
void print(int a, int b, int c) {
std::cout << a << ", " << b << ", " << c << std::endl;
}
auto f1 = std::bind(print, 1, 2, 3);
f1(); // 1, 2, 3
auto f2 = std::bind(print, _1, 100, _2);
f2(10, 20); // 10, 100, 20
我个人觉得 std::bind 在 C++11 时代很有用,但 C++14 引入泛型 lambda 后,很多场景可以用 lambda 替代,代码更直观。不过在一些遗留代码或需要与 C 回调接口对接时,std::bind 仍然是一把利器。
能用 lambda 的地方优先用 lambda,可读性更好。
std::bind 的占位符语义对新手不太友好。我只有在需要「部分求值」或「适配旧接口」时才用 bind。
知识体系总览
下面这张图把本章的核心概念串起来了。你可以看到,从最基础的递归函数,到函数指针,再到 lambda、std::function、std::bind,其实是一条「让函数越来越像一等公民」的演进路线。
综合示例:把这些特性串起来
最后给一个综合例子,展示递归、lambda、std::function、std::bind 如何协同工作。这是一个简单的「函数计算器」,支持加减乘除,并且可以递归组合。
#include <iostream>
#include <functional>
#include <map>
#include <string>
using namespace std;
using namespace placeholders;
// 基础运算
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div_op(int a, int b) { return b != 0 ? a / b : 0; }
// 递归计算:支持嵌套表达式
// 用 std::function 存储可调用对象
using OpFunc = std::function<int(int, int)>;
// 递归计算函数
int recursive_calc(const std::vector<std::pair<OpFunc, int>>& ops, int init) {
int result = init;
for (const auto& op : ops) {
result = op.first(result, op.second);
}
return result;
}
int main() {
// 用 map 存储运算符到函数的映射
std::map<std::string, OpFunc> calc_map = {
{"+", add},
{"-", sub},
{"*", mul},
{"/", div_op}
};
// 用 lambda 定义复合运算
auto complex_op = [](int x) {
return x * x + 2 * x + 1;
};
// 用 std::bind 固定参数
auto add_ten = std::bind(add, _1, 10);
// 测试
std::cout << "add_ten(5) = " << add_ten(5) << std::endl; // 15
std::cout << "complex_op(3) = " << complex_op(3) << std::endl; // 16
// 递归计算:先加5,再乘2,再减3
std::vector<std::pair<OpFunc, int>> steps = {
{calc_map["+"], 5},
{calc_map["*"], 2},
{calc_map["-"], 3}
};
int final = recursive_calc(steps, 10); // ((10+5)*2)-3 = 27
std::cout << "recursive_calc result: " << final << std::endl;
return 0;
}
这个例子虽然简单,但涵盖了本章所有核心概念。你想想看,如果没有这些特性,要实现同样的灵活性,得写多少重复代码?
递归函数解决嵌套问题,函数指针提供C风格的回调,lambda让匿名函数变得优雅,std::function实现类型擦除,std::bind支持参数预绑定。这五个工具组合起来,几乎可以应对任何「把函数当作值来操作」的场景。
公众号:蓝海资料掘金营,微信 deep3321