我前面发的文章都是基于c++98的,本文主要是针对c++11新特性进行说明
1.列表初始化
1.1{}初始化
下面是98里的初始化方式
#include <iostream> using namespace std; struct kp{ int x, y; }; int main() { int a[] = { 2,3,4,5,6 }; int arr[3] = { 0 }; kp c = { 2,3 }; return 0; }
而对于11来说,可以这样初始化
#define _CRT_SECURE_NO_WARNINGS 1 #include <iostream> using namespace std; struct kp{ int x, y; }; class mm { public: mm(int a,int b){ } }; int main() { //把b初始化为1 int b{ 1 }; int arr[]{ 3,4,5 }; kp a{ 2,4 }; int* x = new int[5] {0}; //构造 mm d(3, 3); //构造+拷贝构造==直接构造 mm d1 = { 3,3 }; mm de{ 3,3 }; //构造 mm* d7 = new mm(2, 3); //构造+拷贝构造==直接构造 mm* d8 = new mm{ 2,3 }; mm* d4 = new mm[3]{ {1,2},{2,3},{4,5} }; //拷贝构造 mm* d5 = new mm[3]{d, d1, de}; //注意,这里开数组大小不能大于初始化个数,因为我没给默认构造 //而内置类型有默认构造,比如int就是0,但自定义类型如果没给默认构造,这样编译器调用就会出现问题 //注意,这样写是不行的,Date d9=(2,3); //因为()默认就是没有=的,语法不支持 return 0; }
而对于int b(3)这样的初始化,是在出现模板后就有的初始化方式,主要是为了让内置类型也能调用构造函数,跟类的构造函数相对应,在一些场景也能万金油的用(前面一些文章在模拟的时候就用了这种方式)
总结下11的{}初始化,就是不管是数组、变量、还是自定义类型、内置类型,都能用{}初始化,但是()只能对内置类型、自定义类型有效,且不能多'='
1.2std::initializer_list
1.2.1
我们先看这个:
vector<int>a({1,3,3,4,56});
跟前面的mm a={2,3,4};
是不是很像,但本质上还是有区别的,后者是构造+拷贝构造,但是前者,是利用的c++11的另一个新特性,是initializer_list,来直接构造的。因为后者使用的前提是参数的固定给的,而前者我们可以看到,里面的数据个数完全是看我们怎么输的。
我们可以看到,vector在c++11中,新增了一个构造方式。
这是c++新的一个容器,这个容器没有太多功能,主要是临时存储
auto a = { 2,3,4,5,6,7 }; initializer_list<int> a1 = { 3,4,5,6,67 };
这个容器的begin和end返回都是const对象,不支持修改,所以只能是临时存储,将数据存在常量区,然后本身容器只有2个指针,数据开头和结尾。
而vector在c++11提供了用这个容器来构造的方式,所以才能在前面那样用
vector<int>M ={2,3,4,5,6}; 这个其实也是先把整个{}作为initializer list调用构造函数,创建临时变量 然后把临时变量通过拷贝构造再拷贝给M。
1.2.2
而在stl库中,list,vector,map都是支持这个构造方式的
map<string, string>mp = { {"23","23"},{"23","23"} }; map<string, string>mp1({ {"23","23"},{"23","23"} });
注意,{"23","23"}调用了pair的带参构造函数,构造的是pair<const char*,char*>,而这里我们之前讲了,是构造+拷贝构造,而pair的拷贝构造是比较独特的
支持不同类型的pair进行拷贝,拷贝构造里面的初始化列表会直接调用first(U或者更形象的是pr.first),second(V/pr.second),也就是说调用了string的构造函数,而string有一个构造函数是支持char*或char*进行构造的。
从而实现了让pair<const char*,char*>被拷贝进了pair<const string,string>。
1.2.3
注意,这些容器的赋值操作 operator=也是支持initializer list的
比如v1={1,2,3,45,6};
2.声明
2.1auto
注意,98里面也有auto,但98的auto,只是说明这个变量是局部自动存储类型,但因为全局变量就算用auto声明,变量的生命周期还是没有变,而局部变量就算不用auto也是放在栈上分配,生命周期也是会在出了局部作用域后自动结束,所以显得没什么意义
而c++11就将auto用于自动推导类型
比如auto a=10,那a的类型就会是int,同理auto a="21",就是char*,自定义类型也是支持的
也可以用于返回值(不推荐,危险性很高,14还是17支持来着,如果遇到多重嵌套,光是确认返回值都是一个大麻烦,就算用typeid,输出也是各种容器的全名,非常难看懂),但不能作为形参
2.2decltype
这个是将变量的类型声明成表达式的类型。
首先先说下typeid,这个是用来说明变量类型的
这个简单来说,就是输出了定义
而且是逐一输出,直到最底层的定义,但是只能作为文字输出,而不能作为类型定义新的变量,比如typeid(a) b,就不可以。typeid是98就有的内容。
而c++11新增了一个decltype,用法是decltype(变量),这个是可以作为类型定义变量的
decltype(x) b。
注意,const int a和const int *a。同时调用decltype(a),得到的结果,前者是int,后者是const int *。注意,前者,a类型依旧是int,const只是修饰了a存的值不可修改。后者,const修饰的*a,那对于a来说,a的类型依旧是const int *。如果是decltype(*a)则结果是int.书上有顶层const和底层const的说法,修饰自身的顶层const,修饰指向内容是底层const,decltype去掉的是顶层const
decltype的一个运用场景,就是在上面auto回调函数多层了,这时候我们要用函数的返回值的类型创建新的变量,这时候我们就可以利用auto,或者直接把函数作为参数放入decltype里。
2.2nullptr
主要是因为c语言的NULL被宏定义成了0,0既指向了空指针又指向了常量0,会有安全问题,所以c++新增了nullptr,只代表空指针
3.移动语义和右值引用
3.1左值引用和右值引用
左值是一个表示数据的表达式,比如变量名、解引用的指针。我们可以获取他的地址、一般情况下对他赋值、左值可以出现在赋值符号的左边,右值不可以出现在赋值符合左边。定义const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
int *p=new int(2); int a=3; const int c=4; static int d=4; p,a c d 都是左值 int f1(){ int x; return x; } f1的返回值不是左值,是右值。 int& f2(){ static int x=0; return x; } f2的返回值是左值 左值引用 int* &p1=p; int&a1=a; const int &c1=c; static int &d1 =d; const修饰的左值引用可以引用左值也可以引用右值 const int &x1=10; const int &x2=a;
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回),右值可以出现在赋值符合右边,但不能出现在左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
稍微细分一下,就是纯右值:10 / a
将亡值:匿名对象(不能取地址,用完即毁),传值返回函数(传值返回时的临时对象,比如后置++,to_string(-1234))
int x=1,y=2; int f1(){ return 1; } //右值 10; x+y; f1(); //右值引用 int &&x1=10; int&&x2=x+y; int &x3=f1();
3.1.2小总结
语法上:引用都是别名,不开空间,左值引用是给左值取别名,右值引用是给右值取别名。
底层:引用都是指针实现。左值引用就是存当前左值的地址。右值引用,是把当前右值拷贝到栈上一个临时空间,存储这个临时空间的地址。
注意:
一般的左值引用不能给右值取别名,但是const左值引用可以
int &a=10;//错误的 const int &a=10;//正确
一般的右值引用不能给左值取别名,但是move之后的可以
int x=10; int &&r=move(x);
move看后面。这里先理解为返回一个x的右值,但不改变x本身左值的属性。但是平时要注意,如果对一个左值move,那么就可能会让这个左值被修改(比如swap),因此要注意使用
右值被右值引用以后,这个右值引用本身的属性是左值。如上所说,右值引用底层是指针,指向的是临时空间,这个临时空间是可以被修改的,也是有地址的。因此才会是左值。因为有这个性质,下面的移动构造移动赋值中,swap明明参数接受的是左值,但却能让右值引用传入。
3.1.3意义
左值引用的意义
1.传参时的拷贝问题解决了(传值传参的方式会构造一个新的形参,效率低,用引用不需要开新的)
2.解决了一部分函数返回值拷贝的问题,特指解决的全局变量或静态变量等对象左值引用返回
但没有解决形参或者说出了作用域的局部对象返回拷贝的问题(左值引用不适合这个,因为出了作用域会销毁)
右值引用的意义:
1、主要是解决了左值引用的缺陷,如上面所写。
那么右值引用怎么解决这个问题?移动构造和移动赋值,且c++11以后,所有的容器都增加了移动构造和移动赋值
先看下面的string模拟,不需要全看,看拷贝构造的部分即可
#pragma once #include<assert.h> namespace test { class string { public: //拷贝构造 string(const string& s) { _str = new char[s._capacity + 1]; strcpy(_str, s._str); _size = s._size; _capacity = s._capacity; } //移动构造 string(string&& s) { swap(s); } private: //给定缺省值,以免上面构造函数遇到一些编译器不初始化内置类型的问题 //存储字符串 char* _str = nullptr; //当前有效字符个数 size_t _size = 0; //可容纳的有效字符最大个数 size_t _capacity = 0; //是一个静态全局变量,或者说是针对string类的全局变量 //平时用来确认返回值的 const static size_t npos = -1; }; }
先看移动构造的问题
1、一般情况下,我们是调用的是const string &s的拷贝构造,因为const修饰的左值引用可以引用左值也可以引用右值。函数返回局部对象的过程:拷贝构造生成临时对象,临时对象再拷贝构造给外面接受的对象。编译器优化后是直接把局部对象拷贝构造给外面接受的对象。
2、但是在特殊情况,比如,我们的函数返回了一个局部对象,这时候,编译器先让str调用普通的拷贝构造生成临时对象,因为临时变量具有常量性,也可以是右值,所以再让接受对象调用移动构造,让临时对象通过右值引用,传入移动构造,而移动构造里面,直接利用swap,将临时对象里存的内容全部跟接受对象里的内容进行交换,这样就算移动构造完成后,临时对象销毁,此时临时对象里存的是同类型对象的默认内容,销毁了也无所谓,而外面的接受对象,此时存的是本该在临时对象里的内容。
而这是编译器没有优化时的结果。
我们仔细思索上面的过程,第一次的返回值拷贝构造生成临时对象,这个过程是不是也可以用移动构造呢?可以的,我们只需要手动把返回值改成了move(局部返回值对象),注意,库里的很多函数也有移动构造的优化,但是源码只能加不能改,所以编译器在这里的第一个优化就是让局部返回对象强行move。这个时候过程就变成了移动构造->移动构造。但是还是很冗余,于是干脆让外面接受对象调用移动构造,将局部返回对象直接用右值引入的方式传入该移动构造。这样就将过程优化到了一次移动构造。
注意,对于外面接受的对象来说,因为移动构造也是构造,肯定会走初始化列表,而我们知道,一个对象在初始化列表会进行初始化(进行定义,并且调用成员相应的默认构造),这就保证了在函数内的局部对象(用move强行变右值)以参数形式传入移动构造后,进行swap交换时,外面的接受对象本身也是正常的一个对象(保证了在swap之后局部对象在出了函数之后执行析构函数是正确的,否则的话,假如析构函数要销毁一段空间,但是swap之后,局部对象存的是一段地址随机值,那析构函数也是会报错的)
注意,移动构造适用深拷贝,因为深拷贝的对象,内部肯定有malloc或者new出来的,空间开辟在堆上的,亦或者是移动构造可以有效减少这种对象的构造次数。对于浅拷贝,因为其成员都是直接开辟好,且会随着对象的销毁而销毁,最典型的就是内置类型,比如一个int变量局部变量,就算是用swap交换了,这个int存储的空间还是会随着函数的结束而销毁。
而对于堆上的空间,比如vector、string,本身都是有new或者malloc在堆上开辟一个数组空间,对象本身只是存了指向这些空间的指针,所以这种对象在进行swap之后,就算原先的对象执行析构函数,那也是销毁被替换进去的空间(基本都是null),而这片有数据的空间,他的地址会被存到外面的对象的某个成员。
所以对于浅拷贝,就算swap了,也没用,所以最终还是要进行传值拷贝,但是内置类型大多占空间不大,拷贝的消耗也小。
然后是移动赋值的问题
比如
假设xa是我们自己模拟的string的命名空间,没有移动构造和移动赋值的版本 xa::string ret; ret=xa::to_string(5466); 那么这个时候,就会出现3次构造。首先是第一次ret定义之后的默认构造。 然后第二次是to_string()将局部对象深拷贝到临时对象,临时对象再通过赋值重载的方式赋值给ret。 在赋值的过程中,因为赋值重载的写法有很多种,有传值作为参数, 然后直接swap,又或者自行开辟空间,然后复制值,但都是要进行一次深拷贝,也就是第三次。 注意,依靠编译器自身的优化 (局部对象拷贝构造给临时变量,临时变量拷贝构造给接受对象 优化成局部对象拷贝构造给接受对象) 是不行的,因为第三次构造其实是执行的赋值重载内的拷贝构造,而编译器没有对这种情况优化。 所以在没有移动构造和移动赋值的情况下,会有3次构造。
注意,这个流程中,临时对象其实是一个将亡值,如果我们加入了移动构造和移动赋值
#pragma once #include<assert.h> namespace test { class string { public: //拷贝构造 string(const string& s) { _str = new char[s._capacity + 1]; strcpy(_str, s._str); _size = s._size; _capacity = s._capacity; } //移动构造 string(string&& s) { swap(s); } //赋值拷贝 string& operator=(string s) { swap(s); return *this; } //移动赋值 string& operator=(string&& s) { swap(s); return *this; } private: //给定缺省值,以免上面构造函数遇到一些编译器不初始化内置类型的问题 //存储字符串 char* _str = nullptr; //当前有效字符个数 size_t _size = 0; //可容纳的有效字符最大个数 size_t _capacity = 0; //是一个静态全局变量,或者说是针对string类的全局变量 //平时用来确认返回值的 const static size_t npos = -1; }; }
这个时候,同样的流程,3次构造,除了默认构造无法避免外。剩下的两次深拷贝,我们可以变成移动拷贝,首先是函数用移动构造拷贝给临时对象,然后临时对象以右值的形式传入移动赋值中,进行swap之后,临时对象会自动执行析构销毁空间,具体的可以类比上面的移动构造。这样就把2次深拷贝变成移动拷贝了。
2、在容器插入等场景
比如list的push_back()、Insert(),在c++11,都是加入了传右值的版本,这就让我们在一些场景插入效率很高,比如
list<string>lt; 隐式类型转换,生成临时对象,临时对象是右值, 而list内部每个节点都要生成一个string, 这个string会调用移动构造,将上面的临时对象以右值形式传进来 lt.push_back("wddw"); 匿名对象也是右值,也是类似 string a1(string("wxxx"));
3、完美转发
对于模板,c++11针对引用做了新的机制
也就是说,这里的T&&,整体就是一个参数,T和&&都会进行推演,T可能推演得到int、long long等等,&&可以推演得到&或者&&。
这个特性,可以成为引用折叠或者万能引用。给左值就是左值引用,给右值就是右值引用。
还要值得注意的是,如果是const 左值或者const 右值,也是可以传入这个模板生成对应函数的,这个特性就是为了让模板更加的标准化,否则光为了这个左值右值,const左值,const右值,就得写好几个模板。
而完美转发就是forward,跟move相比,move是不管参数是左值还是右值,都转成右值。
而forward就是做了一层识别,如果参数是左值或左值引用、右值,不变;如果是右值引用,则属性变成右值。
完美转发,针对的是,我们不需要全都move的场景,需要的是根据传参的参数是左值还是右值来确定属性。如:
这种场景,因为右值引用的属性是左值,所以一直弹出左值引用或const左值引用,这时候move没有用,只是都变成右值罢了。
4.新的类功能
在c++11以前,类的默认成员函数是6个,而在c++11出来后,新增了2个成员函数,移动构造函数,移动赋值运算符重载。
1、移动构造函数
如果我们自己没有实现移动构造且析构、拷贝、拷贝赋值重载都没有实现,那么编译器会自动生成移动构造函数,针对内置类型成员,进行浅拷贝(逐字节拷贝),对于自定义类型的成员,看这个成员是否实现了移动构造,有则调用这个成员自身的移动构造,没有就调用这个成员的拷贝构造(利用const T&x可以接受右值也可以接受左值的特性)。
2、移动赋值函数
如果我们自己没有实现移动赋值重载且析构、拷贝、拷贝赋值重载都没有实现,那么编译器会自动生成默认移动赋值函数,针对内置类型成员,进行浅拷贝(逐字节拷贝),对于自定义类型的成员,看这个成员是否实现了移动赋值,有则调用这个成员自身的移动赋值,没有就调用这个成员的拷贝赋值
3、构造、赋值、拷贝
如果我们自己写了移动构造或移动赋值,编译器就不会生成默认拷贝构造和默认拷贝赋值
4、强制生成默认函数 default
如果我们要强制生成某个默认函数,比如我们自己写了拷贝构造函数,但这个时候,我们不想自己写移动构造了(前提是,成员是内置类型或者自定义类型(且是具备拷贝构造或移动构造))
class A { A() {} A(const A& s) { //..... } A(A&& s) = default; private: string s; int a; };
5、禁止生成默认函数 delete
c++98针对不想让某个类进行拷贝的操作,是通过只声明不定义,且把声明放在了私有里。但是也防不住类里面自身进行拷贝,或者友元类。
class A { A() {} A(const A& s) { //..... } A(A&& s) = delete; private: string s; int a; };
6、缺省值、final、override。第一个可看我类与对象的文章,final和override可看我多态文章。
5.可变参数模板
template <class ...Args> void ShowList(Args... args) {} Args是模板参数包,里面的参数是类型,名字随意取,主要是前面加... args这个参数包,里面的参数是函数的形参 Args... args 是把Args这个参数包里的类型参数匹配到args的每个参数形参上 参数包里面的参数个数不限,全看你传多少
注意,c语言典型的可变参数,就是printf,根据格式,后面可以有多个参数,但这个是运行时解析,而c++的可变参数模板,模板,说明了必须要在编译时就完成类型的推演等工作,所以不能用数组的方式访问args。
可变参数,问题在于,怎么取参数
sizeof...(args)可以返回args这个参数包里有多少个数据
如果想知道每个参数的值和类型等,可以采取编译时递归的方式
,除了编译时递归,也可以这么用。
创建类对象的时候也可以利用参数包。
对了,形参参数包那还可以加个&&,如果有需求的话。
然后是使用场景,可变参数,在c++中最典型的就是容器是emplace_back()
我之前关于容器的文章都没有关于如何使用和如何模拟这个接口,就是因为这个接口涉及到了可变参数模板,内部是比较复杂的,就使用的角度来说,我们需要知道的是,比如下面的场景
queue<string>mp1; mp1.push("swdw"); mp1.emplace("wda"); //push是先构造临时对象,然后用移动构造 //emplace是直接构造,具体原理可以自行去搜索, //主要是利用可变参数模板和编译时的一些操作,节省了运行时的时间 //单参数,效率上其实没有太大区别,但是对于后面的pair等复合类型或者一些特殊情况 //效率上会很不错 string s = "dwad"; mp1.push(s); mp1.emplace(s); //没有区别,都是构造+拷贝构造(如果传右值就是移动构造) queue<pair<string, string>>mp2; mp2.push({ "wda", "wdawd" }); mp2.emplace("wda", "wdawd"); //push是先构造临时对象,然后用移动构造 //emplace是直接构造,具体原理可以自行去搜索。 //这里参数开始多了,push一共2个构造加2个移动构造,emplace就2个构造。 string s1 = "wda", s2 = "wdada"; mp2.push({ s1, s2 }); mp2.emplace(s1, s2); //没有区别,都是2个构造+2个拷贝构造(如果传右值就是移动构造)
然后要注意的是,push可以{s1,s2},但emplace不行,因为push是确定了类型的,不是函数模板,多参数会自动进行隐式类型转换,未优化就是多参数隐式类型转换进行构造,然后拷贝构造,编译器优化之后,直接构造。
emplace同样的格式{s1,s2},编译器会考虑列表格式化{},或者隐式类型转换,但是emplace是函数模板,在编译时是不知道传多少参数的,所以隐式类型转换不确定转换成什么,而列表格式化是可能容器不支持列表格式化构造节点。所以emplace不支持这样的格式。
emplace针对的是浅拷贝的类,如果直接写插入对象的初始化参数(在这里有名对象或者匿名对象,都一样,没有提升),效率提升比较显著,因为浅拷贝的类,不执行移动构造,只有拷贝构造,这样就节省一个拷贝构造,只要一次构造。对深拷贝的类,也有一定作用(在算法竞赛中比较有用吧,尤其是限制了时间、内存后,比如最小生成树、最短路在采取迪杰斯特拉等算法,运用到了queue的push、emplace时,emplace还是比较有优势的),节省了一次移动构造,只要一次构造,但移动构造消耗不大,所以提升不显著。
总结就是浅拷贝的类,节省一次拷贝构造,深拷贝的类节省一次移动构造,但不管怎么样,终归有提升效率的,所以还是推荐使用emplace。
具体的实现,以list为例(这个版本,我没加入移动构造和移动赋值,只加入了emplace,更加完整的应该是加入移动构造和移动赋值)
#pragma once #include<assert.h> namespace manba { template<class T> struct ListNode { ListNode<T>* _next; ListNode<T>* _prev; T _data; ListNode(const T& x = T()) :_next(nullptr) , _prev(nullptr) , _data(x) {} ListNode(T&& x) :_next(nullptr) , _prev(nullptr) , _data(forward<T>(x)) {} template<class ...Args> ListNode(Args && ...args) : _next(nullptr) , _prev(nullptr) , _data(forward<Args>(args)...) {} }; template<class T, class Ref, class Ptr> struct _list_iterator { typedef ListNode<T> Node; typedef _list_iterator<T, Ref, Ptr> self; Node* _node; _list_iterator(Node* node) :_node(node) {} //不等于 bool operator !=(const self& s) { return _node != s._node; } //等于 bool operator==(const self& s) { return _node == s._node; } //解引用,利用ref,实现const迭代器和普通迭代器 Ref operator*() { return _node->_data; } Ptr operator->() { return &_node->_data; } }; template<class T> class List { typedef ListNode<T> Node; public: typedef _list_iterator<T, T&, T*> iterator; typedef _list_iterator<T, const T&, const T*> const_iterator; //空链表初始化 void empty_init() { _head = new Node; _head->_next = _head; _head->_prev = _head; } //默认构造函数 List() { empty_init(); } //拷贝构造函数 List(List<T>& cp) { empty_init(); for (const auto& e : cp) { push_back(e); } } void swap(List<T>& t) { std::swap(_head, t._head); } //返回第一个有效节点的迭代器 iterator begin() { return _head->_next; } iterator end() { return _head; } template<class ...Args> void emplace_back(Args && ...args) { emplace(end(), forward<Args>(args)...); } //在指定位置前面插入 //vector 迭代器在insert会失效,list不会 iterator insert(iterator pos, const T& x) { Node* cur = pos._node; Node* prev = cur->_prev; Node* newnode = new Node(x); prev->_next = newnode; newnode->_prev = prev; newnode->_next = cur; cur->_prev = newnode; return iterator(newnode); } template<class ...Args> iterator emplace(iterator pos, Args && ...args) { Node* cur = pos._node; Node* prev = cur->_prev; Node* newnode = new Node(forward<Args>(args)...); prev->_next = newnode; newnode->_prev = prev; newnode->_next = cur; cur->_prev = newnode; return iterator(newnode); } iterator erase(iterator pos) { assert(pos != end()); Node* cur = pos._node; Node* prev = cur->_prev; Node* next = cur->_next; prev->_next = next; next->_prev = prev; delete cur; return next; } void clear() { iterator it = begin(); while (it != end()) { it = erase(it); } } //析构函数 ~List() { clear(); delete _head; _head = nullptr; } private: Node* _head; }; }
template <class T> void PrintArg(T t) { cout << t << " "; } //展开函数 template <class ...Args> void ShowList(Args... args) { int arr[] = { (PrintArg(args), 0)... }; cout << endl; } int main() { ShowList(1); ShowList(1, 'A'); ShowList(1, 'A', std::string("sort")); return 0; } 这里主要是2个知识点,一个是c++11的列表初始化,还有逗号表达式。 上面的过程,对于每个showlist,其实都是在创建数组的过程中, 将参数表的每个数据,都以(PrintArg(数据的类型), 0)放入{}中。 那么相应的,就是数组的每个元素的值都是0,并且在创建这个元素的时候,根据逗号表达式会先执行前面的表达式(这里就是PrintArg函数),然后再把该元素的值赋为0。 参数包有多少个参数,PrintArg函数就会调用多少次,给数组增加多少个值为0的元素
6.lambda表达式
在c++98,对于自定义类型对象的排序,依靠自定义函数cmp、重载运算符、仿函数来实现排序的规则,但3者写起来确实麻烦,如果排序的规则很复杂且只需一种排序的规则,还好,但是如果排序规则很简单却需要排序多次,每次排序的规则都不一样,那这样要写的内容就很多了,而且代码量会很大,所以才有了lambda,就没必要单独写个函数、仿函数又或者重载运算符。
而c++11也是学了点别的语言的,python中的lambda就被学了。
语法
[capture-list] (parameters) mutable ->return-type {statement}
[capture-list]是捕捉列表,编译器依靠这个来确认是否是lambda函数。可以捕捉代码的上下文中的变量,跟参数很像,但是又有不同。参数的话,可以改,而捕捉过来的变量默认是不能改变的,而且如果是传值捕捉的话本质上是拷贝下来的(必写)
(parameters) 是参数列表,与普通函数的参数列表一致,如果不需要传参,则连()也可以省略。(可省略)
mutable:lambda默认是个const函数,而mutable可以取消常量性,捕获的变量也可以修改了,但注意就算修改了,如果是传值捕捉的话,也不影响原来的变量,只影响这个本质是拷贝下来的变量。 (一般不用写)
->return-type 返回类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时可省略,如果返回值明确,也可省略,交给编译器自动推导。(一般不用写)
{statement} 函数体,可以使用参数列表的参数,也可以用捕获到的变量。(必写)
使用
auto remove=[](int a, int b) ->int {return a - b; }; cout << remove(3, 4) << endl; //输出-1 auto remove = [](int& a, int& b) { int t = a; a = b; b = t; cout << a << endl; cout << b << endl; }; int x = 6, y = 7; remove(x,y); //输出7 6 auto remove = []{return 4; }; cout << remove() << endl; //4
class Da { public: Da(int a1=1,int a2=2,int a3=3) :b1(a1) ,b2(a2) ,b3(a3) {} void print() { cout << "b1:" << b1 << " b2:" << b2 << " b3:" <<b3<< endl; } int b1, b2, b3; }; int main() { Da x1(3, 4, 5); Da x2(7, 6, 2); Da x3(3, 1, 2); Da x[] = {x1,x2,x3}; sort(x, x + 3, [](Da& c1, Da& c2) { if (c1.b1 == c2.b1) { return c1.b2 < c2.b2; } else { return c1.b3 > c1.b3; } }); x[0].print(); x[1].print(); x[2].print(); return 0; } b1:3 b2:1 b3:2 b1:3 b2:4 b3:5 b1:7 b2:6 b3:2
传值捕获 int x = 1, y = 2; auto ret = [x, y]()mutable { int tmp = x; cin >> x; y = tmp; }; ret(); cout << x << " " << y; //1 2 传引用捕获 int x = 1, y = 2; //注意,这里不是传地址,是传引用,这是c++11额外的规定 auto ret = [&x, &y](){ int tmp = x; cin >> x; y = tmp; }; ret(); cout << x << " " << y; //输入5 //5 1
[var]是传值捕获 [=]用传值的方式,捕获父作用域所有的变量(包括this) [&var]是传引用捕获 [&]用传引用的方式,捕获父作用域所有的变量(包括this) [this]是传值的方式捕获当前的this指针。 父作用域就是指包含lambda语句的语句块 不能重复捕获,比如已经[=]了,不能[=,a] 注意,必须是在块作用域下的lambda,如果是写在全局,那[]必须为空。 lambda不能互相赋值。 比如下面的rea和reb rea=reb是错误的,不能赋值 但是auto rec(rea)确实可以的,可以拿来拷贝构造 也可以对空的函数指针赋值 p=rea; p();
int x = 1, y = 2; auto ret = [&](){ int tmp = x; x = y; y = tmp; }; auto rea = [=] { cout << x << " " << y << endl; }; auto reb = [&, x] { y = 4; //x=3 是不行的 cout << x << " " << y << endl; }; ret(); rea(); //1 2 reb(); //1 4 注意,reb的写法,是混合捕捉,x是以传值的形式捕获的
本质
我们知道函数对象,即仿函数。类通过仿函数的方式使得可以用对象()的方式,来调用相应的函数
class x1 { public: x1(double x) :x2(x) {} double operator()(double a1, int a2) { return a1 * x2 * a2; } private: double x2; }; int main() { // 函数对象 double s1 = 0.49; x1 s2(s1); s2(10000, 2); // lamber auto s3 = [=](double a1, int a2)->double {return a1 * s1 * a2; }; s3(10000, 2); return 0; }
在底层,lambda就是按照函数对象的方式进行的,声明lambda的时候,lambda根据uuid(不用管,只用知道,可以让值不重复)生成不同的类名,然后生成相应的构造函数(可以有参数的那种,参数就是捕获列表里的内容,将捕获列表的内容作为成员变量),最后生成operator()函数,而这个operator()的函数体就是lambda的函数体内容,参数列表就是lambda的参数列表,返回值就是lambda的返回值。
这也是为什么不能互相赋值,因为本质上不同的lambda函数,就是不同的类,不同的类默认是不能互相赋值的。
本质上lambda表达式是一个匿名类,而这个类里有仿函数,而上面的s3是这个类的实例化对象,所以用s3()
然后就是一个小技巧,我们知道priority_queue是个优先队列的库容器,在构造对象的时候,第3个参数,是传的类型,而不是函数(sort的第3个参数,是函数,所以是对象()),这里的是类型,而s3是对象,不是类型,类型我们上面也说了根据uuid的随机,所以这个时候可以传decltype(s3)来当做第3个参数。
但光是这样还不够,因为lambda生成的类,是没有默认构造的,decltype(s3) x2; 是会报错的。所以必须priority_queue<int,vector<int>,decltype(s3)> x2(s3)
因为lambda生成的类禁止了默认构造,所以上面的Compare()即比较类(lambda)的默认构造 是不能生成的。
但是lambda生成的类是可以引用的,所以我们把s3以引用的形式传进去。
7.包装器
我们知道,类似函数的调用,有3种,函数指针,仿函数,lambda。
如果我们把这3个(参数列表相同)传入同一个函数模板,那么会生成3份函数。
而c++11又增加了function这个包装器(在functional头文件)
function是个类模板
function
格式
// 类模板原型如下 template <class T> function; // undefined template <class Ret, class... Args> class function<Ret(Args...)>; 模板参数说明: Ret: 被调用函数的返回类型 Args…:被调用函数的形参
#include <functional> int f(int a, int b) { return a + b; } struct Functor { public: int operator() (int a, int b) { return a + b; } }; class Plus { public: static int plusi(int a, int b) { return a + b; } double plusd(double a, double b) { return a + b; } }; int main() { // 函数名(函数指针) std::function<int(int, int)> func1 = f; cout << func1(1, 2) << endl; // 函数对象 std::function<int(int, int)> func2 = Functor(); cout << func2(1, 2) << endl; // lamber表达式 std::function<int(int, int)> func3 = [](const int a, const int b) {return a + b; }; cout << func3(1, 2) << endl; // 类的成员函数 std::function<int(int, int)> func4 = Plus::plusi;//&可加可不加,下面必须加 cout << func4(1, 2) << endl; std::function<double(Plus, double, double)> func5 = &Plus::plusd; cout << func5(Plus(), 1.1, 2.2) << endl; //注意还可以这样写 Plus s1; std::function<double(Plus*, double, double)> func6 = &Plus::plusd; cout << func6(&s1, 1.1, 2.2) << endl; //总结下,静态成员函数和普通函数、仿函数、lambda函数都不用传对象指针或对象 //只有成员函数,需要传对象指针或者对象来调用相应的成员函数。 return 0; }
经过function的包装,这3个(函数指针,仿函数,lambda)都被包装在同一个类型function(int(int,int))上,这样就算传入了函数模板中,也只会生成一份函数
使用
下面是98的写法
class Solution { public: int evalRPN(vector<string>& tokens) { stack<int>s; for(auto &e:tokens) { if(e=="+"||e=="-"||e=="*"||e=="/") { int right=s.top(); s.pop(); int left=s.top(); s.pop(); switch(e[0]) { case '+': s.push(left+right); break; case '-': s.push(left-right); break; case '*': s.push(left*right); break; case '/': if(right!=0) s.push(left/right); break; } } else { s.push(stoi(e)); } } return s.top(); } };
11的写法
class Solution { public: int evalRPN(vector<string>& tokens) { stack<int> st; map<string, function<int(int, int)>> opFuncMap = { { "+", [](int i, int j){return i + j; } }, { "-", [](int i, int j){return i - j; } }, { "*", [](int i, int j){return i * j; } }, { "/", [](int i, int j){return i / j; } } }; for(auto& str : tokens) { if(opFuncMap.count(str))//找到了操作符 { int right = st.top(); st.pop(); int left = st.top(); st.pop(); st.push(opFuncMap[str](left, right)); } else//找到了操作数 { st.push(stoi(str)); } } return st.top(); } };
就算不止这4个运算符,我们也可以在map里增加相应的映射来完成,而且不一定是lambda的形式,传函数指针,仿函数,都可以,因此更加的方便和简洁。
而函数指针(类型写起来很复杂),仿函数(不同仿函数类型不同),lambda(语法层面没有类型),因此function可以统一3个的类型。
bind
bind是对可调用对象进行调整,调整顺序、个数,是一个函数模板
// 原型如下: template <class Fn, class... Args> /* unspecified */ bind (Fn&& fn, Args&&... args); // with return type (2) template <class Ret, class Fn, class... Args> /* unspecified */ bind (Fn&& fn, Args&&... args);
// 使用举例 #include <functional> int Plus(int a, int b) { return a + b; } class Sub { public: int sub(int a, int b) { return a - b; } }; int main() { //表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定 std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1, placeholders::_2); //auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2); //func2的类型为 function<void(int, int, int)> 与func1类型一样 //表示绑定函数 plus 的第一,二为: 1, 2 auto func2 = std::bind(Plus, 1, 2); cout << func1(1, 2) << endl; cout << func2() << endl; //所以上面2个的输出结果是相同的 Sub s; // 绑定成员函数 std::function<int(int, int)> func3 = std::bind(&Sub::sub, s, placeholders::_1, placeholders::_2); // 参数调换顺序 std::function<int(int, int)> func4 = bind(Plus,placeholders::_2, placeholders::_1); cout << func4(1, 2) << endl; //绑定参数和参数调整是可以一起写的 Sub s; std::function<int(int, int)> func3 = std::bind(&Sub::sub, s, placeholders::_2, placeholders::_1); cout<<func3(1, 2); Sub s; std::function<int(int, int)> func7 = std::bind(&Sub::sub, s, placeholders::_1, 4); cout<<func7(1); return 0; }
杂项
.1范围for
具体可以看我之前“c++入门”的文章再结合我stl容器的文章即可
2.2智能指针
我后面会专门出一篇文章的
2.3stl变化
新增了一些容器,unordered_map、unordered_set、array、forward_list。
前两者我前面专门有文章写的。后两者,forward_list是单链表,之前的list是双向链表
forward_list是单向迭代器,只支持++,前面一些容器支持++、--,+=,
最后的array,就是一个静态数组,因为容易栈溢出,功能完全被vector覆盖,没有什么优势,这里不多说了
stl除了新的容器,对旧的容器,也新增了一些内容,像是cbegin,shirk_of_fit()、移动构造和移动赋值,对insert/push_back/emplace 加入了右值版本,还有新增了个initializer list的构造方式
8.补充
还是将范围for转化为迭代器来访问元素的。但容器适配器不一定有迭代器,所以不一定能用范围for。
数组可以直接用范围for。auto在98只能声明自动类型变量。c++11中,去掉了声明自动类型的功能,而是只用于类型推导。 auto仅仅只是占位符,编译阶段编译器根据初始化表达式推演出实际类型之后会替换auto auto不能推演函数类型,因为函数在编译时,还没有传递参数,因此在编译时无法推演出形参的实际类型
自定义类型需要提供迭代器才能用范围for
C++98中{}只能初始化数组,C++11中支持列表初始化,才可以初始化容器
使用{}初始化时,加不加都一样。列表初始化在初始化时,如果出现类型截断,是会报警告或者错误的
自定义类型可以支持多个对象初始化,只需要增加initializer_list类型的构造函数即可
final修饰类时,表示该类不能被继承,修饰派生类的虚函数时,表示该虚函数不能被子类继承
且修饰成员函数时只能修饰派生类的虚函数override的作用时让编译器帮助用户检测是否派生类是否对基类总的某个虚函数进行重写,如果重写成功,编译通过,否则,编译失败,因此其作用发生在编译时。
override只能修饰子类的虚函数。delete是直接删除某个函数,不只是默认成员函数,普通的成员函数也是可以删除的,静态函数也是可以删除的。
右值引用在move(左值)的情况下,可以右值引用接受左值。