C++11列表初始化与移动语义
代码语言:javascriptAI代码解释。
C++11中的{}
- C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化
- c++11开始,自定义类型支持用初始化列表,c++98只有内置类型支持初始化列表
代码语言:javascript
AI代码解释
#include<iostream>
#include<vector>
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 };
// ⾃定义类型⽀持
// 这⾥本质是⽤{ 2025, 1, 1}构造⼀个Date临时对象
// 临时对象再去拷⻉构造d1,编译器优化后合⼆为⼀变成{ 2025, 1, 1}直接构造初始化
Date d1 = { 2025, 1, 1};
//C++98⽀持单参数时类型转换,也可以不⽤{}
Date d3 = { 2025};//c++11
Date d4 = 2025;//c++98
//可以省略掉=
Point p1 { 1, 2 };
int x2 { 2 };
Date d6 { 2024, 7, 25 };
const Date& d7 { 2024, 7, 25 };
//只有初始化列表才支持省略=
Date d8 2025//会报错
}
C++11中的std::initializer_list
- 上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如⼀个vector对象,我想用N个值去构造初始化,那么我们得实现很多个构造函数才能支持:vector v1 = {1,2,3};vector v2 = {1,2,3,4,5};
- C++11库中提出了⼀个std::initializerlist的类, auto il = { 10, 20, 30 }; // the type of il is an initializerlist ,这个类的本质是底层开⼀个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。
代码语言:javascript
AI代码解释
vector<int> v1={1,2,3,4};
initializer_list<int>l1={10,20,30};//本质是底层在栈上开一个数组,

代码语言:javascript
AI代码解释
//这里在语义上表示构造+拷贝构造+优化,,,但编译器会优化成直接构造
//本质也可以理解为隐式类型转换
vector<int> v1={1,2,3,4};
vector<int> v2{1,2,3,4};
//这里在语义上表示直接进行构造
vector<int> v3({1,2,3,4});//调用initializer_list进行构造
//上述两种方式在语义上表示的意思不同,但最后的结果是相同的
右值引用和移动语义
- Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。
- 左值引用不能直接引用右值,但是const左值引用可以引用右值
- 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
- move是库里面的一个函数模板,本质内部是进行强制类型转换,当然他还涉及⼀些引用折叠的知识
- 是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值
代码语言:javascript
AI代码解释
double x = 1.1, y = 2.2;
const int& rx1 = 10;
const double& rx2 = x + y;
int* p = new int(0);
int b = 1;
string s("111111");
int&& rr1 = move(b);
int*&& rr2 = move(p);
string&& rr3 = move(s);
string&& rr4 = (string&&)s;//move本质是进行强转
int& tt1 = rr1;//用左值引用来引用右值引用表达式
左值引用与右值引用在底层其实就是指针
引用延长生命周期
右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生存期,但这些对象无法被修改。
如果想用引用来延长被调用的函数内部局部变量的生命周期,这是不被允许的。第一点:引用不会改变变量的存储位置。第二点:局部变量是创建在函数栈帧中的,当函数调用结束栈帧销毁,局部变量也会随之销毁。
代码语言:javascript
AI代码解释
string s1 = "test";
//string&& r1 = s1;//右值引用无法引用左值
const string& r2 = s1 + s1;
//r2 += s1;//const左值可以引用右值,但无法进行修改
string&& r3 = s1 + s1;
r3 += s1;
cout << r3 << endl;
左值和右值的参数匹配
- C++98中,我们实现⼀个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
- C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会 匹配f(左值引用),实参是const左值会匹配f(const左值引用),实参是右值会匹配f(右值引用)。
代码语言:javascript
AI代码解释
void func(int& x)
{
cout << "左值引用" << x <<endl;
}
void func(const int& x)
{
cout << "const左值引用" << x << endl;
}
void func(int&& x)
{
cout << "右值引用" << x<<endl;
}
int main()
{
int i = 1;
const int ci = 2;
func(i);
func(ci);
func(3);
func(move(i));
int&& x = 1;
func(x);
func(move(x));
return 0;
}
左值引用与右值引用最终目的是减少拷贝、提高效率。 左值引用还可以修改参数或者返回值,方便使用
左值引用的不足:
在部分函数场景,只能传值返回,不能传引用返回。比如:当前函数的局部对象,出了当前函数的作用域就销毁
移动构造和移动赋值
- 移动构造函数是一种构造函数,类似拷贝构造函数,要求第一个参数是该类类型的引用,不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
- 移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
- 对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引用的类型,他的本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。下面的bit::string样例实现了移动构造和移动赋值,我们需要结合场景理解。
移动构造是进行指针交换,其本质是“掠夺资源”。被掠夺的右值的指针则指向”空“
所以一个左值不能轻易的去move,因为这会导致左值的资源被掠夺

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

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

如果是在2019的release或者2022的环境下,则只进行一次构造。在2019的release或者2022的环境下str直接变成了ret的引用。

如果想看未优化的场景,在Linux下通过:g++ test.cpp -fno-elide-constructors关闭构造优化来观察。
移动赋值
- 移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
- 移动赋值也是对资源进行掠夺
右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
- 图4左边展示了vs2019debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次拷贝构造,一次拷贝赋值。
- 需要注意的是在vs2019的release和vs2022的debug和release,下面代码会进一步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。

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


左值调用拷贝构造,因为左值数据不能轻易改动,可能会影响到后面的程序。 右值调用移动构造,因为右值生命周期极短,比起拷贝构造,用移动构造付出的代价更小,并且效率更高
右值引用和移动语义在传参中的提效
- 查看STL文档我们发现C++11以后容器的push和insert系列的接口否增加的右值引用版本
- 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象
- 当实参是一个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上
- 把我们之前模拟实现的bit::list拷贝过来,支持右值引用参数版本的push_back和insert
- 其实这里还有一个emplace系列的接口,但是这个涉及可变参数模板,我们需要把可变参数模板讲解以后再讲解emplace系列的接口。
更多推荐

所有评论(0)