函数进阶:递归函数、函数指针、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

函数指针的语法有点丑,尤其是返回函数指针的函数——那声明能让你怀疑人生。我个人习惯用 usingtypedef 给它起个别名,代码可读性会好很多。

小技巧
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 仍然是一把利器。

现代C++建议
能用 lambda 的地方优先用 lambda,可读性更好。std::bind 的占位符语义对新手不太友好。我只有在需要「部分求值」或「适配旧接口」时才用 bind。

知识体系总览

下面这张图把本章的核心概念串起来了。你可以看到,从最基础的递归函数,到函数指针,再到 lambda、std::function、std::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