8.新的类功能
8.1默认成员函数
原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造
- 赋值重载
- 取地址重载
- const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个默认成员函数------不显式实现,编译器会自动生成。
- 移动构造;
- 移动赋值;
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
8.1.1默认移动构造的生成条件、作用功能
编译器自动生成默认移动构造的条件比其他默认成员函数更苛刻:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器才会自动生成一个默认移动构造。
默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造:
- 实现了移动构造:就调用移动构造;(编译器生成 or 用户显式实现)
- 没有实现移动构造:但有拷贝构造,那就调用拷贝构造;
8.1.3默认移动赋值重载的生成条件、作用功能
编译器自动生成默认移动赋值的条件:
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器才会自动生成一个默认赋值重载。
默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值:
- 如果实现了,就调用移动赋值;(编译器生成 or 用户显式实现)
- 没有实现,就调用拷贝赋值。
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
【测试】
cpp
// 以下代码在vs2013中不能体现,在vs2019下才能演示体现上面的特性。
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()
{}*/
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;
}

- s3调移动构造,结果是s1被偷家了。(s1变成空,或有效但未指定的状态)
- s4调移动赋值,结果是s2被偷家了。(但它们的内容是一样的)

注意,有缺省参数的时候,声明缺省值就没有用了。

写一个析构,编译器就不会生成移动构造、移动赋值了:

这样设计的原因:
- 用户显式实现了析构,一般也就意味着用户同时显式实现了:
- 拷贝构造、赋值重载、移动拷贝、移动赋值重载。
编译器自动生成移动构造的条件看似苛刻,其实反而是更合理的。
这5个默认成员函数是一体化的------需要写的话都需要写,不需要写的话都不用写。

编译器自动生成的移动构造,对于:
- 有自定义类型成员;
- 而且自定义类型成员有自己的移动构造;
- 而且没有指针成员管理资源;
这时候是有意义的。(自身没有资源需要管理,但自己的自定义类型成员有资源需要管理)
虽然person没有资源需要管理,但它的成员string有资源需要管理。
此时person自动生成的移动构造有意义了------要是没有它,就会调person的拷贝构造,而person的拷贝构造再去调用string的拷贝构造。
但是string明明有移动构造。
【注意】一个类不需要析构时,不要随便写析构。
8.2类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这个我们在类和对象默认就讲了,这里就不再细讲了。
8.3强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。
假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显式指定编译器生成移动构造。
【应用场景】
- 场景1:显式实现了构造或拷贝构造,但是没有默认构造。
- 场景2:显式实现了拷贝构造,需要移动构造,但是不想自己写,编译器生成的就够用了。
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(Person&& p) = default;
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
【测试】
有了析构就不会生成移动构造:

可以指定生成移动构造,但是必须保证显式实现了移动构造、赋值重载。


| 你做了什么 | 编译器生成的 | 被删除的 |
|---|---|---|
| 什么都不写 | 默认构造、析构、拷贝构造、赋值重载、 移动构造、移动赋值 | 无 |
= default 移动构造 |
默认构造、析构、移动构造 | 拷贝构造、拷贝赋值 |
= default 移动赋值 |
默认构造、析构、移动赋值 | 拷贝构造、拷贝赋值 |
| 自定义移动构造 | 默认构造、析构 | 拷贝构造、拷贝赋值、移动赋值 |
| 自定义移动赋值 | 默认构造、析构 | 拷贝构造、拷贝赋值、移动构造 |
这些函数是一体化的,显式实现了一个,编译器就认为其他的都你自己显式实现了。
目的在于让编程的人不要瞎搞,写了这样又不写那个,很容易出错,要按规范来。

但是显式实现析构,编译器还是会生成默认拷贝构造、赋值重载。
8.4禁止生成默认函数的关键字delete
如果想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明不实现,这样只要其他人想要调用就会报错。

只声明不实现就够了?

并不是,还需要设置成私有。
否则只声明不实现的公有,使用的人可以在包了头文件之后,可以在类外定义。

