文章目录
![]()
C++11的简介
C++11就是C++在继C++98之后推出的第一个添加了重大改进的版本,相比于C++98增加了很多有用的特性,但是C++11从出来开始也饱受诟病,因为C++11不仅增加了很多有用的特性也增加了很多无用的特性,这就无型之中增大了C++语言的学习难度。
相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。
列表初始化
列表初始化就是我们可以使用{}对容器以及变量进行初始化
在C++98中我们可以使用大括号对数组进行初始化
int arr[10] = {1,3,3,3,2,4,5,4,};
int brr[] = {1,3,2,4,23};
这种初始化方式也可以说是继承自C语言,因为C语言的数组就是这样子初始化的。
还有一种使用{ } 的初始化方式就是C语言中对于结构体变量的初始化
struct T1
{
char a;
int c;
};
int main()
{
struct T1 tt1 = { 'a',9 };
T1 tt2 = {'2',20};//在c++中可以这样使用
struct T1 arr[2] = { {'r',30},{'b',99} };//结构体数组初始化
T1 brr[2] = { { '3',40 }, { 'j',98 } };//T1类对象数组初始化
return 0;
}
这段代码的tt2的初始化只能在c++中使用,因为c++中将struct也看做是类,所以T1就是类名,我们可以直接用类名创建出对象。
第二组用了结构体数组和类对象数组的初始化方式进行的初始化。
但是在C++98中也只是支持了上面的这两种列表初始化方式,我们想用{ } 初始化一个vector 比如下面这段代码,都是不可以的。
vector<int> v1{1,2,3,4,5,6};
因为在C++98中vector的构造函数也根本没有支持使用列表进行初始化。所以上面代码是会报错的。
但是上面这段代码支持C++11的编译器是没有问题的。比如下面这段。
int main()
{
vector<int> v1 = { 1,2,3,4,5 };
vector<int> v2{ 1,2,3,4,5 };
map<int, int> mp1 = { {1,2},{3,4},{5,6} };
map<int, int> mp2{ {1,2},{3,4},{5,6} };
return 0;
}
C++11支持了对于自定义类型和STL的容器使用{ } 也就是列表进行初始化。
我们加不加等号都是可以的。
列表初始化的原理
了解完了列表初始化的使用我们来看一下为什么C++11的容器支持了列表初始化呢?
原理是,C++11为了支持列表初始化引入了一个新的容器,initializer_list,所有的{ }都会被转换成这个容器的对象。
我们先来看看vector在C++98和C++11中的构造函数有什么不同
这是C++98,支持了默认构造,n个val构造和迭代器区间构造,以及最后的拷贝构造。
这是C++的构造函数,比起C++11这里多了两个构造函数,一个是右值引用的构造函数(右值引用后面会讲,这里先不管)最后一个就是使用一个initializer_list对象构造一个vector。
下面我们来看看initializer的官方文档
可以看到这里,使用{ }作为初始化内容的时候,这个内容会被编译器自动推导为initializer_list的类型。因此结合上面vector支持使用initializer_list对象进行初始化可以得出我们可以使用{ } 进行初始化的原因是:列表会被自动转换成一个initializer_list的对象,然后通过这个对象初始化容器。
既然是容器那么initializer_list肯定也是支持迭代器的,我们可以使用迭代器对initializer_list的对象进行遍历。
同样的,上面的代码中我们还测试了map也是可以使用{ }进行初始化的。所以map肯定也是支持initializer_list对象进行初始化的。
但是这里需要注意的是map这里的用来初始化的initializer_list里面的元素是value_type,也就是pair<K,V>,所以上面的代码中我们使用了两层{ } 的嵌套,为了就是先用里面的一层{ } 构造出pair,然后用pair构initializer_list,最后使用initializer_list构造出来了map。
当然pair也是支持使用{ }的列表初始化啦。
自定义类型支持{ }构造
如果我们想要我们自定义的类型也能使用{ }进行初始化,或者进行赋值,那么就需要支持initializer_list的构造函数和赋值运算符重载。
initializer_list里面只有三个函数,size() , begin( ) , end( )
#include <initializer_list>
template<class T>
class Vector {
public:
Vector(initializer_list<T> l) : _capacity(l.size()), _size(0)
{
_array = new T[_capacity];
for (auto e : l)
_array[_size++] = e;
}
Vector<T>& operator=(initializer_list<T> l) {
delete[] _array;
size_t i = 0;
for (auto e : l)
_array[i++] = e;
return *this;
}
private:
T* _array;
size_t _capacity;
size_t _size;
};
类型自动推导
C++11引入了类型的自动推导用来简化声明。
auto类型推导
首先就是auto关键字,auto可以根据常量值或者是函数返回值,自动推导出变量类型比如下面这段代码
int main()
{
auto i = 10;
map<int, int> mp{ {12,23},{24,35} };
auto it = mp.begin();
//std::map<int,int>::iterator it = mp.begin();
return 0;
}
特别是带有模板的类型,如果不展开命名空间还会更加长,所以这时候使用auto就可以很简短的声明变量。
auto可以自动推导类型,但是auto不能作为函数参数
同时auto可以用来推导声明多个变量但是这多个变量必须是同样的类型。
auto不可以用来声明数组。
auto支持的范围for循环
int arr[] = {1,2,3,4,5,6};
for(auto e : arr)
{
cout<<e<<" ";
}
这就是范围for的格式,范围for可以用来遍历容器,其实范围for底层会被编译器转化成迭代器,所以我们也可以使用范围for来遍历容器。
decltype类型推导
前面说到的auto类型声明的变量必须要对其初始化,否则编译器无法推导出auto的类型
比如我们需要定义一个int b 不进行初始化,这时候auto是推导不出来的。
亦或者是定义一个数组比如,int arr[ 10 ],这些auto都是做不到的。但是decltype可以做到
int main()
{
int a = 0, b = 10;
decltype(a) ac;
decltype(a) arr[10];
return 0;
}
decltype的使用方法就是将现成的变量放进括号内,然后decltype就可以创建出与括号内的元素相同类型的对象了。
我们还可以使用decltype推导的类型作为容器的元素类型
map<int, int> mp;
auto it = mp.begin();
vector<decltype(it)> vvm;
所以decltype并不是没用的,decltype在lambda表达式中也会用到。
上述说的auto和decltype都是属于运行时类型识别(RTTI)
但是其实在C++98中也是支持RTTI的。
比如:使用typeid( ).name( )可以打印出类型的字符串,但是我们不能使用typeid推导的类型来定义对象。
dynamic_cast只能用在含有虚函数的继承体系中。
所以C++11中的RTTI实际是扩充了RTTI在代码中的使用。
STL容器变化
C++11对于容器的改变分为两个方面:
- 新增容器( array 、forward_list、unordered_map、unordered_set 等等)
- 对已有的容器增加了更方便使用和效率更高的接口,比如:initializer_list、和右值引用
新增容器
array
array实际就是一个静态数组,他的长度是固定的,支持迭代器。
array在实际中的使用价值不大,主要就是,1.支持迭代器,可以更好的兼容STL容器的使用方法。2.对于越界的检查更加严格。
我们自己定义的数组有时候越界了并不会报错,一般情况下,越界读不报错,越界写才会报错。但是在array这里因为我们访问数组的元素时候使用的是array的接口函数,所以很容易就可以在函数内部使用assert断言进行越界检查。
forward_list
forward_list就是一个单链表
forward_list支持一个单向的迭代器。
支持头插头删,不支持尾插和尾删。
同时支持了在随机位置的后面进行插入和删除,因为这样的效率会高一点。单链表如果要在某给位置之前插入或者删除元素需要找到该位置的前一个位置,时间复杂度就是O(N)了
新增方法
C++11对于const迭代器新增了cbegin( ) 和 cend( )这两个接口很少使用主要是为了规范代码
对所有容器新增了列表 初始化的构造函数
对于构造和某些插入接口提供了右值引用版本的
移动构造,移动赋值函数
final和override
C++11中针对类的继承和多态方面新增了两个关键字
final关键字
修饰类(放在类名后),则该类不可以被派生(继承)
修饰虚函数(放在函数参数列表后),则该虚函数不能被覆盖(重写)
override关键字
放在派生类的虚函数后,检查该虚函数是不是完成了对基类的重写,没完成就会报错。如果函数不是虚函数那么也会报错。
C++11控制默认成员函数
我们知道在C++的一个空类中实际并不是什么都没有的,编译器会默认生成构造函数,拷贝构造函数,赋值运算符重载,析构函数,取地址重载,const取地址重载,C++11引入右值引用后还包括了移动构造函数和移动赋值函数。但是有时候我们自己写了某个带参的函数,比如我们写了拷贝构造函数,因为拷贝构造是特殊的构造函数,这时候编译器就不会在生成默认构造函数了。
所以有时候会编译器是否生成默认函数就会脱离我们的控制,所以C++11推出了关键字default来控制是否生成默认成员函数。
default 显式生成默认成员函数
这就是上面的举例,因为写了拷贝构造所以编译器不会再生成默认构造函数。
只需要在要编译器显示生成默认成员函数的声明后加上 = default即可。
delete 显式删除默认成员函数
在设计模式中有一个单例模式,要求一个类只能生成一个对象,不允许拷贝对象。
我们在C++98中防止对象拷贝的方法有:
将拷贝构造函数和赋值运算符重载函数只声明不实现,并且要将这两个函数声明成为私有。
只声明不实现在调用这些函数的时候就会爆出链接错误,必须要实现成私有的原因是,如果这些函数是公有,那么可以在类外实现定义,不够严谨。
C++11为了解决上面的问题引入了delete可以删除默认成员函数,也就是让编译器不生成这些函数。
被删除的函数我们就不可以在引用了。
C++11引入移动构造和移动赋值的生成规则
C++11引入了两个默认成员函数,但是这两个函数编译器默认生成的条件和以前的六个不同。
规则:
1.没写移动构造,且拷贝构造,析构函数,赋值重载都没有写,编译器才会默认生成移动构造函数。
默认生成的移动构造对于内置类型实现值拷贝,对于自定义类型会调用自定义类型的移动构造,若没有移动构造就调用拷贝构造函数。
2.没写移动赋值,且拷贝构造,析构函数,赋值重载都没有写,编译器才会默认生成移动赋值函数。
右值引用和移动语义
引用的概念
引用是在C++98 中出现的,就是给一块空间或者一个变量取一个别名,这个别名和这个变量共同指向一块空间,实际上引用的底层是通过指针来实现的。
int a = 10;
int& ra = a;
C++11为了提高效率引入了右值引用。
右值和左值
左值就是变量,对象等等,左值可以放在 = 的左边也可以放在 = 的右边。左值是可以取地址的。
右值就有,字面常量,函数返回值,表达式返回值,临时对象等。右值只能放在 = 的右边,右值是不可以取地址的。
右值引用和左值引用
int main()
{
int a = 10;
int b = 20;
int& ra = a;
int& rb = b;
//int& sum = a + b;//左值引用引用右值(X)
int&& sum = a + b;
int&& rrt = 100;
return 0;
}
右值引用的形式是比左值引用多了一个&。
交叉引用
左值引用可以直接引用左值,是不能直接引用右值,const 左值引用可以引用右值。
右值引用可以直接引用右值,但不能直接引用左值,右值引用可以引用move以后的左值。如果左值move之前带有const属性,那么就需要const右值引用才可以引用。
int main()
{
int a = 10;
int b = 20;
const int c = 30;
int& ra = a;
int& rb = b;
const int& rs = a + b;//const左值引用引用右值
const int& rn = 10;
int&& sum = a + b;
int&& rrt = 100;
int&& rra = std::move(a);
int&& rrb = std::move(b);
const int&& rrc = std::move(c);
return 0;
}
move是一个放在std命名空间内的库函数
当一个右值比如函数返回值被右值引用引用了之后,会在内存中开辟一块空间来保存这个返回值。对于这个位置可以取地址,甚至可以修改里面的内容。所以,右值被取别名之后,这个别名就是一个左值(关于完美转发)
C++98传值返回的缺陷
C++11的右值引用是为了解决C++98 中左值引用的缺陷的。
左值引用的用途有:1.可以作为输出型参数。2.可以作为函数形参减少拷贝。3.可以作为函数返回值,但是引用返回的对象必须是出了作用域不销毁的。
所以这时候就有一个问题,传值返回就要进行深拷贝如果是一个自定义类型的对象拷贝数据也多,此时的效率就大大下降了。
当函数使用传值返回的时候必定是因为返回值出了函数的的作用域就会销毁,所以不能使用传值返回。
这时候右值引用的作用就出现了。我们可以增加一个移动构造函数,将即将销毁的返回值看作是右值,将亡值然后将这个对象的资源转移出去。先来看代码
class String
{
public:
String(const char* str = "")
:_str(nullptr)
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s)
: _str(new char[strlen(s._str) + 1])
{
cout << "String(const String& s)--拷贝构造" << endl;
strcpy(_str, s._str);
}
String(String&& s)
:_str(nullptr)
{
cout << "String(String&& s)--移动构造" << endl;
swap(s);
}
void swap(String& s)
{
::swap(_str, s._str);
}
String& operator=(const String& s)
{
if (this != &s)
{
cout << "String& operator=(const String& s)--赋值重载" << endl;
char* pTemp = new char[strlen(s._str) + 1];
strcpy(pTemp, s._str);
delete[] _str;
_str = pTemp;
}
return *this;
}
String operator+(const String& s)
{
char* pTemp = new char[strlen(_str) + strlen(s._str) + 1];
strcpy(pTemp, _str);
strcpy(pTemp + strlen(_str), s._str);
String strRet(pTemp);
return strRet;
}
~String()
{
if (_str) delete[] _str;
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2("world");
s1 + s2;
return 0;
}
这里调用的是operator+,因为其返回值出了函数作用域就会销毁所以我们不可以使用引用返回。当我们将移动构造函数屏蔽之后。
调用的是拷贝构造函数。
移动语义
C++11为了解决上面的函数在传值返回的时候进行了一次深拷贝然后又将资源进行了析构这造成了资源浪费和性能消耗。所以引入了移动语义。
移动语义就是在一个自定义类型对象即将销毁的时候,将其资源转移到另一个对象。
下面是移动构造函数
String(String&& s)
:_str(nullptr)
{
cout << "String(String&& s)--移动构造" << endl;
swap(s);
}
void swap(String& s)
{
::swap(_str, s._str);
}
移动构造就是一个资源转移的过程就是将右值对象s的资源转移走,因为右值对象在函数返回值这里出作用域就会消耗,所以这里被识别成了将亡值。就会调用移动构造函数,使用将亡值的资源在main函数栈帧内构造一个临时对象。
C++11中将右值分为:纯右值(内置类型,表达式等等)将亡值(自定义类型对象)
因为如果不将strRet的资源转移的话就需要进行拷贝,而先拷贝再析构是一种资源浪费。所以为了提高效率C++11对此进行了移动构造的优化。
其实C++编译器对此过程还有一些优化,当我们使用一个对象来接受返回值的时候。
当使用一个对象接受返回值的时候语法上应该出现两次拷贝构造,如上图。但是实际只出现了一次拷贝构造,这是因为编译器进行了优化,既然最后要拷贝构造出ret,那么拷贝构造临时对象这一步就可以省略了。直接使用strRet的字符串构造了ret然后析构了strRet。
当我们加上移动构造函数之后。
编译器没有进行优化的时候是先通过拷贝构造一个临时对象,临时对象会被识别成右值调用移动构造生成了ret。
编译器进行了优化之后就直接通过strRet移动构造了ret因为构造临时对象要深拷贝完成之后还需要析构,效率太低了。
在有移动构造的整个函数内,只需要在生成strRet的时候申请了一块内存空间即可,减少了空间的浪费且提高了效率。
注意:
1.移动构造函数参数不能加const。因为加上const之后资源就转移不出来了。就会导致移动语义失效。
2.在C++11中,编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理
时,用户必须显式定义自己的移动构造。
移动赋值
在C++11中的移动语义中除了有移动构造,还有移动赋值。
String& operator=(String&& str)
{
cout << "String& operator=(String&& str)--移动赋值" << endl;
swap(str);
return *this;
}
返回值是引用为了方便连续赋值。
这里完成了两次移动赋值,因为move之后的值是右值,所以匹配到了右值引用版本的赋值。第二个移动赋值是hello字符串发生了类型转换,因为单参的构造函数支持将hello转换成String的临时对象。临时对象也会被识别成右值所以调用的也是右值引用版本的赋值。
但是这里将左值s1强行move成右值进行赋值的副作用就是s1的内容就会变成ret里面的内容,也就是交换了。
所以move的作用其实就是将左值转换成右值,支持了移动语义。使用的时候要注意他的副作用。
其实移动语义并没有延长对象的生命周期。他只是将资源在不断的传递转移。
右值引用在容器的应用
C++11后的基本所有容器都支持了移动语义,并且在插入接口也支持右值引用版本。
支持了右值引用版本的插入之后,如果是插入的匿名对象,或者是类型转换出来的临时对象就会调用右值引用版本的插入,减少了拷贝构造,提高了效率。
完美转发
在右值引用的时候提到如果一个右值被右值引用了之后就会在内存中开辟出来一块空间保存,所以这个别名是可以取地址的,也就是说是一个左值,下面的代码调用可以看到,不管是右值还是左值,经过调用后都变成了左值。
这里的模板加右值引用是万能引用,不管是左值还是右值都可以传过去,如果传左值那么就会推导出左值引用,如果是右值就会推导出右值引用。但是作为引用别名的这个t,不管是左值引用还是右值引用,这个t都是左值,因为可以对t取地址。
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}
下面来看调用结果
可以看到都是调用到了左值的函数。这也就是说就算是右值引用t再次传参的时候也会发生退化变成了左值。
这时候就要用到了完美转发,完美转发就是保持t的实际类型转发给下一个调用的函数参数。
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}
对使用完美转发后的函数再次调用此次的结果就是
这时候右值就调用到右值引用参数的函数,左值引用调用的就是左值的函数。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数,针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
在STL的原码中也是实现了完美转发,下面用一个list的push_back函数做例子演示。
int main()
{
list<String> lt;
String s = "kisskernel";
lt.push_back(s);//左值对象插入调用的是左值的插入函数
lt.push_back("hello");//字符串转换成临时对象调用的是右值的插入
return 0;
}
list::push_back支持了右值引用版本
可以看到对于左值,list底层构造节点的时候使用了拷贝构造。对于右值调用了移动构造。这里为什么是移动构造不是赋值呢?这是因为我们平常开出的空间都是使用new,这时候会自动调用构造函数。但是STL的空间是来自内存池的。内存池开出来的空间是没有初始化的。所以这时候我们需要调用定位new来操作显式的调用构造函数。
下面手动模拟一下,定位new的使用。
list<T>* newnode = (list<T>*)malloc(sizeof(list<T>));
new(String*)String(forward<T>(newnode->_data));
右值引用总结
C++98中引用作用:因为引用是一个别名,需要用指针操作的地方,可以使用指针来代替,可以提高代码的可读性以及安全性。
C++11中右值引用主要有以下作用:
- 实现移动语义(移动构造与移动赋值)
- 给中间临时变量取别名:
3.实现完美转发
int main()
{
string s1("hello");
string s2(" world");
string s3 = s1 + s2; // 拷贝构造
stirng&& s4 = s1 + s2; // 给中间变量取别名
return 0; }
lambda表达式
lambda表达式的使用
我们在C++98中想要使用sort对自定义类型进行排序,就需要自己手写一个比较的仿函数传进去,但是如果我们需要进行多次比较每次按照自定义类型的不同成员进行比较的时候就需要写多个类的仿函数略显复杂,就像是调用C语言的qsort我们需要写比较函数传函数指针一样。
但是C++11加入了lambda表达式,我们就可以直接写lambda表达式来进行控制。
int main()
{
int arr[] = { 1,3,4,6,2,3,2,1,4,2,3,1,6,6,7,964,2,1 };
int n = sizeof(arr) / sizeof(int);
sort(arr, arr + n,greater<int>());
for (auto e : arr)
cout << e << " ";
return 0;
}
这是内置类型调用排序的时候,我们可以直接使用系统提供的仿函数。关于模板的参数,类模板的参数需要传类型,但是函数模板的参数需要传对象,不要搞混了哦。
struct agg
{
int _id;
string _name;
};
struct Greater
{
bool operator()(const agg& a1, const agg& a2)
{
return a1._name > a2._name;
}
};
int main()
{
agg arr[] = { {1,"hh"},{2,"pp"},{0,"zz"},{9,"ap"} };
int n = sizeof(arr) / sizeof(arr[0]);
sort(arr, arr + n, Greater());
for (auto e : arr)
{
cout << e._id << " " << e._name << endl;
}
return 0;
}
这是使用sort针对于自定义类型的排序时的写法,如果我们要针对不同的参数进行不同的升序降序排序那么总共需要些四个仿函数类。这不够优雅,于是C++11引入了lambda表达式,下面我们先来看看lambda是如何解决这个问题的呢?
int main()
{
agg arr[] = { {1,"hh"},{2,"pp"},{0,"zz"},{9,"ap"} };
int n = sizeof(arr) / sizeof(arr[0]);
sort(arr, arr + n, [](agg& a1, agg& a2)->bool{return a1._name > a2._name;});
for (auto e : arr)
{
cout << e._id << " " << e._name << endl;
}
return 0;
}
这就是使用lambda进行排序的写法,是不是很简洁。那么下面我们就来解释一下lambda该如何写。
通过上面说的函数模板只能传函数对象可以得知,lambda表达式实际也是一个匿名函数对象。所以我们也可以这样写。
int main()
{
agg arr[] = { {1,"hh"},{2,"pp"},{0,"zz"},{9,"ap"} };
int n = sizeof(arr) / sizeof(arr[0]);
auto Greater = [](agg& a1, agg& a2)->bool {return a1._name > a2._name; };
sort(arr, arr + n, Greater);
for (auto e : arr)
{
cout << e._id << " " << e._name << endl;
}
return 0;
}
lambda表达式的规则
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来 **
的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表。与普通函数的参数列表一致**,如果不需要参数传递,则可以连同()一起
省略 。
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修
饰符时,参数列表不可省略(即使参数为空也需要写一个空括号)。
如果不加mutable那么函数体内不可以对复制捕获到的变量进行赋值。
->returntype:返回值类型。没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其形参外,还可以使用所有捕获到的变量。
**注意:**lambda表达式参数列表和返回值类型都是可以省略的,最简单的lambda表达式是。但是该表达式什么用也没有。
int main()
{
// 最简单的lambda表达式, 该lambda表达式没有任何意义
[] {};
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=] {return a + 3; };
// 省略了返回值类型,无返回值类型
auto fun1 = [&](int c) {b = a + c; };
fun1(10);
cout << a << " " << b << endl;
// 各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int {return b += a + c; };
cout << fun2(10) << endl;
// 复制捕捉x
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; };//复制捕捉要加mutable否则不能对x进行修改
cout << add_x(10) << endl;
return 0;
}
lambad是个匿名对象,如不用auto接收是调用不到的。
lambda捕捉列表
捕捉列表就是将上下文代码中的变量捕获,给lambda函数体内使用。
捕获方式:
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
注意:
父作用域指包含lambda函数的语句块 。
语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量 c. 捕捉列表不允许变量重复传递,否则就会导致编
译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,再捕捉a就重复了。
在块作用域以外的lambda函数捕捉列表必须为空。
在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
lambda表达式之间不能相互赋值,即使看起来类型相同(但是在底层上实际是不同的)
仿函数与lambda表达式
仿函数,又叫函数对象,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。
class Rate
{
public:
Rate(double rate)
: _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
// 仿函数
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lambda
auto r2 = [=](double monty, int year)->double {return monty * rate * year; };
r2(10000, 2);
return 0;
}
这段代码的仿函数和lambda表达式都是完成了同样的功能。下面我们看一下汇编带大家了解一下他们的底层。
我们可以看到他们的底层汇编代码几乎是一样的,只是最后调用的不同的类里面的operator( )函数,所以lambda实际在底层就是一个仿函数。我们写的lambad表达式在编译的时候被编译器转成了一个仿函数类。这个类名就是lambad后面接上的字符串是一个uuid为了保证lambda生成的仿函数类不会出现命名冲突。