1 左值和右值
左值(loactor value):存储在内存中、可寻址的数据
右值(read value):可以提供数据值的数据(不一定可寻址,例如存储在寄存器中的数据)
区分:
- 可以位于=左侧的表达式就是左值;右值只能位于=右侧(c++中左值也能位于=右侧,当做右值使用)
- 有名称、可寻址的是左值;反之是右值
2 右值引用
2.1 右值引用的引入
C++11之前的引用(&)只能操作左值,无法操作右值。
常量左值引用可以操作右值,但是无法对右值进行修改。
int a = 10;
int& b = a; //左值引用
int& c = 10; //错误,左值引用无法操作右值
const int& d = 10; //常量左值引用可以操作右值
因此,C++11引入了右值引用 &&
2.2 右值引用的使用
- 初始化:只能用右值初始化,且和左值一样,声明后必须立即初始化
- 右值引用可以修改右值
- 可定义常量右值引用,但没什么意义。右值引用主要用于实现移动语义和完美转发,移动语义需要有修改右值的权限;常量右值引用可有常量左值引用实现
int&& a = 10; //右值引用
a = 11; //通过右值引用修改右值
const int&& b = 15; //常量右值引用
2.3 总结
非常量左值引用 只能引用 非常量左值
常量左值引用 可以引用 非常量左值、常量左值、右值
非常量右值引用 只能引用 非常量右值
常量右值引用 可以引用 常量右值、非常量右值
3 move()函数将左值转换到右值
int num = 10;
int&& a = std::move(num);
4 右值引用的场景
4.1 拷贝构造函数
使用其它对象初始化一个同类的新对象,在C++11之前,只能使用拷贝构造函数。当类中有指针成员变量时,拷贝构造函数需要以深拷贝的方式复制该成员(开辟一片新的空间,复制原对象指针所指向的数据)。
#include <iostream>
using namespace std;
class demo{
public:
//构造函数
demo():num(new int(0)){
cout<<"construct!"<<endl;
}
//拷贝构造函数(深拷贝)
demo(const demo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
}
~demo(){
cout<<"class destruct!"<<endl;
}
private:
int *num;
};
demo get_demo(){
return demo(); //返回一个demo对象,是一个右值
}
int main(){
//demo a = get_demo(); //拷贝赋值
demo a(get_demo()); //拷贝构造
return 0;
}
如上所示,demo 类自定义了一个拷贝构造函数。该函数在拷贝 d.num 指针成员时,必须采用深拷贝的方式,即拷贝该指针成员本身的同时,还要拷贝指针指向的内存资源。否则一旦多个对象中的指针成员指向同一块堆空间,这些对象析构时就会对该空间释放多次,这是不允许的。
dema a(get_demo()); 的流程:
- 执行 get_demo() 函数,demo()调用构造函数生成一个匿名对象
- 执行 return demo() ,调用拷贝构造函数拷贝匿名对象,作为get_demo()的返回值(get_demo()执行完毕,匿名对象会被销毁)
- 执行 a(get_demo()), 调用拷贝构造函数(此行代码执行完毕,get_demo()的返回值会被析构)
- 程序结束前,a被析构。
在这个过程中,底层执行了2次深拷贝。如果指针指向的堆空间较大,会大大降低执行的效率。通过移动构造函数可以解决这个问题。
4.2 移动构造函数
移动语义:以移动而非深拷贝的方式初始化含有指针类成员的对象。将其他对象的内存资源移为己用,而非拷贝。这大大提高了初始化的执行效率。
#include <iostream>
using namespace std;
class demo{
public:
demo():num(new int(0)){
cout<<"construct!"<<endl;
}
//拷贝构造函数
demo(const demo &d):num(new int(*d.num)){
cout<<"copy construct!"<<endl;
}
//移动构造函数
demo(demo &&d):num(d.num){
d.num = NULL;
cout<<"move construct!"<<endl;
}
~demo(){
cout<<"class destruct!"<<endl;
}
private:
int *num;
};
demo get_demo(){
return demo();
}
int main(){
//调用右值初始化当前对象,移动构造函数存在的情况下,优先调用移动构造函数
demo a(get_demo());
return 0;
}
移动构造函数:使用右值引用类型的参数,指针浅拷贝,右值对象指针置为nullptr, 从而,避免拷贝堆空间,完成初始化。
当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
std::move()可以将左值转换为右值,从而使用移动构造。