一、Lambda表达式:让代码更简洁

Lambda表达式,说白了就是匿名函数。你可以在需要函数的地方直接写一段逻辑,不用专门去定义一个函数。我个人觉得,这是C++11引入的最实用的特性之一。

1.1 基本语法

Lambda的语法结构其实很简单:

[捕获列表](参数列表) -> 返回类型 {
    函数体
}

嗯,这里要注意,返回类型可以省略,编译器会自动推导。我刚开始用的时候,经常纠结要不要写返回类型,后来发现大部分场景下都可以不写。

来看个最简单的例子:

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> nums = {3, 1, 4, 1, 5, 9, 2, 6};
    
    // 用Lambda排序
    std::sort(nums.begin(), nums.end(), 
              [](int a, int b) { return a < b; });
    
    // 用Lambda打印
    std::for_each(nums.begin(), nums.end(),
                  [](int n) { std::cout << n << " "; });
    
    return 0;
}

你想想看,如果没有Lambda,你得单独写个比较函数,再传进去。代码分散不说,可读性也差。

1.2 捕获列表:Lambda的灵魂

捕获列表是Lambda最核心的部分。它决定了Lambda能访问外部哪些变量。

捕获方式 语法 说明
值捕获 [x] 捕获x的副本,Lambda内部修改不影响外部
引用捕获 [&x] 捕获x的引用,内部修改会影响外部
隐式值捕获 [=] 捕获所有用到的外部变量(按值)
隐式引用捕获 [&] 捕获所有用到的外部变量(按引用)
混合捕获 [=, &x] 除x按引用外,其余按值
避坑指南:我曾经在项目里吃过亏——用值捕获了一个大对象,结果Lambda被反复调用,每次都要拷贝这个对象,性能直接崩了。后来改成引用捕获,问题解决。记住:值捕获会拷贝,引用捕获不会

来看个实际例子:

#include <iostream>

int main() {
    int x = 10, y = 20;
    
    // 值捕获
    auto lambda1 = [x]() { 
        // x = 100; // 编译错误!值捕获的变量是const的
        return x + y; // 错误!y没有被捕获
    };
    
    // 引用捕获
    auto lambda2 = [&x, &y]() {
        x = 100; // 可以修改
        y = 200;
        return x + y;
    };
    
    // 隐式捕获
    auto lambda3 = [=]() { return x + y; };  // 全部按值
    auto lambda4 = [&]() { x = 100; return x + y; };  // 全部按引用
    
    lambda2();
    std::cout << "x = " << x << ", y = " << y << std::endl;
    
    return 0;
}

二、函数对象(仿函数):类的另一种用法

函数对象,也叫仿函数,就是重载了operator()的类对象。它看起来像函数,实际上是个对象。我个人的习惯是,当需要保存状态时,优先用函数对象而不是Lambda。

#include <iostream>
#include <vector>
#include <algorithm>

class GreaterThan {
private:
    int threshold;
public:
    GreaterThan(int t) : threshold(t) {}
    
    bool operator()(int value) const {
        return value > threshold;
    }
};

int main() {
    std::vector<int> data = {1, 5, 8, 3, 10, 2};
    
    // 使用函数对象
    int count = std::count_if(data.begin(), data.end(), 
                              GreaterThan(5));
    
    std::cout << "大于5的元素个数: " << count << std::endl;
    
    // 用Lambda实现同样的功能
    int threshold = 5;
    count = std::count_if(data.begin(), data.end(),
                          [threshold](int v) { return v > threshold; });
    
    return 0;
}
我的经验:函数对象比Lambda多一个优势——它可以有多个成员函数。比如你可以在类里加个reset()方法重置状态,Lambda做不到这一点。

三、std::function与std::bind:更灵活的函数封装

3.1 std::function:万能函数包装器

std::function可以存储任何可调用对象——普通函数、Lambda、函数对象,甚至成员函数。说白了,它就是个类型擦除的容器

#include <iostream>
#include <functional>

// 普通函数
int add(int a, int b) {
    return a + b;
}

