目录
[2.1 C++98传统的{}](#2.1 C++98传统的{})
[2.2 C++11中的{}](#2.2 C++11中的{})
[2.3 C++11中的std::initializer_list](#2.3 C++11中的std::initializer_list)
[3.1 左值和右值](#3.1 左值和右值)
[3.2 左值引用和右值引用](#3.2 左值引用和右值引用)
[3.3 引用延长生命周期](#3.3 引用延长生命周期)
[3.4 左值和右值的参数匹配](#3.4 左值和右值的参数匹配)
[3.5 右值引用和移动语义的使用场景](#3.5 右值引用和移动语义的使用场景)
[3.5.1 左值引用主要使用场景回顾](#3.5.1 左值引用主要使用场景回顾)
[3.5.2 移动构造和移动赋值](#3.5.2 移动构造和移动赋值)
[3.5.3 右值引用和移动语义解决传值返回问题](#3.5.3 右值引用和移动语义解决传值返回问题)
右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
[3.5.4 右值引用和移动语义在传参中的提效](#3.5.4 右值引用和移动语义在传参中的提效)
[4.1 lambda表达是语法](#4.1 lambda表达是语法)
[4.2 捕捉列表](#4.2 捕捉列表)
[4.3 lambda的应用](#4.3 lambda的应用)
[4.4 lambda的原理](#4.4 lambda的原理)
1.C++11的发展历史
C++11 是 C++ 的第⼆个主要版本,并且是从 C++98 起的最重要更新。它引⼊了⼤量更改,标准化了既有实践,并改进了对 C++ 程序员可用的抽象。在它最终由 ISO 在 2011 年 8 月12 日采纳前,人们曾使用名称"C++0x",因为它曾被期待在 2010 年之前发布。C++03 与 C++11 期间花了 8 年时间,故而这是迄今为止最长的版本间隔。从那时起,C++ 有规律地每 3 年更新⼀次。

如果还想学的话,C++11之后的C++20,C++14是一个小版本,C++17是一个中版本,C++20相对是一个大版本。
2.列表初始化
2.1 C++98传统的{}
C++98中一般数组和结构体可以用 { } 进行初始化
2.2 C++11中的{}
- C++11以后想统一初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做列表初始化。
- 内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造
- {}初始化的过程中,可以省略掉=
- C++11列表初始化的本意是想实现一个大统一的初始化方式,其次它在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很⽅便
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
#include<map>
using namespace std;
struct Point
{
int _x;
int _y;
};
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
private:
int _year;
int _month;
int _day;
};
// ⼀切皆可⽤列表初始化,且可以不加=
int main()
{
// C++98⽀持的
int a1[] = { 1, 2, 3, 4, 5 };
int a2[5] = { 0 };
Point p = { 1, 2 };
// C++11支持的
// 内置类型支持
int x1 = { 2 };
int x3{ 2 };
// ⾃定义类型⽀持
// 这⾥本质是⽤{ 2025, 1, 1}构造⼀个Date临时对象
// 临时对象再去拷贝构造d1,编译器优化后合⼆为⼀变成{ 2025, 1, 1}直接构造初始化d1
// 运⾏⼀下,我们可以验证上⾯的理论,发现是没调⽤拷⻉构造的
Date d1 = { 2025, 1, 1 };
// 这⾥d2引用的是{ 2024, 7, 25 }构造的临时对象,临时对象具有常性加const,更多的是用在传参的地方
const Date& d2 = { 2024, 7, 25 };
// 需要注意的是C++98⽀持单参数时类型转换,也可以不⽤{}
Date d3 = { 2025 };
Date d4 = 2025; //自定义类型转换成单参数的构造函数,构造一个日期类的临时对象,日期类的临时对象再去拷贝构造,编译器优化之后变为直接构造
map<string, string> dict; //map insert时参数应该是一个pair,构造一个pair有点麻烦,走隐式类型转换就可以
dict.insert({ "sort","排序" });
// 可以省略掉=
Point p1{ 1, 2 };
int x2{ 2 };
Date d6{ 2024, 7, 25 };
const Date& d7{ 2024, 7, 25 };
// 不支持,只有{}初始化,才能省略=
// Date d8 2025;
vector<Date> v;
v.push_back(d1);
v.push_back(Date(2025, 1, 1));
// 比起有名对象和匿名对象传参,这⾥{}更有性价⽐
v.push_back({ 2025, 1, 1 });
return 0;
}
2.3 C++11中的std::initializer_list
- 初始化的时候,原始的版本都是只支持n个value初始化,现在想用一个list自己写的,value都不同的话没办法支持,C++11就提供了这个功能
- C++11库中提出了⼀个std::initializer_list的类, auto il = { 10, 20, 30 }; // the type of il is an initializer_list ,这个类的本质是底层开⼀个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。
- std::initializer_list⽀持迭代器遍历。
- 容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的 {x1,x2,x3...} 进行初始化。STL中的容器⽀持任意多个值构成的 {x1,x2,x3...} 进⾏初始化,就是通过std::initializer_list的构造函数支持的。

代码:
同样也是支持迭代器和范围for

迭代器返回的是原生指针:需要指向数组,相当于自己开了一个临时的数组

cpp
int main()
{
auto il1 = { 10,20,30 };
initializer_list<int> il2 = { 10,2,13,5,9 };
//这个和int a1[] = { 1, 2, 3, 4, 5 };的底层原理有点像,底层会开一个数组,会将空间拷贝进去
//il2这个对象里面会有两个指针,指向这个对象的开始和结束
cout << sizeof(il1) << endl; //x86 8字节
//x64 16字节
int i = 1;
cout << &i << endl;
cout << il1.begin() << endl;
//指向数组的原生指针就是迭代器,可以看见上面打印的地址是非常相近的
return 0;
}
运行结果:

说明指针指向的空间都是存在栈上的,常量数组:{ 10,2,13,5,9 };不是存在常量区的
initializer_list 直接用的话没啥意义,配合着容器做容器的构造的时候的参数,容器的构造的时候传参就好传了。
vector<int> v1({ 1,2,3,4,5 }); //本质是直接构造
vector<int> v2 = { 1,2,3,4,5 }; //本质是隐式类型转换,可以传任意多个,是由initializer_list支持的,initializer_list 构造了个vector,再去拷贝构造
Date d1 = { 2025, 1, 1}; //由日期类直接支持的3个参数的构造支持的,走的是隐式类型转化,然后再拷贝构造,编译器优化之后是直接构造
cpp
//这里是pair对象的{}初始化和map的initializer_list构造结合到一起用了
map<string, string> dict = { {"sort","排序"},{"left","左边"} };
外层是initializer_list pair,里层是pair的转换,{"sort","排序"}走的是pair的构造
C++98是支持单参数的转换,C++11才支持的多参数的转换。
{ }优先识别为initializer_list,有时候不是匹配的是initializer_list,而是匹配对应的构造,所以花括号有两种匹配的规则,一种匹配的是对应的构造,一种匹配的是initializer_list的参数
3.右值引用和移动语义
左值和右值的概念早在C语言中提出来了。C++98就有引用的语法,将之前的引用叫做左值引用,引用左值,给左值取别名;C++11新增的右值引用的语法,给右值取别名。左值引用和右值引用都是取别名,C++11进一步区分是一个是给左值取别名,一个是给右值取别名。
3.1 左值和右值
- 左值是一个表示数据的表达式 (比如变量叫做变量表达式,对他解引用这些),一般是有持久状态,直接存储在内存当中,**最大特点:可以获取它的地址,**左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。用const修饰后的左值,不能拿给他赋值,但是可以取它的地址(也不能说:可以给他赋值的就是左值。const变量也是不能给他赋值的,但他也是左值)能不能取地址不能直接看出来,只能拿代码验证,左值一般可以修改,但是有一些也是不能修改的
- 右值也是一个数据表达式,要么是字面值常量,要么是表达式求值过程中创建的临时对象等,例如:a+b会产生临时对象,传值返回会产生临时对象,匿名对象通常就是右值,右值可以出现子啊赋值符号的右边,但是不能出现在赋值符号的左边,右值都是不能修改的,右值不能取它的地址(最大的区别)
- 值得⼀提的是,左值的英文简写为lvalue,右值的英文简写为rvalue。传统认为它们分别是left value、right value 的缩写。现代C++中,lvalue 被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象 ,而rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址, 例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别就是能否取地址。
cpp
int main()
{
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]都是常见的左值
int* p = new int(0);
int b = 1;
const int c = b;
// p b c 变量表达式
*p = 10;
string s("111111");
s[0] = 'x'; //s[0]返回的是第一个位置的值,是左值
cout << &c << endl;
cout << (void*)&s[0] << endl;
// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下几个10、x + y、fmin(x, y)、string("11111")都是常见的右值
10; //字面量常量
x + y; //临时对象
fmin(x, y); //函数调用,返回小的那个值,传值返回会生成一个临时对象,返回临时对象的拷贝
string("11111");
//cout << &10 << endl; //右值 -> 不能取地址
//cout << &(x+y) << endl; //右值 -> 不能取地址
//cout << &(fmin(x, y)) << endl; //右值 -> 不能取地址
//cout << &string("11111") << endl; //右值 -> 不能取地址
return 0;
}
将上面最后4行代码取消注释后,就会报错,因为是右值不能取地址

3.2 左值引用和右值引用
- Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引用,左值引用就是给左值取别名,第⼆个就是右值引用,同样的道理,右值引用就是给右值取别名。
- 左值引用不能直接引用右值,但是const左值引用可以引用右值
- 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
- template <class T> typename remove_reference<T>::type&& move (T&&arg);
- move是库里面的⼀个函数模板,本质内部是进行强制类型转换,当然他还涉及⼀些引用折叠的知识,这个我们后面会细讲。
- 需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值
- 语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的⻆度看下⾯代码中r1和rr1汇编层实现,底层都是⽤指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到⼀起去理解,互相佐证,这样反而是陷⼊迷途。
cpp
template <class _Ty>
remove_reference_t<_Ty>&& move(_Ty&& _Arg)
{ // forward _Arg as movable
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
using namespace std;
int main()
{
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]都是常见的左值
int* p = new int(0);
int b = 1;
const int c = b;
// p b c 变量表达式
*p = 10;
string s("111111");
s[0] = 'x'; //s[0]返回的是第一个位置的值,是左值
cout << &c << endl;
cout << (void*)&s[0] << endl;
// 左值引用给左值取别名
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
char& r5 = s[0];
// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下几个10、x + y、fmin(x, y)、string("11111")都是常?的右值
10; //字面量常量
x + y; //临时对象
fmin(x, y); //函数调用,返回小的那个值,传值返回会生成一个临时对象,返回临时对象的拷贝
string("11111");
//cout << &10 << endl; //右值 -> 不能取地址
//cout << &(x+y) << endl; //右值 -> 不能取地址
//cout << &(fmin(x, y)) << endl; //右值 -> 不能取地址
//cout << &string("11111") << endl; //右值 -> 不能取地址
// 右值引用给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");
// 左值引用不能直接引用右值,但是const左值引用可以引用右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
// 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
//move的底层就是强制类型转换
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s;
return 0;
}
3.3 引用延长生命周期
右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生存期,但这些对象无法被修改。
cpp
class AA
{
public:
~AA()
{
cout << "~AA()" << endl;
}
private:
int _a1 = 1;
int _a2 = 2;
};
int main()
{
//延长生命周期
AA&& ref1 = AA();
const AA& ref2 = AA();
AA(); //匿名对象,生命周期在当前行
AA aa1; //有名对象,出了作用域才调用析构函数
return 0;
}
匿名对象的生命周期在当前的一行,所以要调用析构函数

有名对象的声明周期是出了当前作用域才调用析构函数

const 引用和右值引用都会延长它的生命周期


按理来说临时对象和匿名对象都是不能修改的,但是右值引用它之后是可以修改的,右值引用的属性会转换为左值,是因为这条规则:**需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值,**所以下面代码中的 r3的属性是左值,是可以加等的,可以修改的
cpp
int main()
{
std::string s1 = "Test";
// std::string&& r1 = s1; // 错误:不能绑定到左值
const std::string& r2 = s1 + s1; //右值:传值返回,产生临时对象,生命周期都在当前这一行(匿名对象也一样) // OK:到 const 的左值引用延长生存期
// r2 += "Test"; // 错误:不能通过到 const 的引用修改
std::string&& r3 = s1 + s1; // OK:右值引用延长生存期 右值引用变量变量表达式的属性是左值,所以代码中的 r3的属性是左值,是可以加等的,可以修改的
r3 += "Test"; // OK:能通过到非 const 的引用修改
std::cout << r3 << '\n';
return 0;
}
3.4 左值和右值的参数匹配
const 左值引用他匹配的范围是比较宽泛的,既可以匹配左值又可以匹配右值,在C++11中,函数调用和参数匹配中如果只有const左值引用,那么const左值匹配和右值匹配都可以,如果同时存在,就会选择最匹配的。
- C++98中,我们实现一个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
- C++11以后,分别重载左值引用,const左值引用,右值引用作为形参的f函数,那么实参是左值会匹配f(左值引用),实参是const左值会匹配f(const左值引用),实参是右值会匹配f(右值引用)。
- 右值引用变量在用于表达式时属性是左值。
cpp
void f(int& x)
{
std::cout << "左值引⽤重载 f(" << x << ")\n";
}
void f(const int& x)
{
std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}
void f(int&& x)
{
std::cout << "右值引⽤重载 f(" << x << ")\n";
}
三个同时存在的话就会构成函数重载,并且函数调用的时候还有一个参数匹配的更匹配的规则,传参的时候函数调用会调用最匹配的
cpp
int main()
{
int i = 1;
const int ci = 2;
f(i); //左值,匹配左值引用 ,调用 f(int&)
f(ci); //const左值 匹配const左值引用,调用 f(const int&)
f(3); //右值,匹配右值引用,调用 f(int&&),如果没有 f(int&&) 重载则会调用 f(const int&)
f(std::move(i)); //左值被move之后匹配右值引用,调用 f(int&&)
int&& x = 1;
f(x); // x是右值引用,将右值引用拿去传参匹配左值引用,因为所有的变量表达式都是左值,调用 f(int& x)
// x单独拿出来就是一个引用的变量表达式,右值引用引用你的,但是x自身的属性就是左值
f(std::move(x)); // 调用 f(int&& x)
int& j = i; //左值引用传过去匹配的还是左值引用
f(j);
return 0;
}
右值引用int&& x = 1; f(x); 传过去和左值引用 int& j = i; f(j); 传过去匹配的都是左值引用
因为变量表达式的属性都是左值。
3.5 右值引用和移动语义的使用场景
3.5.1 左值引用主要使用场景回顾
左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决了大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如addString和generate函数,C++98中的解决方案只能是被迫使用输出型参数解决。那么C++11以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束时这个对象就析构销毁了,右值引用返回也无法改变对象已经销毁的事实。

3.5.2 移动构造和移动赋值
- 移动构造函数是一种构造函数,类似拷贝构造函数(左值引用),移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他的参数,额外的参数必须有缺省值。
- 移动赋值是一个赋值运算符的重载,他跟拷贝赋值(左值引用)构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
- 当拷贝构造、移动构造、拷贝赋值、移动赋值都存在的时候,传左值或右值,此时编译器就会匹配最合适的,第一、参数匹配的时候会将左值右值区分开来;第二、左值通常是有存储空间的,能修改的那些值(除了const修饰的那些值),右值通常是字面量,自定义类型一般没有字面量的概念,内置类型才会有对应的字面量,那么自定义类型的右值通常包括两类:一类就是临时对象,一类就是匿名对象 ;临时对象和匿名对象的特点就是他们的生命周期快要结束了,临时对象和匿名对象他们的生命周期都在当前这一行的。右值引用引用的都是一些匿名对象、临时对象,拷贝构造拷贝的那些值都是一些长期存在的值。右值可以老老实实拷贝,同样也可以将他的资源直接移动过来,所以叫移动构造。**移动构造的特点是可以把那些值给区分出来,如果是右值引用的话就直接不进行拷贝构造了,直接掠夺资源,移动走。**此时移动构造的代价就很低。
- 对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,它的本质是要"窃取"引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样取拷贝资源,从而提高效率。下面的ysy::string样例实现了移动构造和移动赋值。
cpp
#include<assert.h>
namespace ysy
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)-构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) --拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
//移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) --拷贝赋值" << endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
//移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
cout << "~string() -- 析构" << endl;
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity *
2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}
int main()
{
ysy::string s1("1111111"); //左值
ysy::string s2 = s1; //拷贝构造
ysy::string s3 = ysy::string("2222222"); //移动构造
return 0;
}
运行结果:

加上代码:


此时 s1 的资源转走了,全部转给了 s4
这里的ysy::string s3 = ysy::string("2222222")在vs下不是很好演示,Linux中可以将优化给关掉,可以看见:

跟vs下是一致的:(这里也是因为编译器优化的原因导致的)

关闭优化的指令:


cpp
namespace ysy
{
string addStrings(string num1, string num2)
{
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
cout << "******************************" << endl;
return str;
}
}
// 场景1
int main()
{
ysy::string ret = ysy::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
// 场景2
int main()
{
ysy::string ret;
ret = ysy::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
同样也是在Linux下演示:
右值对象构造,只有拷贝构造,没有移动构造的场景(关闭优化)

右值对象构造,有拷贝构造,也有移动构造的场景(关闭优化)

为什么移动构造之后还要析构?
移动构造将资源转走之后,临时对象是还存在的,移动构造是将我的资源转移给你,你的资源转移给我,我是一个构造的对象没有资源,所以析构的就是空,虽然还是有那么对析构,但是有些析构的是空


移动构造的价值:(右值引用是如何起作用的)

右值引用是借助移动构造,移动构造又是和拷贝构造构成重载去区分出那些右值对象,然后转移它的资源,因为传值返回要进行对象的构造,就要调用对应的构造,要么是拷贝构造,要么是移动构造。自己控制的话,将即将要销毁的对象可以对他进行转移他的资源,如果是左值,用move
右值对象赋值,只有拷贝构造,没有移动构造和移动赋值的场景(关闭优化)

右值对象赋值,既有拷贝构造,也有移动构造和移动赋值的场景(关闭优化)

3.5.3 右值引用和移动语义解决传值返回问题
右值对象构造,只有拷贝构造,没有移动构造的场景
- 图1展示了vs2019 debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造,右边为编译器优化的场景下连续步骤中的拷贝合⼆为⼀变为⼀次拷贝构造。
- 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为⼀,变为直接构造。变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所示。
- linux下可以将下⾯代码拷贝到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elide-constructors 的方式关闭构造优化,运行结果可以看到图1左边没有优化的两次拷贝。

图1
右值对象构造,有拷贝构造,也有移动构造的场景
- 图2展示了vs2019 debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷贝合⼆为⼀变为⼀次移动构造。
- 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为⼀,变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的角度理解,如图3所示。
- linux下可以将下⾯代码拷贝到test.cpp文件,编译时⽤ g++ test.cpp -fno-elide-constructors 的方式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次移动。

图2

图3
右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
- 图4左边展示了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,⼀次拷贝构造,⼀次拷贝赋值。
- 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。

图4
右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
- 图5左边展示了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,⼀次移动构造,⼀次移动赋值。
- 需要注意的是在vs2019的release和vs2022的debug和release,下面代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层角度用指针实现。运行结果的角度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。

图5
总结:
- 传值返回的场景,没有移动构造没有移动赋值,依赖编译器的优化,拷贝构造接受效率很好,赋值接收有一定的拷贝代价。
- 传值返回的场景,既有移动构造也有移动赋值,不依赖编译器的优化,无论如何拷贝接收还是赋值接收,代价都很低。(移动构造和移动赋值是将事情把握在我们自己手中,而不是将所有的事情都交给编译器,这是不靠谱的)


3.5.4 右值引用和移动语义在传参中的提效
- 查看STL⽂档我们发现C++11以后容器的push和insert系列的接⼝否增加的右值引⽤版本
- 当实参是⼀个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷⻉到容器空间中的对象
- 当实参是⼀个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上
- 把我们之前模拟实现的bit::list拷贝过来,支持右值引用参数版本的push_back和insert
- 其实这⾥还有⼀个emplace系列的接⼝,但是这个涉及可变参数模板,我们需要把可变参数模板讲解以后再讲解emplace系列的接口。


除了左值引用版本,还有右值引用版本。参数可以用左值引用也可以用右值引用,如果是左值走第一个,如果是右值走第二个。
cpp
int main()
{
std::list<ysy::string> lt;
ysy::string s1("111111111111111111111");
lt.push_back(s1);
cout << "*************************" << endl;
lt.push_back(ysy::string("22222222222222222222222222222"));
cout << "*************************" << endl;
lt.push_back("3333333333333333333333333333");
cout << "*************************" << endl;
lt.push_back(move(s1));
cout << "*************************" << endl;
return 0;
}
运行结果:


传右值对象效率反而高,左值对象反而效率不行。
换成自己实现的list:
cpp
//list.h
#pragma once
#include<assert.h>
namespace ysy
{
template<class T>
struct list_node
{
list_node<T>* _next;
list_node<T>* _prev;
T _data;
list_node(const T& x = T())
:_next(nullptr)
, _prev(nullptr)
, _data(x)
{
}
list_node(T&& x)
:_next(nullptr)
, _prev(nullptr)
, _data(move(x))
{
}
};
template<class T, class Ref, class Ptr>
struct list_iterator
{
typedef list_node<T> Node;
typedef list_iterator<T, Ref, Ptr> Self;
Node* _node;
list_iterator(Node* node)
:_node(node)
{
}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self operator++(int)
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
Self operator--(int)
{
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
};
/*template<class T>
struct list_iterator
{
typedef list_node<T> Node;
Node* _node;
list_iterator(Node* node)
:_node(node)
{}
T& operator*()
{
return _node->_data;
}
T* operator->()
{
return &_node->_data;
}
list_iterator<T>& operator++()
{
_node = _node->_next;
return *this;
}
bool operator!=(const list_iterator<T>& it)
{
return _node != it._node;
}
bool operator==(const list_iterator<T>& it)
{
return _node == it._node;
}
};
template<class T>
struct list_const_iterator
{
typedef list_node<T> Node;
Node* _node;
list_const_iterator(Node* node)
:_node(node)
{}
const T& operator*()
{
return _node->_data;
}
const T* operator->()
{
return &_node->_data;
}
list_const_iterator<T>& operator++()
{
_node = _node->_next;
return *this;
}
bool operator!=(const list_const_iterator<T>& it)
{
return _node != it._node;
}
bool operator==(const list_const_iterator<T>& it)
{
return _node == it._node;
}
};*/
template<class T>
class list
{
typedef list_node<T> Node;
public:
//typedef Node* iterator;
/* typedef list_iterator<T> iterator;
typedef list_const_iterator<T> const_iterator;*/
typedef list_iterator<T, T&, T*> iterator;
typedef list_iterator<T, const T&, const T*> const_iterator;
iterator begin()
{
//return iterator(_head->_next);
iterator it(_head->_next);
return it;
}
iterator end()
{
return iterator(_head);
}
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
list()
{
empty_init();
}
list(initializer_list<T> lt)
{
empty_init();
for (auto& e : lt)
{
push_back(e);
}
}
// lt2(lt1)
list(const list<T>& lt)
{
empty_init();
for (auto& e : lt)
{
push_back(e);
}
}
// lt1 = lt3
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
size_t size() const
{
return _size;
}
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(), move(x));
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_front()
{
erase(begin());
}
void pop_back()
{
erase(--end());
}
void 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;
++_size;
}
void insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(move(x));
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* nextNode = cur->_next;
Node* prevNode = cur->_prev;
prevNode->_next = nextNode;
nextNode->_prev = prevNode;
delete cur;
--_size;
return iterator(nextNode);
}
private:
Node* _head;
size_t _size;
};
}
cpp
int main()
{
ysy::list<ysy::string> lt;
ysy::string s1("111111111111111111111");
lt.push_back(s1); //左值
cout << "*************************" << endl;
lt.push_back(ysy::string("22222222222222222222222222222")); //右值
cout << "*************************" << endl;
lt.push_back("3333333333333333333333333333"); //右值,常量字符串,走隐式类型转换,构造一个string
cout << "*************************" << endl;
lt.push_back(move(s1)); //将左值进行move之后就是右值
cout << "*************************" << endl;
return 0;
}
push_back()增加,push_back()调insert(),insert()也要增加,对应的结点申请也要写一份:

注意:右值引用的属性是左值,所以得move()
运行结果:

4.lambda
4.1 lambda表达是语法
- lambda表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda表达式语法使用层而言,没有类型(其实就是一个匿名函数对象),所以我们一般都是用auto或者模板参数定义的对象去接受lambda对象。
- lambda表达式的格式:[capture - list] (parameters) ->return type { function body }
- [capture - list]:捕捉列表 ,该列表总是出现在lambda函数的开始位置,编译器根据 []来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉。捕捉列表为空也不能省略。
- (parameters):参数列表,与普通函数的参数列表功能相似,如果不需要参数传递,则可以来连同()一起省略。
- -> return type:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- { function body}:函数体,函数体内的实现跟普通函数完全类似,在该函数体内,出来可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。

所以推导出来,add1也是一个对象。

运行结果:

调试的时候都是会跳回去的:

cpp
int main()
{
auto add1 = [](int x, int y)->int {return x + y; };
cout << add1(1, 2) << endl;
// 1、捕捉为空也不能省略
// 2、参数为空可以省略
// 3、返回值可以省略,可以通过返回对象自动推导
// 4、函数体不能省略
auto func1 = []
{
cout << "hello ysy" << endl;
return 7;
};
int x = func1();
cout << x << endl;
int a = 3, b = 66;
auto swap1 = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap1(a, b);
cout << a << ":" << b << endl;
return 0;
}
运行结果:

4.2 捕捉列表
- lambda 表达式中默认只能用 lambda 函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉
- 第一种捕捉方式是在捕捉列表中显示的传值捕捉 和传引用捕捉 ,捕捉的多个变量用逗号分割。[x,y,&z] 表示x和y值捕捉,z引用捕捉。(传值捕捉不能修改,引用捕捉可以修改)
- 第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个**=表示隐式值捕捉** ,在捕捉列表写一个**&表示隐式引用捕捉**,这样我们lambda表达式中用了哪些变量,编译器就会自动捕捉那些变量。
- 第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=,&x]表示其它变量隐式值捕捉,x引用捕捉;[&,x,y] 表示其它变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是&,或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉
- lambda表达式如果在函数局部域中,它可以捕捉lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda表达式中可以直接使用。这也意味着lambda表达式如果定义在全局位置,捕捉列表必须为空。
- 默认情况下,lambda捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就是说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
cpp
int main()
{
// 只能用当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3;
auto func1 = [a, &b]
{
// 值捕捉的变量不能修改,引用捕捉的变量可以修改
//a++;
b++;
int ret = a + b;
return ret;
};
cout << func1() << endl;
// 隐式值捕捉
// 用了哪些变量就捕捉哪些变量
auto func2 = [=]
{
int ret = a + b + c; // 0+2+2=4
return ret;
};
cout << func2() << endl;
return 0;
}
运行结果:(引用捕捉是会改变值的):

隐式的引用捕捉:
cpp
// 隐式引用捕捉
// 用了哪些变量就捕捉哪些变量
auto func3 = [&]
{
a++;
c++;
d++;
};
func3();
cout << a << " " << b << " " << c << " " << d << endl;

cpp
// 混合捕捉1
auto func4 = [&, a, b] //&必须放在最前面;ab值捕捉,其它的引用捕捉
{
//a++;
//b++;
c++;
d++;
return a + b + c + d;
};
func4();
cout << a << " " << b << " " << c << " " << d << endl;

cpp
// 局部的静态和全局变量不能捕捉,局部的静态和全局变量直接就可以用,也不需要捕捉
static int m = 0;
auto func6 = []
{
int ret = x + m;
return ret;
};
cpp
int x = 0;
// 捕捉列表必须为空,因为全局变量不用捕捉就可以用,没有可被捕捉的变量
auto func1 = []()
{
x++;
};
传值捕捉本质是一种拷贝,并且被const修饰了, mutable相当于去掉const属性,可以修改了,但是修改了不会影响外面被捕捉的值,因为是一种拷贝,但是一般不用 mutable。
cpp
// 传值捕捉本质是一种拷贝,并且被const修饰了
// mutable相当于去掉const属性,可以修改了
// 但是修改了不会影响外面被捕捉的值,因为是一种拷贝
auto func7 = [=]()mutable
{
a++;
b++;
c++;
d++;
return a + b + c + d;
};
cout << func7() << endl;
cout << a << " " << b << " " << c << " " << d << endl;
虽然里面改变了,但是运行结果是没变的:

4.3 lambda的应用
- 在了解 lambda 表达式之前,我们使用的可调用的对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用 lambda去定义可调用对象,既简单又方便。
- lambda在很多其它地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等,lambda的应用还是很广泛的。
一般对数值进行排序是没有意义的,实践中都是对一堆结构数组进行排序:
cpp
// 都是可调用对象
// 函数指针 -- 通过函数指针来调用函数,但是函数指针类型太难写了
// 仿函数(类,写在全局)
// lambda
#include<algorithm>
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 } };
// 类似这样的场景,我们实现仿函数对象或者函数指针支持商品中
// 不同项的比较,相对还是比较麻烦的,那么这里lambda就很好用了
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
return 0;
}


lambda的实现:
cpp
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3}, { "菠萝", 1.5, 4 } };
// 类似这样的场景,我们实现仿函数对象或者函数指针支持商品中
// 不同项的比较,相对还是比较麻烦的,那么这里lambda就很好用了
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
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;});
return 0;
}
价格的升序:

价格的降序:

销量的升序:

销量的降序:

4.4 lambda的原理
- lambda的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有 lambda 和范围for这样的东西。**范围for底层是迭代器,而lambda底层是仿函数对象,**也就说我们写了一个 lambda以后,编译器会生成一个对应的仿函数的类。
- 仿函数的类名是编译按一定规则生成的,保证不同的 lambda ⽣成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是⽣成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。
- 上面的原理,我们可以透过汇编层了解⼀下,下面第二段汇编层代码印证了上面的原理。
cpp
//仿函数
class Rate
{
public:
Rate(double rate)
: _rate(rate)
{
}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};


调用operator(),类型是编译器生成的,不同的lambda的类型名是不一样的,lambda在前面就构造了:

编译器在看到lambda的时候在底层本质上生成了一个类。
语法层是拿不到lambda类型的,只能让他自己推导,编译器知道,我们不知道。