在C++11中更简单,只需在该函数声明加上=delete即可------该语法指示编译器不生成对应函数的默认版本。
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete; //防拷贝
private:
bit::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
称=delete修饰的函数为**"删除函数"**。
【应用场景】有时候不希望一个类被拷贝,就可以限制一下拷贝构造、赋值重载。
io流就是不想被拷贝的类,因为io流涉及缓冲区的概念,拷贝的话需要考虑缓冲区的内容拷贝不。

移动构造是受保护的,只能内部用,或者子类用,外部用不了。
8.5继承和多态中的final与override关键字
这个我们在继承和多态章节已经进行了详细讲解这里就不再细讲,需要的话去复习继承和多台章节吧。
- final:修饰一个类,不能被继承;修饰一个虚函数,不能被重写。
- overide:加在派生类的虚函数后面,检查是否完成重写。
9. 模版的可变参数
C++11的新特性------可变参数模板,能够支持创建可以接受可变参数的函数模板和类模板。
相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。
然而由于可变参数模版比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大家如果有需要,再可以深入学习。
写"可变·模版参数"的场景很少,一般写库才会用到。可变·模版参数属于了解内容。
C语言的函数的可变参数的标志是三个点"...",底层类似于用一个动态数组去接收,然后去数组里面依次把对应的参数解析出来。

C++的模版的可变参数也是用的三个点"...",下面就是一个基本可变参数的函数模板:
【...位置1】
cpp
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args> //三点在Args前
void ShowList(Args... args) //三点在Args后
{}
//arguments:n.参数
**参数包:**上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为"参数包",它里面包含了0到N(N>=0)个模版参数。
模版参数包:代表多个类型;
形参参数包:代表多个参数;
总的来说,参数包代表了多个类型和多个参数。++可以传任意多个参数,它们可以是任意类型。++


可变模版参数的调用方式多种多样。

之前的函数模版是参数类型可变,但个数固定。
cpp
//固定参数个数
template <class A>
void ShowList(A&& args)
{
}
可变模版参数------参数类型可变、参数个数可变。
本质还是调用了以后,编译器去实例化,生成了各个版本的ShowList。
- 无参的ShowList;
- 一个int参数的ShowList;
- 一个int参数和一个const char*的ShowList;
- ......
我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。

C语言的printf的可变参数是用类似的方式解析的。是在运行时去取可变参数。
- 而模版是在编译时就要解析出每个参数的类型。
- for循环是运行时逻辑,是在运行获取和解析,所以不支持这样用。
由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来逐个获取参数包的值。
9.0 打印参数包内容
【...位置2】
【使用sizeof打印参数包的大小(参数的个数)】...紧跟在sizeof后

C++11开始,很多新增的语法写出来的方式很怪异,没有老语法好学。
9.1 递归函数方式展开参数包
【...位置3】
编译时·递归推导·解析参数:

【传递参数包】...紧跟在args后
递归需要结束条件,只能用参数匹配的方式。(编译时推导)

零个参数的参数包会优先匹配到无参的重载函数。
cpp
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value <<" ";
ShowList(args...); //递归
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
【说明】


把x,y,z传给print,打印完x之后,把y,z向下传递,然后打印y,然后把z向下传递。
最后调用无参print。
程序员方便了,底层的编译器要做的事多了------根据调用情况生成不同的函数。
C++11出来了以后,C++编译器的开发难度大大提高。
为什么要一层一层地调用,每层只打印一个数据,而不在顶层就把三个数据打印完。
就是为了实现可变参数模版------一个模版就能顶无数个模版,帮程序员节省了很多个要写的模版。
像ShowList(1, "1111", 1.1, 2, "2222")这样的就不用再写一个模版。
**【可变参数模版是模版的模版】**通过可变参数模版,可以实例化出一个参数的函数模版、两个参数的函数模版、三个参数的函数模版、......
9.2 逗号表达式展开参数包
【...位置4】
这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。
- 这种就地展开参数包的方式实现的关键是逗号表达式。
- 逗号表达式会按顺序执行逗号前面的表达式,逗号表达式的值是最后一个式子的值。
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性------初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... )。
最终会创建一个元素值都为0的数组int arr[sizeof... (Args)]。
由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
cpp
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;
}
【测试】