// 函数对象
struct Multiply {
    int operator()(int a, int b) const {
        return a * b;
    }
};

int main() {
    // std::function可以存储各种可调用对象
    std::function<int(int, int)> func;
    
    func = add;  // 存储普通函数
    std::cout << "add: " << func(3, 4) << std::endl;
    
    func = Multiply();  // 存储函数对象
    std::cout << "multiply: " << func(3, 4) << std::endl;
    
    func = [](int a, int b) { return a - b; };  // 存储Lambda
    std::cout << "subtract: " << func(3, 4) << std::endl;
    
    return 0;
}

3.2 std::bind:参数绑定利器

std::bind可以把函数的某些参数固定下来,生成一个新的可调用对象。我在项目中经常用它来适配接口。

#include <iostream>
#include <functional>

void print(int a, int b, int c) {
    std::cout << "a=" << a << ", b=" << b << ", c=" << c << std::endl;
}

int main() {
    using namespace std::placeholders;
    
    // 绑定所有参数
    auto f1 = std::bind(print, 1, 2, 3);
    f1();  // 输出: a=1, b=2, c=3
    
    // 绑定部分参数,用占位符留空
    auto f2 = std::bind(print, _1, 100, _2);
    f2(10, 20);  // 输出: a=10, b=100, c=20
    
    // 改变参数顺序
    auto f3 = std::bind(print, _3, _1, _2);
    f3(10, 20, 30);  // 输出: a=30, b=10, c=20
    
    return 0;
}
注意:std::bind在C++11之后用得少了,因为Lambda可以更清晰地完成同样的工作。我个人建议新项目优先用Lambda,除非你需要处理复杂的参数绑定场景。

四、回调函数实现:实战应用

回调函数是这些技术的典型应用场景。说白了,就是把函数作为参数传给另一个函数,让它在合适的时机调用。

#include <iostream>
#include <functional>
#include <vector>

// 事件处理器
class EventEmitter {
private:
    std::vector<std::function<void(int)>> listeners;
    
public:
    void on(std::function<void(int)> callback) {
        listeners.push_back(callback);
    }
    
    void emit(int data) {
        for (auto& listener : listeners) {
            listener(data);
        }
    }
};

int main() {
    EventEmitter emitter;
    
    // 注册回调 - 用Lambda
    emitter.on([](int data) {
        std::cout << "回调1: 收到数据 " << data << std::endl;
    });
    
    // 注册回调 - 用函数对象
    struct Logger {
        void operator()(int data) const {
            std::cout << "回调2: 记录数据 " << data << std::endl;
        }
    };
    emitter.on(Logger());
    
    // 触发事件
    emitter.emit(42);
    emitter.emit(100);
    
    return 0;
}
核心要点:
  • Lambda适合一次性、简单的回调逻辑
  • 函数对象适合需要保存状态、可复用的回调
  • std::function作为类型擦除的容器,可以统一存储各种回调
  • std::bind在适配接口、固定参数时有用,但Lambda通常更清晰

五、知识体系总览

下面这张图总结了本章的核心内容,帮你理清思路:

Lambda表达式与函数对象知识体系 Lambda表达式 • 基本语法 • 捕获列表 • 值捕获 vs 引用捕获 • 隐式捕获 函数对象 • operator()重载 • 保存状态 • 可复用性 • 多成员函数 std::function • 万能包装器 • 类型擦除 • 存储各种可调用对象 • 统一接口 std::bind • 参数绑定 • 占位符 • 参数重排 • 接口适配 回调函数实现 事件驱动 · 异步处理 · 策略模式 四种可调用对象各有适用场景,实际开发中常组合使用 Lambda最灵活 · 函数对象最稳定 · std::function最通用 · std::bind最适配
我的建议:刚开始学的时候,先掌握Lambda的基本用法和捕获列表。函数对象和std::function可以在实际项目中慢慢体会。别想着一次全学会,编程这东西,用得多了自然就熟了。

公众号:蓝海资料掘金营,微信deep3321