一、列表初始化
在C++98中我们可以使用{}来初始化数组和结构体,比如
cpp
struct Date
{
int _year;
int _month;
int _day;
};
int main()
{
//初始化数组
int a[] = {1,2,3,4,5};
//初始化结构体
Date date = { 2024,10,29 };
return 0;
}
在C++11中可以使用{}来初始化一切对象,用{}初始化也叫做列表初始化。
在使用{}初始化的过程中,可以把=省略掉。
内置类型和自定义类型都支持使用{}列表初始化的方式,自定义类型的本质是类型转化,在中间会产生临时变量。
{}这种初始化的方式的价值在于在给容器的接口传入多参数构造的对象时会有名参数和匿名参数都要方便。
cpp
#include<iostream>
#include<vector>
using namespace std;
struct Date
{
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{
cout << "Date的构造" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date的拷贝构造" << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
//c++11可以不写=
int x{ 1 };
//这里的过程应该是用{ 2024,10,29 }构造一个临时对象
//这个临时对象去拷贝构造d1
//但是编译器会直接把这个临时对象省略掉 直接构造d1这个对象
Date d1{ 2024,10,29 };
//下面这个例子就能证明出{}构造出来的是一个临时对象
// 如果不加const修饰那么编译时就会报错
// 这里报错的原因是以为{}构造出来的是一个临时对象 临时对象具有常性 导致了这里的参数类型不匹配的问题
//Date& d2 = { 2024,10,29 };
//这里就不会发生报错
//因为这里d2引用的是{}构造出来的临时对象 临时对象具有常性
const Date& d2 = { 2024,10,29 };
vector<Date> v;
v.push_back(d1);
v.push_back(Date{ 2024,10,29 });
//这里的编译器已经知道了v里存储的类型是Date
//所以传入多参数时 会直接用{}初始化出一个临时对象
v.push_back({ 2024,10,29 });
return 0;
}
二、右值引用和移动语义
一、左值和右值
左值是一个表示数据的表达式,一般来说是有持久状态的,它的存储位置在内存中,我们可以获取它的地址,左值即可以出现在赋值符号的左边,也可以出现在赋值符号的右边。在定义一个左值时如果用const修饰,虽然我们不能修改它的内容,但是我们还是可以取到它的地址。
右值也是一个表示数据的表达式,但是右值要么是一些字面值常量,要么就是一些过程中创建的临时对象,右值只能出现在赋值符号的右边,同时右值不能取地址。
所以左值和右值的最本质区别就在于,我们能不能取到它的地址。
cpp
int main()
{
//以下都是左值
int a = 1;
const int b = 2;
int c = a + b;
string s("1111111");
//都能够正常的取地址
&a;&b;&c;&s;
//a+b的结果存在一个临时对象里 所以是右值
//&(a + b);
//10是字面值常量 是右值
//&10;
//匿名对象也是一个临时对象 也是右值
//cout<<&string("111111");
return 0;
}
二、左值引用和右值引用
Type& a = x;Type&& b =y;第一个就是我们之前一直在使用的引用,就是我们所说的左值引用;第二个用&&修饰的引用就是右值引用。右值引用和左值引用一样,都是取别名,右值引用就是给右值取别名。
左值引用不能直接引用右值,但是const左值引用可以引用右值。
右值引用也不能直接引用左值,但是右值引用可以引用move以后的左值。
这里需要注意的是,虽然右值引用引用的是右值或者是move以后的左值,但是右值引用本身具有的属性是左值。
cpp
int main()
{
//
int a = 10;
const int b = 5;
string s("111111");
int* p = new int(0);
double x = 1.1, y = 2.2;
//下面都是正常的左值引用引用左值 给左值取别名
int& r1 = a;
const int& r2 = b;
string& r3 = s;
int*& r4 = p;
double& r5 = x;
double& r6 = y;
//右值引用给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
string&& rr3 = string("11111");
//左值引用不能直接引用右值 但是用const修饰左值引用以后就可以引用右值了
const int& rx1 = 10;
const double& rx2 = x + y;
string rx3 = string("11111111");
//右值引用也不能直接引用左值 但是可以引用move以后的左值
int&& rrx1 = move(a);
const int&& rrx2 = move(b);
double&& rrx3 = move(x);
double&& rrx4 = move(y);
//右值引用本身的属性是左值
return 0;
}
三、延长生命周期
右值引用和const左值引用都能延长临时对象的声明周期,但是const修饰的左值引用的对象无法修改。同时这里指的延长生命周期也只是在当前的函数栈帧里延长生命周期,当这个函数栈帧被销毁时,这些对象也还是会被销毁的。
四、左值和右值的参数匹配
在之前我们在传参的时候想要减少拷贝我们都会实现一个用const左值引用作为参数的版本,这样实参在传递的时候,左值和右值可以匹配上。
在C++11以后多了右值引用,如果我们分别重载了一个函数参数是左值引用版本,const左值引用版本,以及右值引用版本,那么在传参时,编译器就会根据最匹配的那一项进行匹配。
这里需要注意的一点是,前面也说过的,右值引用自身的属性是左值,所以如果传入的实参是一个右值引用,那么它会调用的函数应该是左值引用的那个版本。
cpp
#include<iostream>
using namespace std;
void f(int& x)
{
cout << "f(int& x)" << endl;
}
void f(const int& x)
{
cout << "f(const int& x)" << endl;
}
void f(int&& x)
{
cout << "f(int&& x)" << endl;
}
int main()
{
int a = 10;
const int b = 10;
f(a); //调用f(int& x)
f(b); //调用f(const int& x)
f(1); //调用f(int&& x)
int&& x = 1;
//右值引用本身的属性是左值
f(x); //调用f(int& x)
//move以后的左值属性就变成了右值
f(move(x));//调用f(int&& x)
return 0;
}
五、右值引用和移动语义的使用场景
左值引用主要的使用场景在于左值引用传参和左值引用传返回值是能够减少拷贝,同时还可以修改实参和修改返回值对象。左值引用已经解决了大多数场景的情况,但是下面这种情况左值引用并不能解决。
cpp
class Solution {
public:
// 传值返回需要拷⻉
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());
return str;
}
};
这里可能会有人想用右值引用就可以解决这里的问题,但是实际上是不行的。这里的根本原因在于这个返回对象是一个局部对象,这个函数结束的时候这个对象就销毁了,右值引用也无法改变这个局部对象出了这个函数域就会销毁的事实。
可能也有人会疑惑前面不是还说const左值引用和右值引用可以延长生命周期吗?这里为什么又不能实现了。前面也强调了,延长生命周期的概念是在当前函数的栈帧里,在上面的这个情景下,返回对象已经出了当前的函数栈帧了,它的生命周期是不会被延长到当前的函数栈帧外的。所以右值引用做返回值是不行的。
移动构造和移动赋值
移动构造也是一种构造函数,和拷贝构造函数类似,移动构造的要求是这个构造函数的第一个参数必须是该类类型的右值引用,如果有其他参数,那么其他参数必须有缺省值。
移动赋值是赋值运算符的一个重载,它也和拷贝赋值的重载类似,移动赋值的重载要求的是函数的第一个参数是该类类型的右值引用。
只有像string这种需要进行深拷贝的类或者包含需要深拷贝的成员变量的类的移动构造和移动赋值才有意义,因为移动拷贝和移动赋值的本质行为是"窃取"引用的右值对象的资源,因为右值引用能够引用的是临时对象或者move以后的左值对象,默认在这一行以后这个对象就会销毁了,那么它的资源就会回收,所以我们可以很安全把它的资源转移到我们需要的地方去。
右值引用和移动语义解决传值返回问题
现在有以下两种场景:下面两种情况分别是使用了拷贝和构造的不同情况。
cpp
#include<iostream>
#include<string>
using namespace std;
// 场景1
int main()
{
string ret = addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
// 场景2
int main()
{
string ret;
ret = addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
场景1:右值对象构造
在不考虑编译器优化的情况下,如果是只有拷贝构造没有移动构造,那么此时我们需要先用str拷贝构造出一个临时对象,在用临时对象构造出ret对象。
如果是有拷贝构造也有移动构造的情况下,在传值返回时,str虽然是一个左值,但是编译器会把这个str识别为一个右值,此时在构造这个临时对象的时候用的就是移动构造,移动构造不会申请新的空间,它会直接把str的资源转移到临时对象中,在main函数中ret又要用临时对象来进行构造,这个临时对象也明显是一个右值,所以用到的也是移动构造,所以也是转移临时对象中的资源,不需要重新去申请资源,效率就提升了很多。
场景2:右值对象拷贝
如果只有拷贝构造和拷贝赋值,那么在返回的时候会先构造出一个临时对象,ret是一个需要深拷贝的类,所以拷贝赋值也需要申请自己的资源来进行拷贝。
在右拷贝构造和拷贝赋值也有移动构造和移动赋值时,临时对象会调用移动构造,此时临时对象会去转移str中的资源不会自己申请新的空间,ret又会转移临时对象中的资源,也不会自己去申请空间,所以效率也得到了很大的提升。