数组的大小取决于大括号{ }初始化列表里面的值的个数。
不用逗号表达式也可以,不过需要设置返回值:

编译的时候数组开多大,取决于初始化列表里面有多少个值,那就会去执行PrintArg(args)...,解析args里面有多少个参数。
数组的作用就是让编译器去展开参数包。

cpp
// 编译推演生成下面的函数
void ShowList(int x, char y, std::string z)
{
int arr[] = { PrintArg(x),PrintArg(y),PrintArg(z) }; //数组开3个int的空间
cout << "现成" << endl;
}
也可以直接cout输出。

ostream arr[] = {cout << (args)...}是非法的,因为ostream没有拷贝构造。

使用auto也是推导成ostream,而且数组不支持auto。
ostream&也是一样,数组不支持引用类型。
bool arr也不行,虽然ostream支持隐式类型转换成bool。
以上都是尝试解析出参数包的每一个参数,实践当中并不一定需要解析出每一个参数。
9.3 STL容器中的empalce相关接口函数
http://www.cplusplus.com/reference/vector/vector/emplace_back/
http://www.cplusplus.com/reference/list/list/emplace_back/
cpp
template <class... Args>
void emplace_back (Args&&... args);
首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。
9.3.1 empalce用法

emplace_back的功能在一起情况下,和push_back一致:

打印结果:

emplace的可变模版参数不支持插入多个值。

emplace的可变参数是针对一个结点内是一个自定义构造的情况:

这里是:"构造" + "拷贝构造(移动构造)"
- 构造:pair的first成员;
- 拷贝构造(移动构造):将pair给到list的结点的data上。

emplace_back插入的时候,可以单独写出pair的first成员和second成员,而不必传一个pair。
这里是:"构造"

- 直接把构造pair的参数包往下传;
- 底层直接用pair的参数包构造pair;
- 省了一个拷贝构造(移动构造);

如果是push_back,那参数类型确定是string,就必须先构造一个string,给到形参。
这里是emplace_back,是模版,推导出来是cahr*------正好是用于构造string对象的参数包。
而构造string的参数包可以有多个参数:

添加n个ch的构造函数:

测试结果:

存储string的lt,只要是构造string的参数包(有对应的构造),都可以给emplace_back。
参数包往下传,最后用来构造string。
存储pair的lt1,只要是构造pair的参数包(有对应的构造),都可以给emplace_back。
参数包往下传,最后用来构造pair。
emplace_back不需要解析参数包,只需要把参数包传递到底层,用来构造对应的成员。
- 不解析参数包;
- 只是搬运一下参数包;
9.3.2 empalce实现

这里两个函数都只是搬运一下参数包。
参数包最后到底层的结点构造函数里面去匹配:

【测试】
lt.emplace_back("111111111111")一直没有解析参数包,直到结点构造函数_data("11111")调用string的const cahr*构造。

参数包传到底层,然后直接去匹配对应的构造。
本质:编译器生成了对应的emplace_back(const char* ch)、insert(const char* ch)、ListNode(const char* ch)。
用不同的参数包(构造),编译器就会生成多份emplace_back(...)、insert(...)、ListNode(...)。

问题:插入右值,但是调用的是string的深拷贝。
原因:右值往下传的时候,会退化成左值。
解决方法:forward完美转发。

以及最底层也要加上forward,少任一个都不行:

测试:


