C++笔记3
文章目录
可调用对象与lambda表达式
C++ 提供了多种方式来表示和操作可调用对象,包括传统的函数指针、仿函数(Functors)、Lambda表达式、std::function 和 std::bind 等。这些工具极大地增强了C++的灵活性和表达能力,尤其在处理回调、事件驱动编程和函数式编程时表现尤为出色(网络编程会经常用)
另:本节的所有代码写在一个项目里,对于在前边定义过的函数后边不会再写定义
函数指针
详见笔记1,这里做些补充
优点:简单直观,适用于简单的回调函数
局限:
- 不能捕获上下文(比如lambda的闭包)
- 语法相对复杂,尤其是指针声明及使用
仿函数(Functors)
这个之前讲过,又称函数对象(Function Objects)是在C++中重载了operator() 的类或结构体实例。仿函数不仅可以像普通函数一样被调用,还能携带状态,提供更大的灵活性和功能性
使用仿函数的时机?需要携带状态时:当回调函数需要维护内部状态时,仿函数时理想的选择
特点:
- 携带状态:仿函数可以拥有内部状态,通过成员变量存储数据,使其在调用时具备上下文信息
- 灵活性高:可以根据需要添加更多的成员函数和变量,扩展功能
- 性能优化:编译器可以对仿函数进行优化,例如内联展开,提高执行效率
简单案例
#include<iostream>
using namespace std;
class Adder
{
public:
int to_add;
Adder(int val):to_add(val){}//构造函数
int operator()(int a) const//重载()
{
return a+to_add;
}
int add(int x)
{
return x + to_add;
}
};
int main()
{
Adder a;//创建仿函数
cout<<a(1,5);//输出6
return 0;
}
此外,许多标准算法也接收仿函数为参数,使得算法的行为更加灵活和可定制
之前忘记提了,企业开发中最好不要用using namespace std;这种写法。因为企业级代码中会用到多个namespace,这么写很可能会导致命名空间污染。std中有数百个标识符,如vector、count、list等。而如果你自己也定义了同名的变量或者函数,或者别的命名空间有同名的,编译器就不知道该调用哪个了,会报错。当然,对于cout,endl这种高频率需求的语句,我们可以采用精确声明(Preferred Alternative):
//仅引入cout与endl,而不是整个命名空间
using std::cout;
using std::endl;
高级案例
#include<iostream>
#include<vector>
#include<algorithm>
using namespaces std;
struct IsGreaterThan//判断一个值是否大于某阈值
{
int thereshold;
IsGreaterThan(int val):thereshold(val){}
bool operator()(int x)
{
return x > thereshold;
}
};
struct Multiply
{
int operator()(int a, int b) {
return a * b;
}
};
int main()
{
vector<int>numbers={1, 5, 10, 15, 20};
// 使用仿函数进行筛选
IsGreaterThan greaterThan10(10);
//参数分别是起始迭代器、末端迭代器与查找规则
auto it = find_if(numbers.begin(), numbers.end(), greaterThan10);
if(it != numbers.end())
cout << "第一个大于10的数是: " << *it << endl; // 输出: 第一个大于10的数是: 15
else
cout << "没有找到大于10的数。" << endl;
return 0;
}
仿函数也可以与模板相结合,可以实现高度通用和可复用的代码。例如,编写一个通用的比较仿函数
#include <iostream>
#include <vector>
#include <algorithm>
// 通用比较仿函数
template <typename T>
struct Compare {
bool operator()(const T& a, const T& b) const {
return a < b;
}
};
int main() {
std::vector<int> numbers = {5, 2, 8, 1, 9};
// 使用仿函数进行排序
std::sort(numbers.begin(), numbers.end(), Compare<int>());
std::cout << "排序后的数字: ";
for(auto num : numbers) {
std::cout << num << " "; // 输出: 1 2 5 8 9
}
std::cout << std::endl;
return 0;
}
总结
仿函数优势
- 可扩展性: 能够根据需要添加更多功能和状态。
- 与Lambda的互补性: 在需要携带复杂状态或多次调用时,仿函数比Lambda更适合。
- 类型安全: 仿函数是具体的类型,可以在编译期进行类型检查
何时需要仿函数
- 需要携带状态时: 当回调函数需要维护内部状态时,仿函数是理想选择。
- 复杂操作: 当简单的函数指针或Lambda难以表达复杂逻辑时。
- 性能关键场景: 由于仿函数可以被编译器优化,适用于性能敏感的代码
lambda表达式
C++11引入的轻量级函数对象,允许在代码中定义匿名函数。它们可以捕获周围的变量,具有更强的表达能力
基本语法
[_captures](_parameters)->return_type{
//函数体
}
- captures:捕获变量的方式,有值捕获、引用捕获和混合捕获
- 值捕获 (
[=]): 捕获所有外部变量的副本 - 引用捕获 (
[&]): 捕获所有外部变量的引用 - 混合捕获: 指定部分变量按值捕获,部分按引用捕获,如
[=, &var]或[&, var] - 无捕获 (
[]): 不捕获任何外部变量
- parameters:参数列表
- return_type:返回类型,可以省略,编译器会自动推导
- 函数体:实际执行的代码
简单案例
//main函数内
int temp = 2;
auto lambda1 = [&temp](int x)->void//想修改temp的值必须用引用捕获
{
temp *= x;
};
lambda1(3);
cout << temp << endl; //输出6
}
注:也可以用一个指针int *ptemp=&temp,然后捕获此指针。然后在函数体内解引用也可以:*ptemp*=x; 另外尽量不要用[&]这种形式,会把所有局部变量都给你拷贝过来,这样栈可能很快就被占满了
高级案例
#include<thread>//上边的代码已经包含了其他头文件以及相关仿函数定义了,这里不再多写
int main()
{
thread t1;
{
auto add_ptr = make_shared<Adder>(10);
auto lambda2 = [add_ptr](int x)//捕获智能指针
{
this_thread::sleep_for(chrono::seconds(5));
add_ptr->add(x);
cout << add_ptr.use_count() << endl;
};
t1=thread(lambda2,5);//线程复制整个lambda对象
cout << "before } use count: " << add_ptr.use_count() << endl;//输出3
}// add_ptr离开作用域,栈上的add_ptr被销毁,引用计数-1 => 2(但不会归零!)
t1.join();
// 等待线程结束,线程被销毁,引用计数-1 => 1
//lambda自身副本被销毁,引用计数归零
}
这是个很重要的案例,接下来我们解析一下
首先闭包是JS里指有权访问另一个函数作用域中变量(局部变量)的函数,C++是没有闭包的,而lambda表达式配合智能指针可以实现伪闭包(详见C++网络编程-利用伪闭包延长连接的生命周期)
在异步编程中,如果我们不用智能指针的方式申请内存,有可能我们任务执行到一半别的线程就把我们的内存给释放了,这很危险。而这段代码确保了对象的生命周期覆盖整个线程执行过程:我们这个lambda表达式捕获了智能指针,只要我没使用lambda表达式或者没执行函数体,智能指针的值就至少为1,不为0,这样这块内存就不会被回收掉。只要异步回调还没回调,我这块儿内存就是安全的,可以放心使用
上边儿代码的具体时间轴如下:
- 创建
add_ptr[ref = 1] - lambda定义[ref = 2] (值捕获 + 1)
- 启动线程[ref = 3] (线程复制lambda + 1)
- 离开作用域[ref = 2] (原始
add_ptr销毁 - 1) - 线程执行中[ref = 2] (对象存活!)
- 线程结束[ref = 0] (销毁所有副本)
注意体会为什么引用计数不会归零
- 值捕获本质:捕获的是智能指针对象本身(不是裸指针)
- 线程安全机制:
shared_ptr的引用计数是原子操作 - 对象生命周期:由最后一个持有它的智能指针决定
典型错误:
// 危险!裸指针捕获
thread([&add_ptr](){
add_ptr->add(5); // 可能访问已销毁对象
});
这个例子也体现出了RAII思想:对象的生命周期严格绑定到持有它的最后一个智能指针,确保线程安全的同时避免资源泄漏
可变lambda
默认情况下,Lambda表达式是不可变的(const)。通过mutable关键字,可以允许修改捕获的变量副本。用得不多,看看就行
用lambda表达式捕获成员函数和类变量
Lambda表达式可以捕获类的成员变量和成员函数,使其在类的上下文中更加灵活
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Processor {
public:
Processor(int threshold) : threshold(threshold) {}
void process(std::vector<int>& data) {
cout << "处理前数据: ";
for(auto num : data) cout << num << " ";
cout << endl;
// 使用Lambda表达式进行过滤
/*auto new_it=remove_if(data.begin(), data.end(),
[this](int n) {
return n < threshold;
})*/
data.erase(remove_if(data.begin(), data.end(),
[this](int n) {
return n < threshold;
}), data.end());
cout << "处理后数据: ";
for(auto num : data) cout << num << " ";
cout << endl;
}
private:
int threshold;
};
int main() {
vector<int> numbers = {1, 6, 3, 8, 2, 7};
Processor proc(5);
proc.process(numbers);//把小于5的数据都去掉
/*
输出:
处理前数据: 1 6 3 8 2 7
处理后数据: 6 8 7
*/
return 0;
}
Lambda表达式的优势
- 简洁性: 代码更加紧凑,易于理解。
- 灵活性: 能够捕获外部变量,适应更多场景。
- 性能优化: 编译器可以对Lambda进行优化,如内联展开。
- 与标准库的良好集成: 与STL算法无缝结合,简化代码逻辑
function与bind
function
std::function 是C++11提供的一个通用的可调用包装器,能够封装任何可调用对象,包括普通函数、Lambda表达式、函数对象以及绑定表达式。它实现了类型擦除,使得不同类型的可调用对象可以通过统一的接口进行操作。实际上std::function是一个模板类,使用时要包含头文件#include<functional>
直观展示function:
std::function 就像一个万能容器:
+-----------------------+
| 可调用对象适配器 |
| |
| [ 存储空间 ] | → 存放函数指针/Lambda/函数对象
| [调用接口 operator()] | → 统一调用方式
+-----------------------+
简单案例:
#include<functional>
#include<iostream>
using namespace std;
int main()
{
Multiply mul;
//用function封装普通函数
function<int(int,int)>func1=add;//尖括号内为返回类型及参数类型
cout<<func1(1,1)<<endl;//输出2
//封装仿函数
function<int(int, int)>func2 = mul;
cout << func1(3, 2) << endl;//输出6
//封装lambda
function<int(int,int)>func3= [](int a, int b) {return a - b; };
cout << func3(10,10) << endl;//输出0
return 0;
}
特点
- 类型擦除: 可以存储任何符合签名的可调用对象。
- 灵活性: 支持动态改变存储的可调用对象。
- 性能开销: 相比于直接使用函数指针或Lambda,
std::function可能带来一定的性能开销,尤其是在频繁调用时
使用场景
- 回调函数的传递
- 事件处理系统
- 策略模式的实现
示例:回调机制
//定义回调类型:Callback是function类型,返回类型为void,参数类型为int
using Callback=function<void(int)>;
//触发事件的函数
void triggerEvent(Callback cb,int val)
{
cb(val);//事件发生时,调用回调
}
int main()
{
// 使用Lambda作为回调,两参数分别对应cb和val
triggerEvent([](int x) {
cout << "事件触发,值为: " << x << endl;
}, 42); // 输出: 事件触发,值为: 42
return 0;
}
实际开发中的回调不会这么简单,在网络编程中,我们往往会把上边的回调函数注册在定义好的数据结构如map、队列中,用到的时候再通过事件函数去查找
示例:存储和调用不同类型的可调用对象
此为企业级开发中常用的手法
#include<vector>
int main()
{
vector<function<int(int,int)>>operations;
//添加不同类型的可调用对象
//emplace_back比push_back少一次拷贝的开销
operations.emplace_back(add);//普通函数
operations.emplace_back(Multiply());//仿函数
operations.emplace_back([](int a, int b) {return a - b; });//lambda
for (auto& op : operations)
{
cout << op(10, 5) << endl;
}
return 0;
}
输出
15
50
5
bind
std::bind 是C++11中提供的一个函数适配器,用于绑定函数或可调用对象的部分参数,生成一个新的可调用对象。它允许提前固定某些参数,简化函数调用或适应接口需求
这玩意在网络编程中也常用
基本用法
#include<functional>
int main()
{
//将add的第一个参数绑定为10,第二个参数不变
auto new_add=bind(add,10,placeholders::_1);//新生成的函数仅接收一个参数
cout<<new_add(5)<<endl;//输出15
}
占位符std::placeholders:使用它来表示未绑定的参数。在上例中,编译器会自动检测占位符,这样它知道了add的第二个参数并没被绑定,并将new_add的参数传入add的第二个参数
占位符最多使用20个(placeholders::_20)
简单示例
void display(const string &msg,int count)
{
for(int i=0;i<count;i++)
cout<<msg<<endl;
}
void print(int a,int b,int c)
{
cout<<"print: "<<a<<","<<b<<","<<c<<" "<<endl;
}
int main()
{
// 绑定消息为"Hello",生成新的函数对象,只需要传递次数
auto sayHello = bind(display, "Hello", placeholders::_1);
sayHello(3);
/*
输出:
Hello
Hello
Hello
*/
// 绑定次数为2,生成新的函数对象,只需要传递消息
auto sayTwice = bind(display, placeholders::_1, 2);
sayTwice("Hi");
/*
输出:
Hi
Hi
*/
//实现逆向打印
auto new_func=bind(print,placeholders::_3,placeholders::_2,placeholders::_1);
new_func(1,2,3);//输出print:3,2,1
return 0;
}
上例中的new_func可以不用auto,类型为function<void(int,int,int)>,但推荐用auto,别给自己没事找事干,写错了还麻烦
对比lambda
虽然bind曾在C++11中广泛使用,但随着Lambda表达式的普及,很多情况下Lambda更为直观和高效。不过,在某些复杂的参数绑定场景下,bind 依然有其独特优势
byd学了半天白学了?
int main()
{
// 绑定第一个参数为2,生成新的函数对象
//auto multiplyBy2 = bind(multiply, 2, placeholders::_1);
//cout << "2 * 5 = " << multiplyBy2(5) << endl; // 输出: 2 * 5 = 10
// 使用Lambda表达式绑定第一个参数为2
auto multiplyBy2 = [](int b) -> int {
return multiply(2, b);
};
cout << "2 * 5 = " << multiplyBy2(5) << endl; // 输出: 2 * 5 = 10
return 0;
}
绑定成员函数
class Calculator {
public:
int ret;
int multiply(int a, int b) {
ret = a * b;
return ret;
}
};
int main() {
Calculator calc;
// 绑定成员函数multiply,固定第一个参数为5
auto multiplyBy5 = bind(&Calculator::multiply,&calc,5,placeholders::_1);
cout << "5 * 3 = " << multiplyBy5(3) <<endl; // 输出: 5 * 3 = 15
//对比lambda实现
auto new_mul=[&calc](int a){return calc.multiply(5,a);}//一定要引用捕获,不然报错
cout<<"5 * 3 = " << new_mul(3) <<endl; // 输出: 5 * 3 = 15
return 0;
}
注意:
- 绑定类的成员函数时需要取地址
& - 上面也说过了,lambda最好捕获智能指针,因为有可能我new_mul还没调用,calc就被释放了
当然静态的成员函数也能绑定,因为静态成员函数不属于任何对象,所以不用像上面那样绑定一个实例
class Logger {
public:
static void log(const string& message) {
cout << "Log: " << message << endl;
}
};
int main() {
// 使用bind绑定静态成员函数
auto logFunc = bind(&Logger::log, placeholders::_1);
logFunc("This is a static log message."); // 输出: Log: This is a static log message.
return 0;
}
参考
更多推荐

所有评论(0)