1.列表初始化
1.1传统{ }初始化
c++98的{ }初始化主要是用于数组,以及结构体
1.2c++11{ }初始化
1.让内置类型和自定义类型都可以用{ }实现多个数据初始化,而自定义类型的实现原理是类型转换(没优化的版本是先构造临时对象,然后拷贝构造)
2.我们在使用{ }时也可以省略掉=直接用{ }
场景:主要用于传递多参数进行初始化
1.3initializer_list
initiallizer_list是一个库中实现的类,底层是由一个指向常量数组的指针(包含头指针和尾指针)和数据个数封装的类
**使用场景:**主要用于写模板类等底层场景的任意数量数据的构造
cppvector(initializer_list<T> l) { for (auto e : l) { push_back(e) } }
这里就以vector的任意数量数据的构造为例,通过initiallizer_list我们就实现了任意数量的数据的构造
2.右值引用与移动语义
2.1左值与右值的概念
左值: 是变量表达式,一般有持久状态,可以取地址
常见左值举例:
cpp// 左值:可以取地址 int* p = new int(0);//指针 int b = 1;//整形变量 const int c = b;//const整形变量 string s("111111");//字符串变量
右值: 一般是字面值常量,临时对象,匿名对象,不能取地址,不能放在赋值符号左边。
常见右值举例:
cpp//右值不能取地址 double x = 1.1, y = 2.2; //下面为常见右值 10;//字面值常量 x + y;//临时变量 fmin(x, y);//临时对象 string("11111");//匿名对象
2.2左值引用与右值引用
引用就是给变量取别名,右值引用也是一样的。只不过左值引用引用的是左值,右值引用引用的是有右值。
左值引用:
(1)引用左值:&
(2)引用右值:const &
cpp//左值引用 //引用左值 int a = 0; int& b = a; //引用右值 const int& c = 10;
右值引用:
(1)引用右值:&&
(2)引用左值:将左值move为右值类型再用&&
cpp//右值引用 //引用右值 int&& d = 10; //引用左值 int&& e = move(a);
move的底层就是强制类型转换,可看成将左值move强制类型转换为右值,然后用右值引用。
!!!:变量表达式都是左值属性,所以绑定右值的右值引用变量也是左值属性
2.3引用延长变量生命周期
我们知道,临时变量,匿名对象,临时对象都是生命周期为当前行的数据。而此时使用右值引用去绑定该数据后,他们的生命周期就被延长到引用的生命周期了
cpp10;//生命周期为当前行 int&& f = 10;//生命周期为f的生命周期
2.4左值右值参数匹配
在c++中我们的函数参数匹配通常是遵守匹配度优先的原则,这里的右值引用参数,左值引用参数匹配也是一样,优先匹配更匹配的
接下来我们实现了左值引用,const左值引用,右值引用三种参数匹配函数,分别测试三种类型的匹配结果
这里就是按照原本的数据类型进行匹配
接下来我们将右值引用注释掉,再次查看参数匹配结果
我们发现此时右值被const左值引用绑定了,这说明不能完全匹配的时候才会考虑其他的类型匹配
2.5左值引用使用场景
左值引用使用场景:
1.通过传引用,减少拷贝
2.可以通过引用的改变来影响实参
左值引用没有解决的场景:
对于返回的局部变量,返回左值引用也没用,因为局部变量出函数就已经被销毁了,那右值引用有没有用?
其实也没有用,因为只要是引用就是取别名,他引用的对象都被销毁了,返回的就是一个悬空引用。
2.5.1移动构造与移动赋值
我们解决左值引用不足的方法就是使用移动语义。
具体来说就是多写一个移动构造和移动赋值,通过右值引用作为参数去接收**生命周期快结束(不一定本身是右值)**的数据,然后延长他们的生命周期到移动函数结束时,通过直接swap的方式将右值数据的内容接收下来。
移动构造:
对于右值,匹配时直接匹配,对于左值,匹配前先用move将左值类型转换为右值,然后再匹配
cpp// 移动构造 string(string&& s) { cout <<"移动构造" << endl; swap(s); }
添加了移动构造之后,对于效率的提升是很明显的,既没有多余空间申请,也没有时间上多余的消耗
移动赋值:
cpp// 移动赋值 string& operator=(string&& s) { cout << "移动赋值" << endl; swap(s); return *this; }
移动赋值也是一样的,直接将资源交换即可
2.5.2解决传值返回多余拷贝问题
场景1:右值对象构造,只有拷⻉构造,没有移动构造的场景
图示:
不优化情况下进行两次拷贝构造
优化情况进行一次拷贝构造,这里直接越过了临时对象的构建
场景2:右值对象构造,有拷⻉构造,也有移动构造的场景
图示:
不优化情况下进行两次移动构造,因为str是生命周期即将结束的变量,所以使用移动语义可以有效提高效率优化1情况下进行一次移动构造,这里也是省略掉临时对象
优化2情况下会将str的构造,str给临时对象的拷贝构造,临时对象给main中对象的拷贝构造合并为对main中对象的构造。
场景3:右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景
图示:
不优化情况下进行一次拷贝构造,一次拷贝赋值。
优化情况下进行一次拷贝赋值,优化的部分是提前构造出临时对象,然后str是临时对象的引用,最后让临时对象移动赋值给main中对象
场景4:右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景
图示:
不优化情况下进行一次移动构造,一次移动赋值。优化情况下进行一次移动赋值
总结:
在有编译器优化的情况下,没有移动构造和移动赋值也没有问题,但是如果没有编译器的优化,此时就需要我们写移动构造和移动赋值来提高效率
3.引用折叠
在模板中,我们不可避免的会存在引用的引用的情况,但是如果我们直接int& &&a;这样来定义的话,会直接报错。可以通过模板或typedef来达成目的
**引用折叠原则:**右值引用叠加右值引用为右值引用,其他情况都为左值引用
cpp//引用折叠 int n = 0; typedef int& l; typedef int&& r; l& a = n;//左值叠左值-》左值引用 l&& b = n;//左值叠右值-》左值引用 r& c = n;//右值叠左值-》左值引用 r&& d = 1;//右值叠右值-》右值引用
接下来我们看看用const左值引用和用const右值引用当函数参数有什么不同
cpp//const左值引用 template<class T> void f1(const T& a) { }
cpp//const右值引用 template<class T> void f2(const T&& b) { }
左值引用当参数的时候,传的参数无论是左值还是右值,根据引用折叠的规则,他的结果都是左值引用
右值引用当参数的时候 ,传的参数是左值,那么最终折叠完就是左值。传的参数是右值,最终折叠完就是右值,我们发现我们传的参数是什么类型,最终的结果就是什么类型的引用。所以由实参决定T类型的右值引用当参数又被称为万能引用
也就是template要直接在该方法前面,T由参数决定
我们思考一下再一个类中的const T&&属不属于万能引用?
假设我们用的是vector中的push_back方法,这个方法中的T的类型不是由实参决定的,而是由vector类决定的,所以这里不存在引用折叠,直接就是一个确定的引用类型。
4.完美转发
虽然我们前面利用引用折叠弄出了一个万能引用,但是在万能引用模板中我们就不能使用move这个方法去强制类型转换变量了,因为我们不确定变量是左值引用的还是右值引用的。
为了能够正确的匹配对应的函数,我们会使用一个叫完美转发的方法来保持变量的属性,原本是左值就保持左值,是右值就保持右值
完美转发:forward<T>(变量名),其中T表示实参的数据类型
cpptemplate<class T> void f2(const T&& b) { f1(forward<T>(b)); }
完美转发的底层是一个模板函数,对左值引用和右值引用有不同的函数体进行变量属性控制。而我们在这里这样使用相当于显式实例化函数模板,根据这个实例化出的函数的逻辑对右值move从而保持变量属性。在这里就保持了b的属性值是左值或右值。
5.可变参数模板
在c++中有一种模板是支持从0到任意个参数的,也就是可变参数模板。
可变数目参数被称为参数包,参数包又分两种:第一种是函数参数包,第二种是模板参数包
我们利用...来指出接下来的参数是一个包
函数参数中:类型名后面接的参数表示0到n个形参
模板参数中:typename...、class...后面接的参数表示0到n个类型参数。
5.1基本语法格式
cpp//可变模板参数 template<class... Args> void function(Args&&... args) { }
模板位置的写法就是class...然后接可变模板类型名,函数参数位置的写法就是类型名...接形参名。
本质上是实现了类型泛型基础上的个数泛型
**补充:**sizeof...(args)可以求可变参数的个数
5.2包扩展
cppvoid Extend() { // 参数包无参数需要扩展时,匹配这个函数 cout << "扩展结束" << endl; } template <class T, class ...Args> void Extend(T x, Args... args) { cout << x << " "; // args是N个参数的参数包 // 调⽤Extened,参数包的第⼀个参数传给x,剩下N-1个参数的参数包传给第⼆个参数包 Extend(args...); } // 编译时递归推导解析参数 template <class ...Args> void Print(Args... args) { Extend(args...); }
我们可以利用递归函数来对包进行扩展,这里的extend就是递归扩展函数。第一步扩展就是实例化出一个对应的Extend函数,该函数先将包中的第一个参数给到Extend的x参数,然后将剩下的包给到Extend的第二个包参数,如此递归进行。直到包中数据为0,我们就匹配无参数的Extend函数。
注意:
与普通递归函数不同的是,包扩展的递归是编译时进行的,而不是运行时进行的
接下来我们看看另外一个包扩展方法
cpptemplate <class T> const T& GetArg(const T& x) { cout << x << " "; return x; } template <class ...Args> void Arguments(Args... args) {} template <class ...Args> void Print(Args... args) { Arguments(GetArg(args)...); }
注意:
1.getarg必须有返回值:因为他的所有返回值又构成了一个参数包给到argument
2.由于进入一个函数之前要先处理参数,参数中有函数的话我们就先进入参数中的函数,在参数函数处理完之后我们再进入函数体
3.这里的GetArg(args)...可以看为:GetArg(x1),GetArg(x2),GetArg(x3),其中x1,x2,x3都是包中的参数
5.3emplace系列接口
在c++的库中,所有的容器都添加了emplace系列的接口,他的参数部分是由万能引用叠加可变模板参数组成的。整体而言他的效率要比普通的接口要高
接下来我们看看在什么场景下emplace系列效率要更高
cpplist<string> li; li.push_back("11111"); li.emplace_back("111111");
首先我们看看push_back的参数要求
由于push_back的模板参数是在对象实例化的时候就确定了,所以value_type的类型是string。而我们这里传的是右值,所以匹配的就是类型string&&。
可是"11111"的类型是const char*,参数匹配类型不一样,所以我们要进行类型转换,创建一个临时变量,然后再匹配参数**。此时我们就需要进行一次构造**
然后是emplace_back的参数要求
由于emplace_back的模板参数是根据传递的参数决定的,所以函数参数被实例化为
const char*&&,我们可以直接进行参数匹配,不用进行构造
综上,push_back需要传递参数时要进行一次构造,而emplace_back不需要进行构造,这里就体现出emplace_back的效率更高
注意:
1.emplace系列传递多参数不用加{},因为参数是可变模板参数,且如果我们加了{}会导致编译器无法识别参数类型,pair中的变量是什么类型是不明确的。
6.lambda
6.1表达式语法
他是一个匿名函数对象,可以定义在函数体内部。不过他没有语法层面的数据类型,我们一般用auto去充当接收类型
接下来我们写一个加法功能的lambda:
cpp//Lambda auto y = [](int a, int b)->int {return a + b; }; cout << y(1, 2) << endl;
右侧的lambda实际上就是一个对象。
\]:方括号中的就是捕捉列表,主要用于确定需要使用的lambda函数外部的参数 ():小括号中的就是函数参数列表,可省略 -\>:后面的是函数返回值类型,可省略 {}:大括号内是函数体 **简化lambda:** ```cpp //简化Lambda auto x = [] {cout << "简化lambda" << endl; }; x(); ``` **使用方式:和使用普通函数的方法一样**
6.2捕捉列表
lambda的表达式默认只能使用参数和函数体内的变量,若我们需要使用这两个位置之外的变量就需要通过捕捉列表进行捕捉
第一种捕捉方式:传值捕捉与传引用捕捉
传值捕捉传的值在lambda中不能被修改,传引用捕捉的值在lambda中可以被修改,不过需要注意的是传引用捕捉的值改变的也只是形参,而不会影响实际的参数的值
cpp//传值捕捉与传引用捕捉 int a = 1, b = 2; auto e = [a, &b] { b++; return b; };
这里的a在lambda中无法修改,b在lambda中可以修改,且由于是引用,所以实参b也被修改了
第二种捕捉方式:隐式值捕捉与隐式引用捕捉
这两种捕捉方式又叫全捕捉,因为只要用了他们就可以使用其他所有变量,需要注意的是这两种捕捉方式不能同时使用,因为这样就不知道到底是要值还是引用了
cpp//隐式值捕捉与隐式引用捕捉 int a = 1, b = 2; auto e = [&] {//隐式引用捕捉 b++; return b; }; auto f = [=] {//隐式值捕捉 return a; };
这种情况下,我们在函数中用到了哪些变量,就会捕捉哪些变量,而不是真正的全都捕捉下来
第三种捕捉方式:混合捕捉
这里我们就可以使用隐式值捕捉/隐式引用捕捉+引用捕捉/值捕捉
cpp//混合捕捉 int c = 3, d = 4; auto g = [&, c] {//隐式引用混合值捕捉 d++; return 0; }; auto g = [=, &d] {//隐式值混合引用捕捉 d++; return 0; };
注意:
1.隐式值/引用捕捉只能出现一个,不能同时出现。
2.隐式值/引用捕捉需要放在第一个位置,不能放在后面
3.隐式值后面只能接引用捕捉,同理隐式引用后面只能接值捕捉
(1)当lambda定义在函数局部域中,可以捕捉在他之前定义的局部变量,不能捕捉静态局部变量和全局变量,但是他可以直接使用这两种变量。也就是说,如果lambda定义在全局域中,捕捉列表必须为空。
(2)传值过来的变量默认有const修饰,所以不可修改,但是如果加了mutable修饰就可以把const属性去掉,然后我们就可以修改传值捕捉的变量了。
但是他实际上还是一个拷贝变量,改变的只是形参,而不是实参
6.3应用
我们思考一个场景,当我们需要用水果的某个指标进行排序,此时我们可以使用仿函数控制排序逻辑,但是这样我们需要在全局域定义一个新的仿函数,且该仿函数唯一作用也就是这个地方,此时会导致代码全局域冗余。
那么我们有没有什么其他方法控制排序逻辑,使得我们可以不在全局域上定义新的仿函数?
此时我们就可以使用lambda,将sort的仿函数传参位置写上一个lambda对象,对对应逻辑进行控制
cpp//比较场景 struct fruit { int price; string colour; fruit(int prices,string colours) :price(prices) ,colour(colours) { } }; vector<fruit> v = { {1,"red"},{2,"yellow"},{3,"blue"}}; sort(v.begin(), v.end(), [](const fruit& a, const fruit& b)->int {return a.price > b.price; } );
这里我们创建一个水果结构体,然后通过lambda实现按照水果的价格进行排序
**注意:**在调试的时候如果我们把断点打在了sort上面,会直接进入sort中,不方便查看lambda的功能,此时我们可以进入一次sort后就直接将sort的断点取消掉
6.4原理简介
lambda和范围for很像,他们实际上都是封装出来的,lambda底层就是一个仿函数的类
**捕捉列表:**实际上就是生产在仿函数中的成员变量
**参数列表:**operator()中的传参
**返回值:**operator()的返回值
**函数体:**operator()的函数体
7.新的类功能
7.1默认的移动构造和移动赋值
默认生成移动构造的前提: 没有自己实现移动构造,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个。
默认生成移动赋值的前提: 没有⾃⼰实现移动赋值重载函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个
7.2default和delete
default的作用是显式指定编译器去实现默认的特殊成员函数:一共有六个(分别是构造,拷贝构造,赋值重载,移动构造,移动赋值,析构函数)
用法:以移动构造的指定生成为例
cppPerson(Person&& p) = default;
delete的作用和default刚好相反,他是可以阻止某个默认特殊函数的生成
用法:以默认生成移动构造的阻止为例
cppPerson(Person&& p) = delete;
这里其实就相当于将默认特殊函数的函数体外的部分写在前面,然后接个=和修饰符来控制默认特殊函数的生成和阻止生成
8.包装器
8.1function(处于functionnal头文件)
在c++中有很多的可调用对象,比如:函数指针,仿函数,lambda,bind等。他们的使用是分开来使用的,而function的作用就是将他们统一起来(在返回值的参数都一样的前提下)
function包装的对象称为function的目标
这里我们看到他的模板参数中有ret(返回值),args(可变模板参数)
格式:function<返回值类型(参数)> fn[] = {}
这里的function<返回值类型(参数)>就是一个类型
接下来我们看看使用样例来理解他的优势
这里我们就是将返回值为int,参数都为两个整形的加减乘法包装在function中,从而实现根据索引调用不同的函数的目的,也就是我们把分开的加减乘三个函数统一到了function函数中。
包装可调用对象:
cpp// 包装各种可调⽤对象 function<int(int, int)> f1 = f; function<int(int, int)> f2 = Functor(); function<int(int, int)> f3 = [](int a, int b) {return a + b; };
包装静态成员函数:
cpp// 成员函数要指定类域并且前⾯加&才能获取地址 function<int(int, int)> f4 = &Plus::plusi; cout << f4(1, 1) << endl;
其实静态成员函数是不用加&的,但是普通成员函数需要,所以为了统一我们就一起都加上
包装普通成员函数:
cpp// 普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以 function<double(Plus*, double, double)> f5 = &Plus::plusd;//传指针 Plus pd; cout << f5(&pd, 1.1, 1.1) << endl; function<double(Plus&, double, double)> f6 = &Plus::plusd;//传左值引用 cout << f6(pd, 1.1, 1.1) << endl; cout << f6(pd, 1.1, 1.1) << endl; function<double(Plus&&, double, double)> f7 = &Plus::plusd;//传右值引用 cout << f7(move(pd), 1.1, 1.1) << endl; cout << f7(Plus(), 1.1, 1.1) << endl;
疑问:为什么普通成员函数的包装多了一个传递参数?
因为成员函数本身就隐含了一个指向调用该函数的对象的指针,也就是
this
指针。在调用成员函数时,编译器会自动把调用对象的地址当作第一个参数传递给成员函数。
- 使用指针调用成员函数 :借助
->
运算符来调用成员函数,其实质是将指针所指向的对象地址传递给成员函数。- 使用引用调用成员函数 :通过
.
运算符调用成员函数,编译器会把引用转换为对象的地址,然后传递给成员函数。所以本质上我们都是为了成员函数能借助我们传递的对象地址,正确调用函数
接下来我们看看function的整合优势:
cppmap<string, function<int(int, int)>> opFuncMap = { {"+", [](int x, int y){return x + y;}}, {"-", [](int x, int y){return x - y;}}, {"*", [](int x, int y){return x * y;}}, {"/", [](int x, int y){return x / y;}} };
这里我们就把加减乘除的字符串和对应的运算函数通过map的k-v特性联系起来了,而在这里function充当了调用函数类型的作用,正是有了function我们才能实现将字符串和对应函数联系起来的功能
8.2bind
bind的作用是调整可调用对象的参数个数与顺序,然后返回一个调整后的新的可调用对象
实例1:调整参数位置
cpp#include<functional> using placeholders::_1; using placeholders::_2; using placeholders::_3; int Sub(int a, int b) { return (a - b) * 10; }
bind也是包含在头文件functionnal中的,然后placeholders作用域中的_1,_2,_3等等就是占位符。
这里我们把第一个参数和第二个参数的传递顺序改变了,也就是我们传递的参数a是4,参数b是3,从而得出结果为10.
实例2:调整参数个数(绑定一些参数)
这里我们就把第一个参数a给绑定为100,从而实现调整参数个数的目标。
我们在绑定参数的时候是按照原本的参数顺序绑定的,因为绑定了参数a为100,所以只剩下一个参数需要传递,所以后面只剩下_1占位符,传递给参数b。
总结:有几个参数就有几个占位符,绑定参数是按照原本的顺序绑定的
我们是否可以将function和bind结合使用,从而简化代码?
cppfunction<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2); cout << f7(1.1, 1.1) << endl;
这里我们本来是对普通函数进行function包装,需要传递对象指针/引用,但是每次使用包装函数都要传一次就比较麻烦,所以我们直接用bind绑定对象引用,然后返回绑定后的调用对象,从而以后每次使用都不用再传递对象的引用了