【emplace可变模版参数】用的时候不用解析,只需要不断往下传,在底层匹配对应的构造函数。
9.3.1 empalce功能比较
那么相对insert和push_back系列,emplace接口的优势到底在哪里呢?
cpp
int main()
{
std::list< std::pair<int, char> > mylist;
// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30, 'c'));
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });
for (auto e : mylist)
cout << e.first << ":" << e.second << endl;
return 0;
}
int main()
{
// 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢
// 我们会发现其实差别也不到,emplace_back是直接构造了,push_back
// 是先构造,再移动构造,其实也还好。
std::list< std::pair<int, bit::string> > mylist;
mylist.emplace_back(10, "sort");
mylist.emplace_back(make_pair(20, "sort"));
mylist.push_back(make_pair(30, "sort"));
mylist.push_back({ 40, "sort"});
return 0;
}
emplace_back可以直接传待插入的对象。
也可以直接传·用于构造传待插入的对象的参数包------这会更高效。
插入右值就是"构造"+"移动构造",相比于直接构造,效率其实差不多。
对于左值,使用emplace插入会高效很多。
emplace_back总体而言是更高效,推荐使用。
emplace除了传成品,还可以直接传原材料。
10. lambda表达式
10.1C++98中的一个例子
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。
cpp
#include <algorithm>
#include <functional>
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>());
return 0;
}
实践当中对整型数组的排序意义不大。
更多的是对结构体·数组的排序,按结构体的某一个成员来进行排序。
如果待排序元素为自定义类型,需要用户提供排序时的比较规则:
cpp
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), ComparePriceLess()); //sort是函数,需要匿名对象
sort(v.begin(), v.end(), ComparePriceGreater());
}
小于比较排升序。

大于比较排降序。

随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类(仿函数)。
如果每次比较的逻辑不一样------按价格比较(升、降)、按评价......,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。
因此,在C++11语法中出现了Lambda表达式。
10.2lambda表达式
cpp
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._price < g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._price > g2._price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._evaluate < g2._evaluate; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
return g1._evaluate > g2._evaluate; });
}
上述代码就是使用C++11中的lambda表达式来解决,可以看出lambda表达式实际是一个匿名函数。
【课堂演示】


lambda表达式:其他函数体内部,定义了一个局部匿名函数对象,这个对象是一个函数。

不一定是局部,定义在全局也是可以的。
返回值类型和参数可省略:

有返回值的时候,返回值的类型可以省略,可以自动推导。
没有返回值的时候,就要注意不要去获取返回值。

还是推荐把返回值写上,函数的逻辑更好懂,可读性更好。
直接明了地写出return 0还好说,要是返回变量在定义的时候被定义成auto,就需要一层一层往下看,不写返回值的可读性就很差。

这种情况就最好把返回值写上,或者去把注释写清楚。
用仿函数的方式进行排序,缺点在于每种比较逻辑都要单独写一个仿函数,还要创建对应的对象。而且仿函数还有命名的问题。

不如使用lambda表达式:

测试:

缺点:不方便调试。
断点最好不要打到lambda函数体内:

否则按F10调试会在944行和945行之间来回跳------sort函数内部频繁调用这个lambda对象。
lambda函数、函数指针都是一种回调。

- 传值捕捉本质是一种拷贝,并且const修饰了。
- mutable相当于去掉const属性,可以修改了。------加mutable必须带(),哪怕没有参数
mutable只对值捕获有作用。

可以看到a和b的值只交换了一次:

- mutable去掉常属性,修改了也不会影响外面被捕捉的值,因为是一种拷贝。
捕捉列表分为:传值捕捉、传引用捕捉。


