
童帅 2020-2-22
文中的“表达式”都是指赋值表达式
左值,右值,左值引用,右值引用 到底是什么
左值和右值
int a = 10;
int b = 5;
int c = a + b;
int d = a;
左值就是可以出现在表达式左边的值,右值就是只能出现在表达式右边的值
上面的代码中,a, b, c是左值,可以被各种赋值,出现在表达式左边,当然也可以出现在表达式右边,比如第四行
10,5,a+b这种是右值,因为它们不能出现在表达式的左边,只能出现在表达式的右边
左值实际上指的是某个内存地址,而右值指的就是那个数值,这个数值是暂时的,可能没有明确的地址,也可能只出现在一个寄存器中。
直观上来讲,左值是容器,右值就是东西。容器里可以有或者没有东西,可以把里面的东西替换成别的东西;东西就是东西,它本身不可以被替换。
上面示例的汇编代码如下
mov DWORD PTR [rbp-4], 10 ;这里a代表一个栈上的地址,也就是rbp-4;但是10被硬编码到了指令中,没有地址
mov DWORD PTR [rbp-8], 5
mov edx, DWORD PTR [rbp-4]
mov eax, DWORD PTR [rbp-8]
add eax, edx ;这里a+b只是短暂地存储与寄存器中
mov DWORD PTR [rbp-12], eax ;已经存储到c里
mov eax, DWORD PTR [rbp-4]
mov DWORD PTR [rbp-16], eax
左值引用
int a = 10;
int &b = a;
b = 5;
左值引用,实际上是给原来的变量名起了一个别名,操作数据的时候是对原来地址的数据进行操作的
可以看一下上面代码的汇编代码
mov DWORD PTR [rbp-12], 10 ;a代表rbp-12这个地址
lea rax, [rbp-12] ;b实际上存储了a代表的地址
mov QWORD PTR [rbp-8], rax ;b代表rbp-8这个地址,这个地方存储了a代表的地址
mov rax, QWORD PTR [rbp-8] ;取出b存储的地址
mov DWORD PTR [rax], 5 ;对a代表地址处存储的数据进行操控
也就是说,要想左值引用a,a必须是左值,必须有地址,下面的代码是错的
int &b = 10; // error code
但是,有一种引用右值的方法,就是用const关键字,这个关键字给右值分配一个存储空间
const int &b = 10;
汇编代码如下
mov eax, 10
mov DWORD PTR [rbp-12], eax ;这里分配了一个地址用于存储10这个立即数
lea rax, [rbp-12]
mov QWORD PTR [rbp-8], rax ;并且把存储10的地址存储在了b代表的地址。也就是说,存储10的地方有数据,但是没有变量代表存储10的地址,只有b引用这个地址,b存储了存储10的地址
但是这个方法的缺点是,我们只能读取值,不能更改值。
右值引用
为了既可以引用右值,又可以更改这个数据,C++11引入了右值引用
int &&a = 10;
a = 5;
这里引用类型的变量a引用了右值10,来看一下汇编代码
mov eax, 10
mov DWORD PTR [rbp-12], eax ;把10存储在了栈上,rbp-12的位置
lea rax, [rbp-12] ;
mov QWORD PTR [rbp-8], rax ;把存储10的地址保存在了变量a的位置
mov rax, QWORD PTR [rbp-8] ;
mov DWORD PTR [rax], 5 ;直接取出a存储的地址进行操作
可以看出,const类型的左值引用和右值引用在底层代码没有区别!只是右值引用可以更改,左值引用不可以更改。
既然这样,直接左值引用不就得了?为啥还要个右值引用?
右值引用的应用
class Holder{
private:
size_t mSize;
int *mData;
public:
Holder(size_t size){
mSize=size;
mData=new int[size];
}
~Holder(){
delete[] mData;
mSize=0;
}
// copy constructor
Holder(const Holder &h){
if(h.mSize!=mSize){
delete[] mData;
mSize=h.mSize;
mData=new int[mSize];
}
memcpy(mData,h.mData,sizeof(int)*mSize);
}
Holder &operator=(const Holder &h){
if(h==*this)return *this;
if(h.mSize!=mSize){
delete[] mData;
mSize=h.mSize;
mData=new int[mSize];
}
memcpy(mData,h.mData,sizeof(int)*mSize);
return *this;
}
};
Holder getHolder(void){
return Holder(1000);
}
int main(){
Holder h(500);
h = getHolder();
return 0;
}
分析一下这个代码发现,声明h时分配一次内存,调用getHolder时分配一次内存,第38行等于操作符分配一次内存,而且getHolder时分配的临时变量很快就不用了,多了一次分配内存和释放内存的操作。
并且我们是不能直接移动数据的,因为左值引用的变量无法更改,也就不能清除原来对象的值,那么两个对象指向同一块内存,是很危险的。
可以应用右值引用
// copy constructor
Holder(Holder &&h){
delete[] mData;
mSize=h.mSize;
mData=h.mData;
h.mData=nullptr;
h.mSize=0;
}
// operator = is same as copy constructor
在这里,我们把原对象(上面的代码中的临时对象)的东西直接“移动”到了当前对象中,因为原对象是个临时对象,反正以后也不会用到,这样就减少了一次内存分配操作。也就是实现了转移语义。
编译器会很智能地识别出我们需要地是左值引用还是右值引用,比如说getHolder函数返回的对象就是右值引用,编译器会自动调用右值引用对应的函数。
参考资料
[1] Understanding the meaning of lvalues and rvalues in C++
[2] C++ rvalue references and move semantics for beginners
[3] Complier Explorer
参考资料1,2详细不能再详细地介绍了C++中左值,右值,左值引用,右值引用的概念和应用
参考资料3可以在左边输入C++代码,右边输出对应的汇编代码。当你好奇某些代码的底层实现时,这是个很方便的工具。