文章目录
C++11简介
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于TC1主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。
相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。
相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。
列表初始化
在C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。比如
int array1[] = {1,2,3,4,5};
int array2[5] = {0};
对于一些自定义的类型,却无法使用这样的初始化。比如:
vector<int> v{1,2,3,4,5};//报错
就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
内置类型的列表初始化
// 内置类型变量
int x1 = {10};
int x2{10};
int x3 = 1+2;
int x4 = {1+2};
int x5{1+2};
// 数组
int arr1[5] {1,2,3,4,5};
int arr2[]{1,2,3,4,5};
// 动态数组,在C++98中不支持
int* arr3 = new int[5]{1,2,3,4,5};
// 标准容器
vector<int> v{1,2,3,4,5}; //底层也是利用initializer_list类模板
map<int, int> m{{1,1}, {2,2,},{3,3},{4,4}};
注意:列表初始化可以在{}之前使用等号,其效果与不使用=没有什么区别。
自定义类型的列表初始化
利用initializer_list支持列表初始化
多个对象想要支持列表初始化,需要自己给该类(模板类)添加一个带有 initializer_list 类型参数的构造函数即可。
注意:initializer_list 是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法 size()
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;
};
vector<int> v({ 1, 2, 3, 4, 5 });
//在实例化对象时候调用了构造函数,传进去的实参是 { 1, 2, 3, 4, 5 },这样就会创建一个 initializer_list 的临时对象,再用来构造vector
变量类型推导
auto类型推导
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂,比如:
short a = 32670;
short b = 32670;
// c如果给成short,会造成数据丢失,如果能够让编译器根据a+b的结果推导c的实际类型,就不会存在问题
short c = a + b; //可能存在的数据溢出问题
// 使用迭代器遍历容器, 迭代器类型太繁琐
std::map<std::string, std::string>::iterator it = m.begin();
C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。
将程序中c与it的类型换成auto,程序可以通过编译,而且更加简洁。
auto c =a+b;
auto it =m.begin();
decltype类型推导
auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。
但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。
template<class T1, class T2>
void Add(const T1& left, const T2& right)
{
auto ret=left+right; //对于模板类型,编译期间是无法推导类型的
cout<<ret<<endl;
}
template<class T1, class T2>
auto Add(const T1& left, const T2& right)
{
return left + right;
}
如果能用加完之后结果的实际类型作为函数的返回值类型就不会出错,但这需要程序运行完才能知道结果的实际类型,即RTTI(Run-Time Type Identification 运行时类型识别)。
decltype
decltype是根据表达式的实际类型推演出定义变量时所用的类型.
- 推演表达式类型作为变量的定义类型
int a = 10;
int b = 20;
// 用decltype推演a+b的实际类型,作为定义c的类型
decltype(a+b) c;
cout<<typeid(c).name()<<endl;//使用typeid可以查看类型名称
//注意typid只能查看类型不能用其结果类定义类型
- 推演函数返回值的类型
void* GetMemory(size_t size)
{
return malloc(size);
}
int main()
{
// 如果没有带参数,推导函数的类型
cout << typeid(decltype(GetMemory)).name() << endl;
// 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
cout << typeid(decltype(GetMemory(0))).name() <<endl;
return 0;
}
这样就可以解决auto编译时无法推演模板类型表达式的问题:
template<class T1, class T2>
auto Add(const T1& left, const T2& right)->decltype(left+right)
{
return left + right;
}
此时auto作用仅为占位符,真正的返回值是后面的decltype(left+right) 。
如果没有后置返回值,则函数声明时放到之前为:
decltype(left+right)auto Add(const T1& left, const T2& right)
而此时left 和 right都还未曾声明过,编译自然无法通过;
所以使用auto 作为类型占位符,即我现在还不知道要返回的表达式的类型,而在函数后面定义了参数之后在进行表达式的实际类型推演函数返回值类型。
范围for循环
范围for循环是基于容器的迭代器实现的,就是说一个类只要提供了(实现了)底层的迭代器函数,就可以支持基于范围的for循环。而像vector、数组、string等迭代器都是原生指针,也支持范围for循环。
语法:
for(元素类型 元素对象变量:容器对象)
{
循环体;
}
举例:
vector<int> v{1,2,3,4,5};
for(auto e:v)
{
cout<<e<<endl;
}
//也可以使用auto 推演变量实际类型
final与override
final
- 修饰虚函数,表示该虚函数不能再被继承
final是C++11中定义的继承控制关键字,它可以修饰虚函数,表示该虚函数不能被继承 - 修饰类,表示此类为最终类,不能被继承。
override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写,则编译报错(因为实际开发中派生类继承虚函数是必然会重写逻辑的,要实现多态嘛,但是由于可能疏忽忘了重写,此派生类就继承为抽象类了,无法实例化对象了,所以必须重写)。
新增加容器
静态数组array、单链表forward_list以及 unordered系列的容器。
前面两个基本没啥实用性,而基于哈希原理实现的unordered系列容器要比基于红黑树实现的map、set等容器要高效。
默认成员函数控制
在C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数和&和const&的重载、移动构造、移动拷贝构造等函数。如果在类中显式定义了,编译器将不会重新生成默认版本。
有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。
而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成。
编译器生成构造函数的场景
- A类具有无参 或者 带有全缺省的构造函数,B类没有显式定义任何构造函数,但B类中包含A类的对象,编译器在编译阶段会给B类生成默认的构造函数
- 在继承体系下,如果基类具有无参 或者 带有全缺省的构造函数,派生类没有显式定义任何构造函数,编译器在编译阶段会给派生类生成默认的构造函数
- 在虚拟继承体系中,需要在构造对象阶段将指向虚基表的指针放在对象的前4个字节
- 如果类中具有虚函数,在创建对象期间需要将虚表的地址放在对象的前4个字节
显式缺省函数
在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数。
class A
{
public:
A(int a): _a(a)
{}
// 显式缺省构造函数,由编译器生成
A() = default;
// 在类中声明,在类外定义时让编译器生成默认赋值运算符重载
A& operator=(const A& a);
private:
int _a;
};
A& A::operator=(const A& a) = default;
int main()
{
A a1(10);
A a2;
a2 = a1;
return 0;
}
删除默认函数
如果能想要限制某些默认函数的生成,在C++98中,是将该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。
在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对
应函数的默认版本,称=delete修饰的函数为删除函数。
class A
{
public:
A(int a): _a(a)
{}
// 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
A(const A&) = delete;
A& operator(const A&) = delete;
private:
int _a;
};
int main()
{
A a1(10);
// 编译失败,因为该类没有拷贝构造函数
//A a2(a1);
// 编译失败,因为该类没有赋值运算符重载
//a3 = a2;
return 0;
}
NULL与nullptr 空值指针
在C语言中,使用NULL表示空指针。实际在C中,NULL通常被如下定义:
#define NULL ((void *)0)
也就是说NULL实际上是一个 void * 的指针,然后将 void * 指针赋值给对应类型的指针的时候,隐式转换成相应的类型。
而如果换做一个C++编译器来编译的话是要出错的,因为C++是强类型的,void *是不能隐式转换成其他指针类型的,所以通常情况下,编译器提供的头文件会这样定义NULL:
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
可以看到NULL 还被宏定义为 0 ;但是实际上,用NULL代替0表示空指针在函数重载时会在C++程序中出现二义性:
void func(void* i)
{
cout << "func1" << endl;
}
void func(int i)
{
cout << "func2" << endl;
}
int main()
{
func(NULL); //这里NULL会被替换成整型 0 ,调用了第二个函数,但是我们明明是调用传空指针对应的函数
return 0;
}
上述代码中,运行结果与我们使用NULL的初衷是相违背的,因为我们本来是想用NULL来代替空指针,但是在将NULL输入到函数中时,它却选择了int形参这个函数版本,所以是有问题的,这就是用NULL代替空指针在C++程序中的二义性。
所以在C++11 中引入了 nullptr 这个关键字来代指空指针,来解决NULL代替空指针的二义性问题:
func(nullptr); //这样便解决了问题
右值引用
右值引用概念
引用的概念:
C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。
void Swap(int& left, int& right) //引用传参,减少拷贝
{
int temp = left;
left = right;
right = temp;
}
int main()
{
int a = 10;
int b = 20;
Swap(a, b); //对于变量的引用,称为左值引用
}
为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。
int Add(int a, int b)
{
return a + b;
}
int main()
{
const int&& ra = 10; //10为常量
// 引用函数返回值,返回值是一个临时变量,为右值
int&& rRet = Add(10, 20);
return 0;
}
为了与C++98中的引用进行区分,C++11将该种方式称之为右值引用。
左值与右值
左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式:
一般认为(不一定完全正确):
可以放在=左边的,或者能够取地址的称为左值,
只能放在=右边的,或者不能取地址的称为右值。
也可以这样区分:
可以修改就可以认为是左值,即通常是变量;
右值通常是常量,表达式或者函数返回值(临时对象)。
int x1 = 10; int x2 = x1; int x3 = x1+x2;
//这里x1是左值,10是右值,x2是左值,x1+x2表达式返回值就是右值
int g_a = 10;
// 函数的返回值结果为引用
int& GetG_A()
{
return g_a;//由于是全局变量,作用域为整个程序生命周期,所以可以使用引用返回
}
int main()
{
int a = 10;
int b = 20;
// a和b都是左值(变量),b既可以在=的左侧,也可在右侧,
// 说明:左值既可放在=的左侧,也可放在=的右侧
a = b;
b = a;
const int c = 30;
// 编译失败,c为const常量,只读不允许被修改
//const int c = a; //可以赋值,但是为const属性,不能修改
// 因为可以对c取地址,因此c严格来说不算是左值
//cout << &c << endl;
// 编译失败:因为b+1的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值
//b + 1 = 20;
GetG_A() = 100;
return 0;
}
从上面的例子可以这样区分:
- 普通类型的变量,因为有名字,可以取地址,都认为是左值。
- const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。
- 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
- 如果表达式运行结果或单个变量是一个引用则认为是左值
总结:
- 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断,比如上述:c常量
- 能得到引用的表达式一定能够作为引用,否则就用常引用。
C++11对右值进行了严格的区分:
C语言中的纯右值:常量或者临时对象,比如:a+b, 100
将亡值:比如:表达式的中间结果、函数按照值的方式进行返回。
引用与右值引用比较
在C++98中的普通(左值)引用与const引用在引用实体上的区别:
int main()
{
// 普通类型引用只能引用左值,不能引用右值
int a = 10;
int& ra1 = a; // ra1为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
const int& ra3 = 10; //但是const引用可以引用右值
const int& ra4 = a;
return 0;
//注意: 普通引用只能引用左值,不能引用右值,
//const引用既可引用左值,也可引用右值。
}
C++11中右值引用:只能引用右值,一般情况不能直接引用左值。
int main()
{
// 10纯右值,本来只是一个符号,没有具体的空间,
// 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
int&& r1 = 10;
r1 = 100;
int a = 10;
int&& r2 = a; // 编译失败:右值引用不能引用左值
return 0;
}
问题:既然C++98中的const类型引用左值和右值都可以引用,那为什么C++11还要复杂的提出右值引用呢?答案:肯定是与效率有关!
值的形式返回对象的缺陷
如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数(深拷贝),否则编译器将会自动生成一个默认的(浅拷贝,会有double free问题)。
比如string类:
class String
{
public:
String(char* str = "")
{
if (nullptr == str)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s)
: _str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
String& operator=(const String& s)
{
if (this != &s)
{
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");
String s3(s1+s2);
return 0;
}
问题:
在operator+中:strRet在按照值返回时,必须创建一个临时对象,临时对象创建好之后,strRet就被销毁了,最后使用返回的临时对象构造s3,s3构造好之后,临时对象就被销毁了。
仔细观察会发现:strRet、临时对象、s3每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内容完全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大,那能否对该种情况进行优化呢?即移动语义的出现。
移动语义
C++11提出了移动语义概念,即:将一个对象中的资源移动到另一个对象中的方式,可以有效缓解该问题。
在C++11中如果需要实现移动语义,必须使用右值引用。上述String类增加移动构造
String(String&& s) //参数s其实是实参的构造临时对象,出作用域自动调用析构的,也称为将亡值,仅仅只是为了传参而构造出来的
: _str(s._str) //移动语义
{
s._str = nullptr;//没用了,会自动析构的
}
因为strRet对象的生命周期在创建好临时对象后就结束了,即将亡值,C++11认为其为右值,在用strRet构造临时对象时,就会采用移动构造,即将strRet中资源转移到临时对象中。而临时对象也是右值,因此在用临时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率。
注意:
- 移动构造函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效。
- 在C++11中,编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理时,用户必须显式定义自己的移动构造。
总结:
- 右值引用做参数和作返回值减少拷贝的本质是利用了移动构造和移动赋值
- 左值引用和右值引用本质的作用都是减少拷贝,右值引用本质可以认为是弥补左值引用不足的地方, 他们两相辅相成
左值引用:解决的是传参过程中和返回值过程中的拷贝
- 做参数:void push(T x) -> void push(const T& x) 解决的是传参过程中减少拷贝
- 做返回值:T f2() -> T& f2() 解决的返回值过程中的拷贝
ps:但是要注意这里有限制,如果返回对象出了作用域不在了就不能用传引用, 这个左值引用无法解决,等待C++11右值引用解决
右值引用:解决的是传参后,push/insert函数内部将对象移动到容器空间上的问题+ 传值返回接收返回值的拷贝问题
- 做参数: void push(T&& x) 解决的push内部不再使用拷贝构造到容器空间上,而是移动构造过去
- 做返回值:T f2(); 解决的外面调用接收f2()返回对象的拷贝,T ret = f2(),这里就是右值引用的移动构造,减少了拷贝
右值引用引用左值
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?
因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。
C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
String f(const char* str)
{
String tmp(str);
return tmp; // 这里返回实际是拷贝tmp的临时对象
}
int main()
{
String s1("左值");
String s2(s1); // 参数是左值
String s3(f("右值-将亡值")); // 参数是右值-将亡值(传递给你用,用完我就析构了)
//String s4(move(s1));
//注意:以上代码是move函数的经典的误用,因为move将s1转化为右值后,在实现s4的拷贝时就会使用移动构造,此时s1的资源就被转移到s2中,s1就成为了无效的字符串。
//除非s1之后不会用了,否则就得不偿失了
完美转发
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
void Func(int x)
{
// ......
}
template<typename T>
void PerfectForward(T t)
{
Fun(t);
}
PerfectForward为转发的模板函数,Func为实际目标函数,但是上述转发还不算完美,完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
void Fun(int &x){ cout << "lvalue ref" << endl; }
void Fun(const int &x){ cout << "const lvalue ref" << endl; }
void Fun(int &&x){ cout << "rvalue ref" << endl; }
void Fun(const int&& x){ cout << "const rvalue ref" << endl; }
template<typename T>
void PerfectForward(T &&t)
{
// 右值引用会在第二次之后的参数传递过程中右值属性丢失,下一层调用会全部识别为左值
Fun(std::forward<T>(t)); //而C++11通过forward函数来实现完美转发
}
lambda表达式
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。
#include <algorithm>
#include <functional>
template<class T>
struct Greater
{
bool operator()(const T& x1, const T& x2)
{
return x1 > x2;
}
};
bool g2(const int& x1, const int& x2)
{
return x1 > x2;
}
int main()
{
int array[] = {4,1,8,5,3,7,0,9,2,6};
// 默认按照小于比较,排出来结果是升序
std::sort(array, array+sizeof(array)/sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
Greater<int> g1;//比较器仿函数对象
g1(1, 2); // g1是一个对象,这里调用的是他的operator()实现的比较
g2(1, 2); // g2是一个函数指针,这里是调用他指向的函数
// 他们是完全不同的对象但是他们用起来是一样的。
return 0;
}
如果待排序元素为自定义类型,需要用户定义排序时的比较规则:
struct Goods
{
string _name; // 名字
double _price; // 价格
int _num; // 数量
// ...
};
// 那么这里如果去重载Goods的operator>/operator<是不好的,因为你不知道需要按哪一项成员去比较
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
struct CompareNumGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._num > gr._num;
}
};
struct CompareNameGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._name > gr._name;
}
};
// 其实还有小于的,大于等于和小于等于,会发现我们要写很多个仿函数
// 其实直接写函数也可以,不过类似要写很多个函数
int main()
{
Goods gds[] = { { "苹果", 2.1 , 3}, { "相交", 3.0, 5}, { "橙子", 2.2, 9}, { "菠萝", 1.5, 10} };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), ComparePriceGreater());//根据价格从高到低
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), CompareNumGreater());
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), CompareNameGreater());
return 0;
}
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C11语法中出现了Lambda表达式。
// 下面看我们用lambda表达式来更好的解决问题
// lambda
auto price_greater = [](const Goods& g1, const Goods& g2){
return g1._price > g2._price;
};
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), price_greater);
//也可以直接写到一起
sort(gds, gds + sizeof(gds) / sizeof(gds[0]),[](const Goods& g1, const Goods& g2)
{return g1._price > g2._price;}
);
上述代码就是使用C++11中的lambda表达式来解决,可以看出lambda表达式实际是一个匿名函数。
lambda表达式语法
lambda表达式书写格式:
[capture-list] (parameters) mutable -> return-type { statement }
lambda表达式各部分说明
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- return-type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的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; };
cout << add_x(10) << endl;
return 0;
}
捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
注意:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:
[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
f. 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;
}
底层还是依靠仿函数来实现,也就是说你定义了一个lamber表达式,实际上编译器会全局域生成一个叫lambda_uuid类,而此仿函数的operator()的参数和实现就对应我们写的labmber表达式的参数和实现: