什么是右值(right value)
- 所有的具名变量或者对象都是左值,而右值不具名字,也不会开辟地址存储,比如
x=y+z
中,y+z
的计算结果是一个右值
什么是引用
- 我们通常说的引用就是左值引用
- 左值引用的是有具体对象的,因为左值引用就是多个变量共享一段内存,换句话说就是别名
- 右值引用的是临时数据
- 矛盾点:引用是指向一段空间的变量,临时变量是没有地址的,比如取地址
&1
这样的操作是不允许的,那么右值与引用怎么联系起来的? - 千万要注意,存储右值引用的变量是一个左值,并非临时对象,
- 矛盾点:引用是指向一段空间的变量,临时变量是没有地址的,比如取地址
右值引用
- 符号
&&
,只能引用右值,不能引用左值 - 右值引用是什么:笔者在看了各种解释之后,总结出右值引用的几个点
- 右值并不是从来都不占用内存,而是会存在一个
内存分配
->变量析构
->内存删除
的过程 - 右值引用其实就是省略了临时变量
内存删除
的过程。 - 右值引用核心在于移动语义
- 右值并不是从来都不占用内存,而是会存在一个
- 乍一看,这不就是左值赋值,就和
int a = 2;
一样,把右值赋予左值了吗?对于单纯的数字来说,可能真的是这样,但是对于一个对象来说,=
赋值久相当于拷贝构造,和右值引用中的移动构造是两种实现,那么移动构造是什么呢?首先要知道移动语义
移动语义
- 移动语义其实就是顾名思义,将一个对象的内容移动到另一个对象中
- 我们可以类比一下,要将文件从A文件夹移动到B文件夹中,可以
复制-粘贴-删除
,也可以是剪切-粘贴
,无疑,第二种方法能够更快速一些。 - 按照笔者的理解,移动语义是为了提高临时变量拷贝的效率而出现的。
- 曾经的对象拷贝往往就是为临时变量开辟一段内存,复制给目标对象,释放临时变量,可见这个临时变量的开辟和删除是没有意义的。
- 移动语义希望能够实现让目标变量接替临时变量的内存空间,这种做法,如图所示,图中实线变成虚线表示对象被析构了:
举例拷贝构造和移动构造
- 首先我们介绍一下,如果没有移动语义,实现自动释放类中指针的做法,很简单,就是在析构函数中添加
delete
class HasPtrMem{
public:
int *d;
HasPtrMem: d(new int(0)) {}
~HasPtrMem(){delete d;}
};
- 这种实现会有什么问题? 浅拷贝会产生悬挂指针,比如有这么个main函数
int main(){
HasPtrMem a;
HasPtrMem b(a);
}//报错
- 这个函数中,创建了一个对象a,以及一个拷贝对象b,那么在结束的时候,会发现,a先delete了,b指向的是a删除的空间,当b调用析构函数的时候,就会出现经典的
悬挂指针
问题 - 传统的做法是重写拷贝构造函数,实现深拷贝,让b也有属于自己的内存空间
class HasPtrMem{
public:
int *d;
HasPtrMem: d(new int(0)) {}
HasPtrMem(HasPtrMem &h):d(new int(*h.d)) {}
~HasPtrMem(){delete d;}
};
- 深拷贝的缺点
- 缺点非常明显,就是当指针数据非常庞大的时候,中间涉及到的开辟,复制与最后的删除,都会带来空间和时间上大量的开销,尤其是有时候拷贝的意义不大,会导致大开销与小回报,比如
get()
函数
- 缺点非常明显,就是当指针数据非常庞大的时候,中间涉及到的开辟,复制与最后的删除,都会带来空间和时间上大量的开销,尤其是有时候拷贝的意义不大,会导致大开销与小回报,比如
class HasPtrMem{
public:
int *d;
HasPtrMem: d(new int(0)) {}
HasPtrMem(HasPtrMem &h):d(new int(*h.d)) {}
~HasPtrMem(){delete d;}
};
HasPtrMem GetTmp(){return HasPtrMem();}
int main (){
HasPtrMem a = GetTmp();
}
- 如何改进这种无意义的拷贝呢?C++11给出了移动构造的解法,这一段一定要在自己编译器上输出结果,光看花花绿绿的代码很难看进去的,下面这段代码需要加上
-fno-elide-constructors
,从而让编译器不要省略构造函数的内容。
#include<bits/stdc++.h>
using namespace std;
class HasPtrMem{
public:
HasPtrMem():d(new int(3)){
cout << "Construct:" << ++n_cstr << endl;
}
HasPtrMem(HasPtrMem &h):d(new int(*h.d)){
cout << "Copy Construct:" << ++n_cptr << endl;
}
HasPtrMem(HasPtrMem &&h):d(h.d){
h.d = nullptr;
cout << "Move Construct:" << ++n_mvtr << endl;
}
~HasPtrMem(){
delete d;
cout << "Destruct: " << ++n_dstr << endl;
}
int *d;
static int n_cstr;
static int n_cptr;
static int n_mvtr;
static int n_dstr;
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;
int HasPtrMem::n_dstr = 0;
HasPtrMem GetTmp(){
HasPtrMem h;
cout << "Resource from: " << __func__ << ": " << hex << h.d << endl;
return h;
}
int main(){
HasPtrMem c(GetTmp());
HasPtrMem a = GetTmp();
cout << "Resource from: " << __func__ << ": " << hex << a.d << endl;
system("pause");
}
//输出
/*
Construct: 1
Resource from GetTmp: 0x603010
Destruct: 1
Move construct: 1
Destruct: 2
Move construct: 2
Resource from main: 0x603010
Destruct: 3
*/
- 这段代码中,在
GetTmp()
调用了一次构造函数,在return h
的时候调用了一次移动构造,在赋值给a的时候,又调用了一次移动构造,因此出现三次调用,以及三次析构,有趣的是,两次输出的变量地址实一样的,说明并没有新开辟空间,只是新变量接替了旧空间。
总结
-
在构造时使得指针对象指向临时变量的堆内存资源
-
保证临时对象不释放所指向的堆内存
-
总的来说,其实就是浅拷贝临时变量,然后临时变量析构的时候不会释放内存。
-
右值引用有什么用?
- 给右值续命(,让右值的生命周期变为变量的生命周期,换句话说其实就是右指变左值,这一点和变量赋值似乎并无不同。
- 与C++11其他新功能相关:智能指针自动删除空间
- 在迁移数据的时候,实现直接移动,而不是开辟-拷贝-粘贴-删除三个过程。
- 举个例子,比如在函数传递的时候,如果形参的是左值引用,那么在传递过程中,发生了拷贝,因此需要调用构造函数进行构造,但是如果形参是右值引用(或者常左值引用),那么该对象不会重新构造,也就是说,右值形参对象不需要调用构造函数
- 右值引用只能引用右值,如下图所示,如果绑定了左值,将会产生错误,这有利于编译器区分传入参数是左值还是右值
容易混淆的点
std::move(lvalue)
, 将左值强制转换为右值- 该函数并非移动的意思,但是设计它最主要的目的就是为了配合移动语义
std::move(lvalue)
在转换之后,lvalue
对象就会失效move
通常用于转换堆内存,fd等资源,因为这些内存空间通常不需要拷贝构造。
参考
- 《深入理解C++11》
- C++11新特性梳理