传值返回编译器才强制识别为右值,是编译器优化结果,若没优化会将临时对象作为右值对象去拷贝,引用返回就都没有了
1.统一的列表初始化
{}初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。C++11 更新了一切皆可用{}初始化 (即所有的内置和自定义的类型 ),并且可以不写= 。
注意区分:{}初始化是指一切可以用{}去初始化,初始化列表是专指构造函数中初始化的一部分
cpp
struct point
{
//explicit point(int x, int y)
point(int x,int y)
:_x(x)
,_y(y)
{
cout << "point(int x,int y)" << endl;
}
int _x;
int _y;
};
int main()
{
int x = 1;
int y = { 2 };
int z{ 3 };
int a1[] = { 1,2,3 };
int a2[]{ 1,2,3 };
// 本质都是调用构造函数
point p0(0, 0);
point p1 = { 1,1 };// 多参数构造函数隐式类型转换
point p2{ 2,2 };
//非常量引用需绑定到左值
//列表初始化生成的是右值,应直接初始化或常量引用
const point& r = { 3,3 };
int* ptr1 = new int[3] {1, 2, 3};
point* ptr2 = new point[2]{ p0,p1 };
point* ptr3 = new point[2]{ {10,24},{3,4} };
return 0;
}
当使用explicit关键字来声明构造函数时,禁止发生隐式类型转换,形如=加上{}初始化的形式都将报错。该段代码中={}形式都是多参数隐式类型转换 ,本质是先调用一个构造函数生成临时对象,再调用拷贝构造,但编译器会优化该过程为一次构造,称为返回值优化。
std::initializer_list
std::initializer_list 是 C++11 引入的一个模板类,用于处理初始化列表。它提供了一种方便的方式,允许使用 {} 语法初始化对象和数组,并在标准库中广泛用于实现函数参数的列表初始化。
1.提供对类型为 const T 的元素列表的访问。
2.内部实现:通常由两个指针 _Start 和 _Finish 组成,分别指向列表的起始位置和结束位置。
注意:实现和管理需要编译器的特殊支持,用户无法自己实现类似的功能
1.注意调用initializer_list的构造和隐式类型转换的区别
2.typeid 是 C++ 中的一个运算符,用于获取变量或表达式的类型信息
3.sizeof(il)计算其头尾指针大小,32位系统下总共8字节,64位系统下占16字节
- 使用场景:
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加
std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值
2.声明
auto
在C++98中auto是一个存储类型的说明符 ,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。int y = 20; // 等效于 auto int y = 20; C++98中,auto是多余的
C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
decltype
关键字decltype将变量的类型声明为表达式指定的类型。
注意:typeid(pf).name() ptr; typeid推出类型是一个字符串,只能看不能用
decltype先推出对象的类型,再定义变量,或者作为模板实参单纯先定义一个变量出现。适用于只需声明不需初始化的场景,auto就无法做到必须进行显示初始化。
nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
一个有歧义的例子
我们本意定义NULL为空指针,但是它为0编译器认为它是整型,func函数参数类型不同构成重载,根据调试可知,调用函数时1编译器将NULL默认为整型0进入第一个func函数,违背了我们的设想,所以C++11中新增了nullptr,用于表示空指针。
3.STL中一些变化
用橘色圈起来是C++11中的一些几个新容器,但是实际最有用的是unordered_map和unordered_set
array再此之前已了解过
std::forward_list 和 std::list对比:
forward_list:
1.实现为单向链表 ,每个节点只维护一个指向下一个节点的指针。因此,它的内存占用相对较小。
2.插入和删除操作:在已知插入位置的迭代器时,插入和删除操作的时间复杂度为O(1)。
随机访问:不支持随机访问 ,遍历操作需要从头开始逐个访问节点,时间复杂度为O(n)。
3.使用场景:
适用于对内存占用敏感的场景,或者主要进行单向遍历和简单插入、删除操作的场景。
list:
1.实现为双向链表,每个节点维护两个指针,分别指向下一个节点和上一个节点。这使得它的内存占用相对较大。
2.插入和删除操作:在已知插入位置的迭代器时,插入和删除操作的时间复杂度为O(1)。
随机访问:同样不支持随机访问,但因为是双向链表,在某些操作中可能更灵活。
3.使用场景:
适用于需要频繁进行双向遍历和复杂链表操作的场景。
容器中的一些新方法如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。实际上C++11更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本,可以提高效率
4.右值引用和移动语义
左值引用和右值引用
无论左值引用还是右值引用,都是给对象取别名。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们**可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。**定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址 。右值引用就是对右值的引用,给右值取别名。
左值引用与右值引用比较
左值引用总结:
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
右值引用总结:
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
右值引用的使用场景和意义
前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!
-
左值引用的使用场景
做参数和返回值,可以减少拷贝提高效率
-
左值引用的短板
当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回 (否则非法访问已释放的空间),只能传值返回。传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
右值不同的称呼
内置类型:纯右值
自定义类型:将亡值
移动构造
移动构造本质是将参数右值的资源窃取过来,占位已有 ,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
我们在string的模拟实现中加上移动构造和赋值
cpp
//普通构造
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
//移动构造
string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
//移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string && s) -- 移动拷贝" << endl;
swap(s);
return *this;
}
下文内容图中编译器是vs2019,代码运行结果截图是vs2022
当存在移动构造和赋值时,返回值是右值就调用移动构造,移动构造没有开新空间,直接交换数据,我可以拿到我想要的数据,你还得帮我带走并销毁掉我不想要的,效率提高了 。如果是返回值是左值就老实调用深拷贝的拷贝构造
返回值是左值的编译器优化情况(右值情况下文有分析)vs2022环境
最后两行和上述一样赋值函数是现代写法,先构造传入参数的临时对象然后进行赋值,编译器将赋值优化为直接构造。看前三行,编译器将返回值str的一次深拷贝优化为ret2和返回值str处的直接构造。
优化之处在于:把移动构造优化为直接构造少了一次交换所以更高效
注意:我们只要明白移动构造的发生条件和作用思想即可,打印结果的优化不用过分关注
ret返回的为一个右值,用这个右值构造ret1,若既有拷贝构造又有移动构造,调用就会匹配调用移动构造,因为编译器会选择最匹配的参数调用,这里就是一个移动语义
注意:编译器版本比较高就可能跳过了拷贝构造和移动赋值,直接在ret1处进行普通字符串构造str
移动赋值
编译器版本高就会跳过移动构造直接在str和ret2处调用普通构造,再移动拷贝给ret2,将两次拷贝优化成一次
左右值引用可构成重载
移动语义总结
左值引用核心价值是减少拷贝,提高效率。右值引用核心价值是进一步减少拷贝,弥补左值引用没有解决的场景,如传值返回
右值引用对内置类型没有意义,代价不大,最大一般就8字节,要关注消耗空间内存的大头,如一些数据结构的拷贝。浅拷贝的类代价略大于内置类型,移动构造不需要实现,没有什么需要转移的资源,只能是直接拷贝
弥补左值引用没有解决的场景:
1.自定义类型中深拷贝的类,必须传值返回的场景
返回值ret被识别为右值(将亡值),ret1处发生移动构造直接继承指向ret的资源空间,ret接收ret1的资源空间出生命周期后直接销毁释放。省略了传值返回先构造临时对象再进行拷贝构造的一次数据拷贝,完成资源交换,对于占用大量内存空间的对象效率大大提高.
下一个场景:
通过调试可知copy不会移动构造ret3,因为ret3被识别为一个左值。
通过一个已存在对象去构造一个新对象会调用拷贝构造,更新一个已有对象的状态会调用赋值,区分!!!
可以看到通过std::move(ret3);实现移动构造,将左值转化为右值。
move的实现机制类似函数调用,返回值是一个右值,但传入的参数本身没有变成右值。总结就是move表达式不会改变传入参数的属性,只有返回值会改变。
2.容器的插入接口,如果插入对象是右值,可以利用移动构造转移资源给数据结构中的对象
第三段代码是平时常写的类型,能像第二段一样实现移动拷贝,因为第三段直接将字面量或临时对象传递给函数时,编译器会隐式地创建一个右值引用触发移动构造,相当于单参数的构造函数支持隐式类型转换,生成一个匿名对象
5.完美转发
模板中的&&(T&& t)不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型 ,后续使用中都退化成了左值,因为右值无法修改,右值引用变量的属性会被编译器识别为左值,否则在移动构造的场景下,无法完成资源的转移,必须要修改。
但在一些场景下希望能够在传递过程中保持它的左值或右值属性,就需要完美转发来实现
cpp
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 万能引用:既可以接收左值,又可以接收右值
// 实参左值,他就是左值引用(引用折叠)
// 实参右值,他就是右值引用
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
这是没有使用完美转发的场景,发现右值引用类型在后续传递过程中都退化为了左值
完美转发在传参的过程中保留对象原生类型属性
只需加上std::forward(t),就可以在传参的过程中保持t的原生类型属性。
总结
完美转发的模板参数必须是传参时推导得出,而不是实例化得出。
类模板的成员函数中可以再写成模板,通常用于在一个类模板中实现完美转发时再定义一个模板参数,确保模板参数是传参时推导得出。
6.lambda表达式
出现背景:
随着C++语法的发展,人们开始觉得普通的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。lambda表达式实际是一个匿名函
数。可以像仿函数一样直接调用
语法
书写格式:[capture-list] (parameters) mutable -> return-type { statement }
lambda表达式各部分说明:
1.[capture-list] : 捕捉列表 ;
该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表可以捕获外部定义的变量和函数,只要这些变量和函数在lambda表达式的上下文中是可访问的 。但对于函数调用,你可以直接在lambda表达式中调用它们,而不需要显式捕获。
2.(parameters)参数列表 :
与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
3.mutable :
默认情况下,lambda函数总是一个const函数 ,mutable可以取消其常量
性 。使用该修饰符时,参数列表不可省略(即使参数为空) 。
4.->returntype :返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导 。
5.{statement} :
函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获
到的变量。
注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
cpp
void func()
{
cout << "func()" << endl;
}
int main()
{
int a = 3, b = 4;
double rate = 2.4;
//完善的
auto add1 = [](int x, int y)->int {return x + y; };
//省略返回值类型
auto add2 = [](int a, int b){return a + b; };
//使用捕捉列表
auto add3 = [rate](int x, int y){return x + y; };
cout << add1(a, b) << endl;
cout << add2(a, b) << endl;
cout << add3(a, b) << endl;
//捕获函数
auto swap1 = [add1](int& x, int& y) {
int tmp = x;
x = y;
y = tmp;
cout << add1(x, y) << endl;
func();
};
swap1(a, b);
return 0;
}
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数 ,该函数无法直接调用 ,如果想要直接调用,可借助auto将其赋值给一个变量。不能显示声明其类型,因为我们不知道,类型是编译器在底层生成的lambda+uuid码,可以借助auto或模板参数推出
捕获列表说明
捕获this时,确保lambda表达式定义在非静态成员函数内部。
捕捉列表捕捉过来的可以理解在构造函数时传进去了, 传成了成员变量,这样就可以直接在operator()中使用
注意:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都 会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同
什么是父作用域,块作用域?
父作用域是指lambda表达式定义时所在的最外层作用域。可以是函数作用域、类作用域、全局作用域等。块作用域(block scope)是父作用域中的一部分,通常由花括号 {} 定义,例如在条件语句、循环或函数内部。块作用域内的变量只在该块内可见。
为什么不能相互赋值?
底层是仿函数 ,运行时编译器生成仿函数的类,lambda后面的字符串是uuid通用唯一识别码,每次生成重复概率很低,确保每个表达式类目不同.
不能构成重载需要两个同名函数,因为它是对象,同一个域中不能定义同名对象。
劣势在类型的控制方面,往其他地方不好传
- 测试代码:
cpp
int main()
{
int a = 0;
int b = 1;
int c = 2;
int d = 3;
//const int e = 1;
auto func = [&, a] {
//a++;表达式默认为const
b++;
c++;
d++;
//e++;const&不能修改
};
func();//通过调试观察
auto func1 = [=, &a] {
a++;
//b++;表达式默认为const
//c++;
//d++;
};
func1();//通过调试观察
return 0;
}
7.可变模板参数
C++11的新特性优点在于提供了灵活的函数模板和类模板,而C++98/03类模版和函数模版中只能含固定数量的模版参数。
基本可变参数模板:
与可变参数函数printf相比:
printf底层开辟了一个数组来存储参数,可以依次取出。语法不支持使用args[i]这样方式获取可变参数,无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点
递归函数方式展开参数包
cpp
void _showlist()
{
//结束条件的函数
cout << endl;
}
template<class T,class...Args>
void _showlist(T val, Args...args)
{
cout << val << " ";
_showlist(args...);
}
template<class...Args>
void CppPrint(Args...args)
{
_showlist(args...);
}
int main()
{
CppPrint();
CppPrint(1);
CppPrint(1, 2);
CppPrint(1, 2, 2.2);
CppPrint(1, 2, 2.2, string("xxxx"));
// ...
return 0;
}
递归调用参数包,进入含两个参数的重载函数中,每一次调用中参数包中第一个元素会被类型T接收然后打印,直到为空,调用最符合的重载函数,视为结束条件。当然这种写法可以直接处理参数包为空的情况
逗号表达式展开参数包
cpp
void CppPrint()
{//接收空参数包
cout << endl;
}
template<class T>
int printargs(T val)
{
cout << val << " ";
return 0;
}
template<class...Args>
void CppPrint(Args...args)
{
int a[] = { (printargs(args),0)... };
cout << endl;
}
int main()
{
CppPrint();
CppPrint(1);
CppPrint(1, 2);
CppPrint(1, 2, 2.2);
CppPrint(1, 2, 2.2, string("xxxx"));
// ...
return 0;
}
1.这种展开参数包的方式,不需要通过递归终止函数,printargs不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。
2.关键是逗号表达式,会按顺序执行逗号前面的表达式,先执行printargs(args),再得到逗号表达式的结果0
3.使用C++11特性通过初始化列表来初始化一个边长数组,最终会创建一个都为0的数组。但在创建数组之前会先展开参数包,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
在STL容器emplace接口中的使用
emplace接口支持在容器中直接构造新元素,支持模板参数和万能引用
因为移动构造效率很高,所以它们性能相差不大,但在自定义类型的传参中更加方便,可以传参数包进去直接构造,不需要提前定义一个对象
对于内置类型来说,emplace接口和其他插入接口相同都会触发深拷贝,在自定义类型上会有一点区别
8.新的类功能
原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载(用处不大)
- const 取地址重载(用处不大)
C++11 新增了两个:移动构造函数 和移动赋值运算符重载。
移动构造和移动赋值需注意:
1.深拷贝的类需自己实现
2.浅拷贝的类不需要实现
自己不写,自动生成的新成员函数会干什么?什么情况下会自动生成?
1.移动构造(移动重载情况完全一样):
如果你没有自己实现移动构造函数 ,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个 。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝 ,自定义类型成员 ,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造 。
注意:如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
关于其他成员函数的存在条件可以这样理解:因为要写析构一般是要深拷贝的类,条件中三个函数一般一起行动。
- 测试代码
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
/*Person(const Person& p)
:_name(p._name)
,_age(p._age)
{}*/
/*Person& operator=(const Person& p)
{
if(this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}*/
//Person(Person&& p) = default;
Person(const Person& p) = default;
/*~Person()
{}*/
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
//Person s4;
//s4 = std::move(s2);
return 0;
}
只需通过不断注释掉一写代码,观察就可以验证上述条件
- 强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。编译器会生成默认的实现,这些实现是隐式的。
- 禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
9.包装器
function包装器
也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
问题引入?为什么需要function呢
cpp
//ret = func(x);
// 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能
//是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
//为什么呢?我们继续往下看
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
//函数指针
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lambda表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
return 0;
}
count是全局变量,在整个程序运行期间都存在而不仅仅是调用期间。
1.如果类模板useF只实例化了一份,count一旦被初始化,每次调用保持上次调用结束时的值。所以每次调用时,count值++,并且地址不变。
2.若类模板实例化了三份,每个实例化版本的count是独立的,之间互不干扰,输出值都为1并且地址不同
通过验证发现useF函数模板实例化了三份。包装器可解决上述问题
还可调用对象存储到容器中vector<>,根据需求直接调用不同对象接口使用,大大提高了效率.
包装器运用
还记得之前在学习STL容器时做过的逆波兰表达式求值的题目,运包装器更简洁效。
原写法:
需要先在给定字符串数组中遍历一遍元素,是操作符取栈顶元素计算,是操作数转为整型入栈。进行计算时还需再遍历一遍操作符来匹配对应运算规则,代码冗长,效率也一般
包装器写法:
用map来存储键元素string操作符,值元素存储包装器。这里包装器可以用仿函数,但是要单独实现略显沉重,使用lambda表达式最好,更轻便灵活。
优势在对可调用对象进行包装,类型统一了,可以存在容器中
bind
std::bind函数定义在头文件functional中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来"适应"原对象的参数列表。
作用1.绑定参数 2.调换参数顺序
- 测试代码
cpp
int Sub(int a, int b)
{
return a - b;
}
double Plus(int a, int b, double rate)
{
return (a + b) * rate;
}
double PPlus(int a, double rate, int b)
{
return rate * (a + b);
}
class SubType
{
public:
static int sub(int a, int b)
{
return a - b;
}
int ssub(int a, int b, int rate)
{
return (a - b) * rate;
}
};
- 调换参数顺序:
场景1:
1.std::placeholders 是一个命名空间,它定义了一系列占位符,用于指定绑定后的函数调用时参数的位置。_N:表示绑定后的函数调用时的第 N 个参数。
2.std::bind的返回值是一个复杂的绑定器对象,其类型由编译器自动生成,具体类型取决于绑定的目标函数和参数。不建议直接使用auto,因为生成的新对象内部实现被封装,这可能会隐藏类型问题,导致编译错误或运行时问题。推荐使用function因为可以容纳任何可调用对象
3.第一个参数表示绑定函数
4.类和命名空间都是域,不展开是不会进去找的,只会在全局域中找,或者加上访问限定符,如placeholders
场景2:
将绑定参数换位,通过调试可以发现绑定参数的对应关系。bind可以对函数调用时传入参数的关系进行顺序调整,但对于函数定义的参数列表会一一对应。可以理解为bind在调用层和定义层的中间,可以改变调用层的顺序去一一对应定义层,只能改变对下的关系,严格遵顼对上的顺序
- 绑定参数:
通过固定部分参数,简化调用,使之更加灵活
- 绑定成员函数:
cpp
int main()
{
//非静态成员绑定
SubType st;
function<double(int, int)> Sub1 = bind(&SubType::ssub, &st, placeholders::_1, placeholders::_2,3);
cout << Sub1(1, 2) << endl;
function<double(int, int)> Sub2 = bind(&SubType::ssub, SubType(), placeholders::_1, placeholders::_2,3);
cout << Sub2(1, 2) << endl;
//静态成员绑定
function<int(int, int)> Sub3 = bind(&SubType::sub, placeholders::_1, placeholders::_2, 3);
cout << Sub3(1, 2) << endl;
return 0;
}
1.绑定非静态成员函数 :
第一个参数传函数对象的地址 ,第二个参数成员函数的指针或对象 (传它们不是给this指针,而是在类的operator()中通过它们去调用函数),后面为参数包
2.绑定静态成员函数 :第一个参数同样为地址,但不需要对象实例可直接通过类名去调用,后面为参数包
注意 :a.非静态成员函数取地址前要加个符号,静态都可以,建议都加上
b.function和绑定底层都是仿函数