【c++面向对象编程】第48篇:Lambda表达式与std::function:OOP中的函数式编程

目录

[一、Lambda 的基本语法](#一、Lambda 的基本语法)

二、捕获列表(Capture)

[值捕获 [=]](#值捕获 [=])

[引用捕获 [&]](#引用捕获 [&])

混合捕获

特定变量捕获

[C++14 广义捕获(带初始值)](#C++14 广义捕获(带初始值))

[C++17 捕获 *this](#C++17 捕获 *this)

[三、mutable 关键字](#三、mutable 关键字)

[四、泛型 Lambda(C++14)](#四、泛型 Lambda(C++14))

五、std::function:统一的可调用对象包装器

存储成员函数

[六、Lambda 与 std::function 的性能对比](#六、Lambda 与 std::function 的性能对比)

七、完整例子:回调系统

[八、Lambda 取代传统函数指针](#八、Lambda 取代传统函数指针)

[九、Lambda 与 STL 算法](#九、Lambda 与 STL 算法)

十、常见错误

[1. 捕获引用导致悬空引用](#1. 捕获引用导致悬空引用)

[2. 默认捕获 [=] 捕获 this](#2. 默认捕获 [=] 捕获 this)

[3. 在循环中捕获引用变量](#3. 在循环中捕获引用变量)

[4. std::function 赋值开销大](#4. std::function 赋值开销大)

十一、这一篇的收获


一、Lambda 的基本语法

cpp

复制代码
[capture](parameters) -> return_type {
    body
}

最简单形式:

cpp

复制代码
auto greet = []() { cout << "Hello" << endl; };
greet();  // 调用

// 带参数
auto add = [](int a, int b) { return a + b; };
cout << add(3, 5);  // 8

// 指定返回类型(通常可省略)
auto divide = [](double a, double b) -> double {
    if (b == 0) return 0;
    return a / b;
};

二、捕获列表(Capture)

捕获列表定义了 Lambda 如何访问外部变量。

值捕获 [=]

cpp

复制代码
int x = 10, y = 20;
auto add = [=]() { return x + y; };  // 拷贝 x, y
cout << add();  // 30
x = 100;        // 不影响 Lambda 内部的值
cout << add();  // 仍然是 30

引用捕获 [&]

cpp

复制代码
int x = 10, y = 20;
auto add = [&]() { return x + y; };
cout << add();  // 30
x = 100;        // Lambda 内部也会变
cout << add();  // 120

混合捕获

cpp

复制代码
int a = 1, b = 2, c = 3;
auto func = [a, &b](int x) {   // a 值捕获,b 引用捕获,c 不捕获
    // return a + b + c;  // ❌ c 不可用
    return a + b + x;
};

特定变量捕获

cpp

复制代码
int x = 10, y = 20, z = 30;
auto f1 = [x]() { return x; };           // 只捕获 x
auto f2 = [&y]() { return y; };          // 只捕获 y(引用)
auto f3 = [x, &y]() { return x + y; };   // x 值,y 引用

C++14 广义捕获(带初始值)

cpp

复制代码
int x = 10;
auto f = [y = x + 5]() { return y; };  // y 用表达式初始化
cout << f();  // 15

auto g = [ptr = make_unique<int>(42)]() { return *ptr; };

C++17 捕获 *this

在成员函数中捕获 *this 的值(而非指针):

cpp

复制代码
class Widget {
    int value = 10;
public:
    void memberFunc() {
        auto f1 = [this]() { return value; };   // 捕获 this 指针
        auto f2 = [*this]() { return value; };  // C++17:拷贝整个对象
        // f2 修改不影响原对象
    }
};

三、mutable 关键字

默认情况下,Lambda 的 operator()const,不能修改值捕获的变量。加 mutable 可修改副本:

cpp

复制代码
int count = 0;
auto increment = [count]() mutable { 
    return ++count;   // 修改的是 Lambda 内部的副本
};
cout << increment();  // 1
cout << increment();  // 2
cout << count;        // 0(原变量不变)

四、泛型 Lambda(C++14)

参数可以用 auto,让 Lambda 成为模板:

cpp

复制代码
auto add = [](auto a, auto b) { return a + b; };
cout << add(3, 5);          // 8
cout << add(3.14, 2.86);    // 6.0
cout << add(string("a"), "b");  // "ab"

相当于编译器生成多个版本的 operator() 重载。


五、std::function:统一的可调用对象包装器

std::function 可以存储任何可调用对象:函数指针、Lambda、函数对象、成员函数。

cpp

复制代码
#include <functional>

// 存储函数指针
int add(int a, int b) { return a + b; }
std::function<int(int, int)> f1 = add;

// 存储 Lambda
std::function<int(int, int)> f2 = [](int a, int b) { return a * b; };

// 存储函数对象
struct Multiply {
    int operator()(int a, int b) const { return a * b; }
};
std::function<int(int, int)> f3 = Multiply();

// 使用
cout << f1(3, 5);  // 8
cout << f2(3, 5);  // 15

存储成员函数

成员函数需要一个对象实例:

cpp

复制代码
struct Calculator {
    int add(int a, int b) const { return a + b; }
};

Calculator calc;
// 使用 std::bind 或 Lambda
std::function<int(int, int)> f = [&calc](int a, int b) { 
    return calc.add(a, b); 
};

// 或用 std::mem_fn
#include <functional>
std::function<int(const Calculator&, int, int)> f2 = &Calculator::add;

六、Lambda 与 std::function 的性能对比

特性 原始 Lambda std::function 包装
类型 唯一匿名类型 类型擦除后的通用类型
内存 栈上(大小=捕获大小) 堆上可能分配(取决于大小)
调用开销 可内联 间接调用(虚函数风格)
适用场景 性能敏感,局部使用 需要存储、传递时

结论 :能直接用 auto 就用 auto,只有在需要存储不同类型可调用对象时才用 std::function

cpp

复制代码
// 推荐
auto lambda = [](int x) { return x * 2; };

// 仅当需要统一类型容器时
vector<function<int(int)>> callbacks;
callbacks.push_back([](int x) { return x + 1; });
callbacks.push_back([](int x) { return x * 2; });

七、完整例子:回调系统

cpp

复制代码
#include <iostream>
#include <vector>
#include <functional>
#include <string>
#include <algorithm>
using namespace std;

// 事件类型
enum class Event {
    Click,
    KeyPress,
    MouseMove
};

// 事件管理器:存储不同事件的回调
class EventManager {
private:
    using Callback = function<void(const string&)>;
    vector<pair<Event, Callback>> handlers;
    
public:
    void subscribe(Event e, Callback cb) {
        handlers.emplace_back(e, move(cb));
    }
    
    void fire(Event e, const string& data) {
        for (const auto& [event, callback] : handlers) {
            if (event == e) {
                callback(data);
            }
        }
    }
};

int main() {
    EventManager em;
    
    // 注册 Lambda 回调
    em.subscribe(Event::Click, [](const string& data) {
        cout << "[Click 处理器 1] " << data << endl;
    });
    
    em.subscribe(Event::Click, [](const string& data) {
        cout << "[Click 处理器 2] 收到点击: " << data << endl;
    });
    
    em.subscribe(Event::KeyPress, [](const string& key) {
        cout << "[按键处理器] 按下: " << key << endl;
    });
    
    // 存储有状态的 Lambda
    int clickCount = 0;
    em.subscribe(Event::Click, [&clickCount](const string& data) {
        clickCount++;
        cout << "[计数器] 点击次数: " << clickCount << endl;
    });
    
    // 触发事件
    cout << "=== 触发 Click 事件 ===" << endl;
    em.fire(Event::Click, "按钮被按下");
    
    cout << "\n=== 触发 KeyPress 事件 ===" << endl;
    em.fire(Event::KeyPress, "Enter");
    
    return 0;
}

输出:

text

复制代码
=== 触发 Click 事件 ===
[Click 处理器 1] 按钮被按下
[Click 处理器 2] 收到点击: 按钮被按下
[计数器] 点击次数: 1

=== 触发 KeyPress 事件 ===
[按键处理器] 按下: Enter

八、Lambda 取代传统函数指针

传统 C 风格回调:

cpp

复制代码
// 传统方式:需要定义全局函数或 static 成员
int compare_int(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}
qsort(arr, n, sizeof(int), compare_int);

现代 C++ 方式:

cpp

复制代码
vector<int> v = {3, 1, 4, 1, 5};

// 使用 Lambda
sort(v.begin(), v.end(), [](int a, int b) { return a > b; });

// 自定义复杂排序
sort(v.begin(), v.end(), [](int a, int b) {
    // 按绝对值降序,绝对值相同时按原值降序
    int abs_a = abs(a), abs_b = abs(b);
    if (abs_a != abs_b) return abs_a > abs_b;
    return a > b;
});

优势

  • 逻辑定义在调用点,可读性好

  • 可以捕获局部变量,不需要全局数据

  • 编译器可内联,性能更好


九、Lambda 与 STL 算法

cpp

复制代码
#include <algorithm>
#include <vector>
#include <numeric>
using namespace std;

// 查找第一个满足条件的元素
vector<int> v = {1, 2, 3, 4, 5, 6};
auto it = find_if(v.begin(), v.end(), [](int x) { return x > 3 && x % 2 == 0; });
// 找到 4

// 转换每个元素
transform(v.begin(), v.end(), v.begin(), [](int x) { return x * 2; });

// 条件删除
v.erase(remove_if(v.begin(), v.end(), [](int x) { return x < 5; }), v.end());

// 累加时处理每个元素
int sum = accumulate(v.begin(), v.end(), 0, [](int acc, int x) { 
    return acc + x * x;  // 平方和
});

// 生成数据
generate_n(back_inserter(v), 10, [n = 0]() mutable { return n += 2; });

十、常见错误

1. 捕获引用导致悬空引用

cpp

复制代码
function<int()> createCounter() {
    int local = 0;
    return [&local]() { return ++local; };  // ❌ 返回后 local 销毁
}
// 使用时会访问已销毁的内存

修正:值捕获 [=][local]

2. 默认捕获 [=] 捕获 this

cpp

复制代码
class Widget {
    int x = 10;
public:
    auto getLambda() {
        return [=]() { return x; };  // [=] 捕获了 this 指针,不是 x 的副本
    }
};

修正:C++17 可用 [*this] 拷贝整个对象,或显式捕获 [x]

3. 在循环中捕获引用变量

cpp

复制代码
vector<function<int()>> funcs;
for (int i = 0; i < 5; i++) {
    funcs.push_back([&i]() { return i; });  // ❌ 所有 Lambda 共享同一个 i
}
for (auto& f : funcs) cout << f() << " ";   // 输出 5 5 5 5 5

修正:值捕获 [i]

4. std::function 赋值开销大

std::function 可能会分配堆内存。频繁复制大的 function 对象可能成为性能瓶颈。


十一、这一篇的收获

你现在应该理解:

  • Lambda 语法[capture](params) mutable -> ret { body }

  • 捕获方式[=] 值、[&] 引用、[this][*this](C++17)

  • 泛型 Lambda[](auto x, auto y) 参数用 auto

  • std::function:类型擦除包装器,可存储任何可调用对象

  • 性能 :优先用 auto 保存 Lambda,需要统一类型时才用 std::function

💡 小作业:实现一个 TaskScheduler 类,支持延迟执行和周期性执行任务(存储 std::function<void()> 回调)。用 Lambda 注册任务,测试单个任务和循环任务。


下一篇预告 :第49篇《面向对象的单元测试:用GoogleTest测试类》------如何用 GoogleTest 测试 C++ 类?测试夹具(Test Fixture)复用对象,EXPECT_* vs ASSERT_*,以及如何 mock 虚接口。下篇讲 OOP 代码的测试实践。

相关推荐
:1216 小时前
java基础---一些没注意的
java·开发语言
marsh02066 小时前
54 openclaw钩子函数使用:在框架生命周期中注入自定义逻辑
java·前端·spring
chxin140166 小时前
CMake 笔记
c++
手写码匠6 小时前
【实战评测】华为云 MaaS 平台 DeepSeek 大模型推理服务 + Dify 一键部署全攻略
人工智能·深度学习·算法·aigc
艳阳天_.6 小时前
星瀚物料序时簿批量分类功能二开
java
日月云棠6 小时前
11 Spring容器整合与核心接口体系
java·后端
咪饭只吃一小碗6 小时前
JS算法基础: 常用方法整理
算法·程序员
日月云棠6 小时前
10 AOP与动态编译源码剖析
java·后端
AI人工智能+电脑小能手6 小时前
【大白话说Java面试题 第70题】【JVM篇】第30题:垃圾回收器是怎样寻找 GC Roots 的?
java·开发语言·jvm·面试