C++11中的列表初始化,右值引用与移动语义
C++11是C++11的一个重要的大更新版本,其中增强了许许多多的特性的语法规则,提高了代码的运行效率,当然也增加了学习成本,我将分几小章分别介绍C++11中常用的语法规则。首选今天要介绍的就是列表初始化,右值引用和移动语义。
列表初始化
{}初始化
在c++11之前,{}可以用于初始化数组或者结构体。在C++11中,c++委员会想让{}有更多的功能,让{}能够初始化万物,因此,{}初始化也叫做列表初始化。{}初始化本质上是先生成临时对象,再进行拷贝构造,然后销毁临时对象。如果是经过编译器优化的,甚至会把临时对象给省略掉。我们也可以省略掉=,让整体代码更加整洁,但是{}是绝对不能省略。
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临时对象
Date d1 = { 2025, 1, 1};
// 这⾥d2引⽤的是{ 2024, 7, 25 }构造的临时对象
const Date& d2 = { 2024, 7, 25 };
// 需要注意的是C++98⽀持单参数时类型转换,也可以不⽤{}
Date d3 = { 2025};
Date d4 = 2025;
// 可以省略掉=
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;
initializer_list
在c++11中引入了std::initializer_list类,其本质底层是一个数组,有两个指针分别指向这个数组的开头和结尾,值得注意的是,这个数组开在了栈,而不是堆。这个类的本质作用是一个中间数组,是为了支持多值初始化。
vector<int> v1 ={1,2,3};
vector<int> v2 = {1,2,3,4,5};
如以上代码,如果没有std::initializer_list类,那就需要写两个构造函数,麻烦复杂。如果有std::initializer_list,那么只要写一个支持std::initializer_list的构造函数即可,函数内遍历std::initializer_list的底层数组,再将数据插入vector既可。
右值引用和移动语义
在c++98版本就已经有引用的语法存在了,但是当时只有左值引用,在C++11中多了个右值引用,这个语法非常有用,极大的提高了C++的运行效率。接下来就是右值引用的部分内容。
左值右值的差别
左值右值的差别最主要在于能不能取地址。左值一般指的是一个变量或者是地址,其在内存中有实际空间,右值一般指的是一个常量或者是临时变量,其内存没有常态,因此没办法取地址。
#include<iostream>
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 = 10;
string s("111111");
s[0] = 'x';
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;
}
左值引用与右值引用
左值引用我们已经很熟悉了,就是给左值取别名,于是右值也是同理了,给右值取别名。
Type& r1 = x; Type&& rr1 = y;
左值引用不能直接引用右值,但是可以通过const来引用右值;
右值引用不能直接引用左值,但是可以通过move()函数来引用左值;
这里有一个值得注意的点,现在rr1是左值属性,不再能被右值引用所引用,如果想要右值引用他,则需要使用move()函数进行强制类型转换。
延长变量生命周期
对于临时变量或者是常量,一般用完即似,如果我们想要延长这些数据的生命,并且不另外定义变量,那么需要使用一些手段。第一个手段便是const Type&,用const来延长生命周期;第二个手段是Type && 右值引用了。当然,利用这两种手段延长寿命的变量都不能改变数据。
右值引用和移动语义的应用场景
左值引用的使用场景
左值引用的主要用处便是左值引用传参和左值引用传返回值,当然,引用的返回值必须在函数生命周期结束后依然存在,不能返回局部非静态变量。一般来说,我们返回的数值会被const修饰,防止更改源对象。如果想要返回局部非静态变量,就会面临大量构造的问题,效率低。编译器本身会进行优化,但是指标不治本,仅仅使用右值引用做返回值也无法解决问题,因为局部变量被销毁这个事实在函数结束以后依然会发生。此时,移动语义的价值就出现了。
#include <vector>class DataHolder {
private:std::vector<int> _largeData; // 一个存储大量数据的成员public:// 构造函数,初始化大数据DataHolder() : _largeData(1000000, 1) {} // 初始化一个包含100万个1的vector// 方式一:传值返回 - 低效// 这会返回 _largeData 的一个副本,涉及大量数据的拷贝,成本高昂std::vector<int> getLargeDataByValue() const {return _largeData;}// 方式二:常量左值引用返回 - 高效// 这只是返回 _largeData 的别名(只读视图),没有任何拷贝!const std::vector<int>& getLargeDataByReference() const {return _largeData;}
};int main() {DataHolder holder;// 情况1:传值返回std::vector<int> dataCopy = holder.getLargeDataByValue(); // 发生一次深拷贝!// 此时内存中存在两份完整的大数据:holder._largeData 和 dataCopy// 情况2:传引用返回const std::vector<int>& dataRef = holder.getLargeDataByReference(); // 无拷贝!仅是别名// dataRef 只是 holder._largeData 的一个“视图”,操作的是同一份数据(只读)return 0;
}
移动构造和移动赋值
移动构造是一种构造函数,类似于拷贝构造,但是要求参数必须是右值引用,如果还有其他参数,则必须设置为缺省值,移动构造本身是一种掠夺,他会掠夺右值对象的资源,然后与其进行交换,此时右值对象就是原对象的资源,即使释放,也无关痛痒。移动赋值也是同理,同样是掠夺资源。
string(string&& s)
{cout << "string(string&& s) -- 移动构造" << endl;swap(s);
}
移动构造是如何解决返回值的问题
int main()
{
bit::string ret = bit::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
} /
/ 场景2
int main()
{
bit::string ret;
ret = bit::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
返回值问题的本质是返回局部变量时,会生成临时变量保存局部变量的值,然后ret再拷贝构造生成最后的值。按照这样的步骤,会生成三个变量,效率太低。在没有移动构造之前,编译器有各种各样的方法来解决这个问题,比如直接不生成临时变量,又或者直接把ret和返回值合二为一,但这都是编译器自己努力的结果,较老的编译器是没有这些功能的。但是在C++11引入移动构造后,效率就大大提升了。比如,当遇到类似的问题的时候,此时,会先移动构造临时变量,掠夺原有的返回值的资源,然后ret再掠夺临时变量的资源。这样就比如要造房子,拷贝构造就比如把这栋房子按照要求重头造一遍,很费劲;但是移动构造就是偷钥匙,直接把想要的房子改到自己名下,效率明显是大幅提升了。而编译器在此的优化也只是省略了临时变量而已。
引用变量
C++中不能直接定义引用的引用,但是可以通过typedef和模版参数可以构成引用的引用。如下图代码,Function函数模版的T&&参数便是万能引用,当穿过来的数据是一个int&&时候,那么t就是int&&,T是int;如果传过的是int& ,那么t是左值引用类型,T是int&类型。
template<class T>
void Function(T&& t)
{
int a = 0;
T x = a;
//x++;
cout << &a << endl;
cout << &x << endl << endl;
} int main()
{
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&
t)
// 所以Function内部会编译报错,x不能++
Function(b); // const 左值std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&
t)
// 所以Function内部会编译报错,x不能++
Function(std::move(b)); // const 右值
return 0;
}
完美转发
我们之前提到了,当int&& rr1 = 10;时,rr1此时是一个左值。在结合前面引用变量的准则,就会导致我们在一个万能引用模版参数函数f1里面,如果再调用一个万能引用参数f2,如果直接传第f1的参数给f2,那么f2接收到的是一个左值,不符合我们预期。此时forward<T>(t)可以自动判断其类型,如果t是T类型呢,那么就会变成T&&,如果T&,那就保留。
template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvalue
return static_cast<_Ty&&>(_Arg);
}
void Fun(int& x) { cout << "左值引⽤" << endl; }
void Fun(const int& x) { cout << "const 左值引⽤" << endl; }
void Fun(int&& x) { cout << "右值引⽤" << endl; }
void Fun(const int&& x) { cout << "const 右值引⽤" << endl; }
template<class T>
void Function(T&& t)
{
Fun(t);
Fun(forward<T>(t));
}
int main()
{
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&
t)
Function(b); // const 左值
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&
t)
Function(std::move(b)); // const 右值
return 0;
}