右值引用
右值引用的概念
概念解析
- 引用:给内存块取别名,C++11之前只能给左值取别名,C++11之后可以给右值取别名
- 左值(lvalue):出现在
=
左边的就是左值 - 右值(rvalue):出现在
=
右边的值,常常为 字面常量、表达式、函数的非引用返回值等
注意点:右值引用不能绑定左值
右值举例:
int a = 10; //a是左值,10为右值
string str = "hello"; //str是左值,字面量“hello”是右值
int func(){
return 100; //函数非引用返回值100 为右值
}
右值引用语法:用&&
代表右值引用
类型 && 变量名 = 右值;
右值引用语法举例
int && a = 10;
string && b = "hello";
移动语义
作用:在函数的参数值传递时(值的返回)就会发生拷贝构造、析构函数,这对程序的性能会有很大影响。移动语义就可以减少此类发生。有点类似于拷贝文件和剪切文件,剪切文件的效率更高。
移动构造函数
C++11新增的构造函数。 在函数中返回非引用类型变量(比如局部变量),编译器先会调用移动构造,如果程序员没有写移动构造才会第调用拷贝构造
语法
class A{
public:
A(A && rv){ //移动构造,参数是非const右值引用
this->xx指针 = rv.xx指针;
rv.xx指针 = nullptr;
}
};
移动赋值函数
本质:重载operator=
,参数是非const 的右值引用。返回左引用
语法:
class A{
public:
A& operator=(A && rv){ //=重载,参数是非const右值引用,返回左引用
if (&rv == *this){
return *this;
}
this->xx指针 = rv.xx指针;
rv.xx指针 = nullptr;
return *this;
}
};
范例
class A
{
public:
A() :array(new int[3]{ 1, 2, 3 })
{
}
~A()
{
cout << "调用析构函数" << endl;
if (nullptr != array) //析构的时候,要看看指针是否为空,不为空就要释放
{
delete[] array;
}
}
A(A &&a) //移动构造
{
this->array = a.array;
a.array = nullptr; //一定要让a的指针为空,注意不是delete!
//a.array = nullptr; //如果是delete会导致相同内存重复delete,从而崩溃
std::cout << "移动构造" << endl;
}
A(const A& a) //拷贝构造
{
this->array = new int[3]{ a.array[0],a.array[1],a.array[2] };
std::cout << "拷贝构造" << endl;
}
A& operator=(const A& a) //等号重载
{
std::cout << "等号重载" << endl;
if (&a == this) {
return *this;
}
this->array = new int[3]{ a.array[0],a.array[1],a.array[2] };
return *this;
}
A& operator=(A&& a) // 移动赋值函数
{
std::cout << "移动赋值函数" << endl;
if (&a == this) {
return *this;
}
this->array = a.array;
a.array = nullptr;
return *this;
}
static A returnTemObj()
{
A a;
std::cout << "局部变量a的数组地址为 " << a.array << endl;
return a; //只要是return非引用类型变量就会调用移动构造
}
int *array{ nullptr };
};
void func1() {
A a1;
A a2(a1);
}
void func2() {
A tem = A::returnTemObj(); //即使没有A tem 只有A::returnTemObj();也会触发移动构造
cout << "tmp的数组地址为" << tem.array << endl;
}
void func3() {
A a;
A b;
b = a;
}
void func4() {
A a;
a = A(); //创建一个临时对象,并且赋值给a对象,这就达成了移动赋值
}
int main()
{
cout << endl << "========拷贝构造=========" << endl;
func1();
cout << endl << "========移动构造=========" << endl;
func2();
cout << endl << "========等号重载=========" << endl;
func3();
cout << endl << "========移动赋值=========" << endl;
func4();
return 0;
}
执行结果
========拷贝构造=========
拷贝构造
调用析构函数
调用析构函数
========移动构造=========
局部变量a的数组地址为 012E03E0
移动构造
调用析构函数
tmp的数组地址为012E03E0
调用析构函数
========等号重载=========
等号重载
调用析构函数
调用析构函数
========移动赋值=========
等号重载
调用析构函数
调用析构函数
解析移动构造
在上面例子中的func2
调用了步骤如下
- 调用类的
static A returnTemObj()
函数 - 程序进入
returnTemObj
函数- 创建局部变量a
- return a;(因为
class A
写了移动构造,所以优先执行A的移动构造,而不是执行A的拷贝构造) - a作为右值进入
class A
的移动构造中(return 非引用的对象,那么这个对象就属于右值)- 将右值a 的array指针赋值给临时对象的array
- 将右值a 的array置空
- delete 局部变量a。(假设在移动构造中右值a 的array没有置空,那么此处会delete1次array)
- 给临时对象命名为tmp
returnTemObj
函数结束,delete tmp
对比拷贝函数func1
的差异:
对比项目 | func1(拷贝构造) | func2(移动构造) |
---|---|---|
触发时机 | 1、拷贝构造时 2、没有移动构造时的return非引用的对象 | return 非引用的对象时 |
入参对比 | const A&obj const修饰的左值引用 | A&& obj 无const修饰的右值引用 |
处理类中指针 | this->xx指针需要重新new, 并且要把obj.xx指针中所指的数据拷贝到this->xx指针中 | this->xx指针直接赋值obj.xx指针 |
是否需要修改obj | 不需要 | 需要将obj中的指针置空 (不是delete) |
解析移动赋值函数
在上面例子中的func4
调用了步骤如下
- 创建一个class A类型的对象a
- 创建一个class A类型的临时对象
- 将临时对象(右值)赋值给a
- 触发移动赋值函数,程序进入移动赋值函数体内
- 将临时变量的array赋值给a的array
- 将临时变量 的array置空
- 移动赋值函数结束,临时对象失效被delete
func4
函数体结束,a被delete
对比等号重载func3
的差异:
对比项目 | func3(传统的=运算符重载) | func4(移动赋值=运算符重载) |
---|---|---|
入参对比 | const A&obj const修饰的左值引用 | A&& obj 无const修饰的右值引用 |
处理类中指针 | this->xx指针需要重新new, 并且要把obj.xx指针中所指的数据拷贝到this->xx指针中 | this->xx指针直接赋值obj.xx指针 |
是否需要修改obj | 不需要 | 需要将obj中的指针置空 (不是delete) |
返回的数据类型 | A& (左引用) | A& (左引用) |
std::move 左值变右值
作用:
- C++11中右值引用不能绑定左值。但是使用move就可以将左值转成右值。
- 右值才能使用移动构造和移动赋值函数,如果左值想要执行移动赋值就需要先转成右值
语法:对于普通左值变量 转 右值变量
int a = 10;
int && b = std::move(a);
std::forward完美转发
可以将参数原封不动的传给另外一个函数。
语法
forward<T>(变量)
举例
void forward1(int && obj){
cout << "obj为右值 "<<endl;
}
void forward1(int & obj){
cout << "obj为左值 "<<endl;
}
void func7(){
int a = 10;
int &&b = 20;
forward1(12); //obj为右值
forward1(a); //obj为左值
forward1(b); //obj为左值
forward1(forward<int>(b));//obj为右值
}