在此之前,取地址和引用不会混淆:
- int&:类型之后的&是引用;
- &x:变量之前的&是取地址;
【各种捕获方式】
cpp
int x = 0;
int main()
{
// 只能用当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3;
// 父域(能访问的)所有变量------传值捕捉
// 所有值传值捕捉
auto func1 = [=]
{
int ret = a + b + c + d + x;
return ret;
};
// 父域(能访问的)所有变量------传引用捕捉
// 所有值传引用捕捉
auto func2 = [&]
{
a++;
b++;
c++;
d++;
int ret = a + b + c + d;
return ret;
};
// 混合捕捉
auto func3 = [&a, b]
{
a++;
// b++;
int ret = a + b;
return ret;
};
// 混合捕捉
// 所有值以引用方式捕捉,但是d用传值捕捉
auto func4 = [&, d]
{
a++;
b++;
c++;
//d++;
int ret = a + b + c + d;
};
// 混合捕捉
// 所有值以传值方式捕捉,但是d用传引用捕捉
auto func5 = [=, &d]() mutable
{
a++;
b++;
c++;
d++;
int ret = a + b + c + d;
};
// 实际上只改变了外面的d
return 0;
}
- 默认可以访问:当前lambda局部域的对象、全局域的对象(例如:cout)
- 父域的对象:需要依靠捕获列表捕获之后,才能使用。
lambda表达式一般都不加mutable,不想改变捕获变量的时候不用加,想改变捕获变量的时候传引用,加mutable允许在内部改变捕获变量的拷贝,不影响外部的捕获变量。
传值捕获加const修饰就是为了避免你认为里面改变了,外面跟着变。
mutable可以解扣,但是慎用。
10.3lambda表达式语法
lambda表达式书写格式:
cpp
[capture-list] (parameters) mutable -> return-type { statement }
(parameters)、mutable、->return-type这三个部分可以省略。
最简单的lambda表达式只有:捕获列表、函数体。
- 用lambda表达式来赋值,要在末尾"}"外面加上分号;
- lambda表达式作参数,不用加分号;
10.3.1lambda表达式各部分说明
- **[capture-list]-捕捉列表:**该列表总是出现在lambda函数的开始位置,编译器根据[]来 判断接下来的代码是否为lambda函数 ,捕捉列表能够捕捉上下文中的变量供lambda 函数使用。
- (parameters)-参数列表:与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- ->returntype-返回值类型:用追踪返回类型形式声明函数的返回值类型。
- 没有返回值时此部分可省略。
- 返回值类型明确情况下,也可省略,由编译器对返回类型进行推 导。
- **{statement}-函数体:**在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
有名函数就是:返回值、参数、函数体,外加函数名。
lambda表达式就可以理解为匿名函数。
【注意】
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为 空。
因此C++11中最简单的lambda函数为:
cpp
[]{};
该lambda函数不能做任何事情。
cpp
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表达式实际上可以理解为无名函数,该函数无法直接调用。
如果想要直接调用,可借助auto将其赋值给一个变量。
10.3.2 捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用 ,以及使用的方式传值还是传引用。
- **[var]:**表示值传递方式捕捉变量var
- **[=]:**表示值传递方式捕获所有父作用域中的变量(包括this)
- **[&var]:**表示引用传递捕捉变量var
- **[&]:**表示引用传递捕捉所有父作用域中的变量(包括this)
- **[this]:**表示值传递方式捕捉当前的this指针
【注意】
- a. 父作用域指包含lambda函数的语句块;
- b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:
=, \&a, \&b\]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量。
- c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:
=, a\]:=已经以值传递方式捕捉了所有变量,捕捉a重复
- **e.**在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
- f. lambda表达式之间不能相互赋值,即使看起来类型相同。
cpp
void (*PF)();
int main()
{
auto f1 = []{cout << "hello world" << endl; };
auto f2 = []{cout << "hello world" << endl; };
// 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0;
}
10.4函数对象与lambda表达式
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象。
cpp
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);
// lamber
auto r2 = [=](double monty, int year)->double
{
return monty*rate*year;
};
r2(10000, 2);
return 0;
}
从使用方式上来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
【课堂演示】

lambda和仿函数都是函数对象,调用方式也是一样的。
从汇编底层来看:


和引用类似,引用是一个语法上的新概念,但是底层没有引用的概念,引用的底层是指针。
lambda表达式也是一个语法上的新概念,底层没有lambda表达式,lambda被编译之后,会生成对应的仿函数,仿函数的类型名是"lambda_ + 一串字符串(UUID)"。
对我们来说它是一个匿名函数(匿名类),只能用auto接收,但是对编译器它不是匿名的,它有名字,这个名字是编译器自己生成的。
- 捕捉列表中的对象rate,是作为构造的参数传给lambda,等价于成员变量存在lambda类对象中。
- 捕捉列表中的变量是以成员变量的形式存储的。
- 捕捉的本质是构造函数的初始化参数。
lambda表达式的调用,本质还是去调用operator()。
这里传参都是传地址(防止参数过大)。

以传值方式捕获所有父域变量,并不会真的传递所有,编译器只会传递用到的变量。

对编译器而言不是匿名的,它有名字,这个名字是编译器自己生成的------lambda_uuid。

UUID 是 Universally Unique Identifier(通用唯一识别码)的缩写。
