c 表达式必须是可修改的左值_C++中的左值,右值,左值引用,右值引用

本文聚焦C++中左值、右值、左值引用和右值引用的概念。左值可在表达式左右,代表内存地址;右值只能在右,是暂时数值。左值引用是变量别名,右值引用在C++11引入,可引用并更改右值。右值引用能减少内存分配,实现转移语义。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

006b38f2ee296dad685e0d78c14f74fb.png

童帅 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++代码,右边输出对应的汇编代码。当你好奇某些代码的底层实现时,这是个很方便的工具。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值