换个角度来看看C++中的左值、右值、左值引用、右值引用

前言

对于左值和右值有一个不太严谨的定义——在赋值表达式 = 左侧是的左值,而在 = 右侧的是右值。通过不断学习和尝试,最近我发现一个新的说法更加贴切,那就是“左值是容器,右值是东西”。对于这个定义我们可以类比一下水杯和水,通过水杯可以操作水杯中的水,操作过程中的中间结果如果想要进一步操作,可以将其放入其他的水杯,如果没有水杯就无法找到曾经操作过的水了,也就无法继续操作了。

int a = 2;
int b = 6;
int c = a + b;

在这个例子中,变量 ab, c 都是水杯,而 26a + b 都是被用来操作的水,只有把这些“水”放到“水杯”中才能被找到,才可以进行下一步操作。

关于左值、右值、左值引用和右值引用的概念可以看看之前的总结:

虽然温故不一定知新,但绝对可以增强记忆,参照着之前的理解,今天来换一种窥探本质的方式。

汇编代码初探

为了熟悉一下汇编代码,我们先写个简单的例子,内容就是上述提到的那一段,新建一个文件 main.cpp,然后编写如下代码:

int main()
{
    int a = 6;
    int b = 2;
    int c = a + b;

    return 0;
}

运行 g++ main.cpp --std=c++11 -S -o main.s 编译这段代码,生成汇编文件 main.s,打开文件内容如下:

    .file   "main.cpp"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $6, -12(%rbp)
    movl    $2, -8(%rbp)
    movl    -12(%rbp), %edx
    movl    -8(%rbp), %eax
    addl    %edx, %eax
    movl    %eax, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
    .section        .note.GNU-stack,"",@progbits

其中代表定义变量和做加法的语句转换成汇编代码如下:

    movl    $6, -12(%rbp)       // 把立即数6放到内存地址为-12(%rbp)的位置,也就是变量a中
    movl    $2, -8(%rbp)        // 把立即数2放到内存地址为-8(%rbp)的位置,也就是变量b中
    movl    -12(%rbp), %edx     // 把内存地址为-12(%rbp)的位置(变量a)的数据放到寄存器%edx中
    movl    -8(%rbp), %eax      // 把内存地址为-8(%rbp)的位置(变量b)的数据放到寄存器%eax中
    addl    %edx, %eax          // 把寄存器%edx中的数据加到寄存器%eax中
    movl    %eax, -4(%rbp)      // 把寄存器%eax中的计算所得结果数据放到内存地址为-4(%rbp)的位置,也就是变量c中

指针变量

首先来看看通过指针来修改变量值的过程,测试代码如下:

    int a = 6;
    int* p = &a;
    *p = 2;

转换成汇编代码如下:

    movl    $6, -20(%rbp)       // 把立即数6放到内存地址为-20(%rbp)的位置,也就是变量a中
    leaq    -20(%rbp), %rax     // 把这个内存地址-20(%rbp),也就是变量a的地址保存在寄存器%rax中
    movq    %rax, -16(%rbp)     // 把寄存器%rax中的保存的变量a的地址,放到内存地址为-16(%rbp)的位置,也就是变量p中
    movq    -16(%rbp), %rax     // 把内存地址为-16(%rbp)的位置(变量p)的数据放到寄存器%rax中
    movl    $2, (%rax)          // 把立即数2放在寄存器%rax中保存的地址位置中,也就是p所指向的地址,即变量a中

通过汇编代码可以发现,通过指针修改变量的值实际上是在指针变量中保存变量的地址值,修改变量时是通过指针变量直接找到变量所在内存,然后直接修改完成的。

左值引用

接着来看下通过引用来修改变量值的过程,测试代码如下:

    int a = 6;
    int& r = a;
    r = 2;

转换成汇编代码如下:

    movl    $6, -20(%rbp)
    leaq    -20(%rbp), %rax
    movq    %rax, -16(%rbp)
    movq    -16(%rbp), %rax
    movl    $2, (%rax)

看到这里是不是有点意思了,这几行通过引用修改变量值的代码转换成汇编代码以后,居然和之前通过指针修改变量值的汇编代码一模一样。咦?仿佛发现了引用的本质呀!

常量引用

在传统C++中我们知道,引用变量不能引用一个右值,但是常引用可以办到这一点,测试代码如下:

    const int& a = 6;

转换成汇编代码如下:

    movl    $6, %eax            //把立即数放到寄存器%eax中
    movl    %eax, -20(%rbp)     //把寄存器%eax中的数字6放到内存地址为-20(%rbp)的位置,一个临时变量中
    leaq    -20(%rbp), %rax     //把临时变量的内存地址-20(%rbp)放到寄存器%rax中
    movq    %rax, -16(%rbp)     //把寄存器%rax中存储的临时变量的内存地址-20(%rbp)放到内存地址为-16(%rbp)的位置

这段代码的翻译结果与前面指针变量的例子很像,首先有一个变量(匿名变量)来存储值,然后是一个新的内存地址来保存之前变量的地址。

右值引用

右值引用需要C++11才能使用,与常引用对比的优点就是可以修改右值,实际上我认为还是修改的左值!测试代码如下:

    int&& a = 6;
    a = 2

转换成汇编代码如下:

    movl    $6, %eax            //把立即数放到寄存器%eax中
    movl    %eax, -20(%rbp)     //把寄存器%eax中的数字6放到内存地址为-20(%rbp)的位置,一个临时变量中
    leaq    -20(%rbp), %rax     //把临时变量的内存地址-20(%rbp)放到寄存器%rax中
    movq    %rax, -16(%rbp)     //把寄存器%rax中存储的临时变量的内存地址-20(%rbp)放到内存地址为-16(%rbp)的位置
    movq    -16(%rbp), %rax     // 把内存地址为-16(%rbp)的位置(变量p)的数据放到寄存器%rax中
    movl    $2, (%rax)          // 把立即数2放在寄存器%rax中保存的地址位置中,也就是p所指向的地址,即变量a中

这段汇编代码与常量引用相比只缺少赋值的部分,与左值引用相比几乎一样,只有在最开始立即数6的处理上有一点点差异,是不是感觉很神奇?

一点点惊奇

对比了前面这些代码的汇编指令后有没有什么想法?什么常量引用,什么右值引用,这些不过都是“愚弄”程序员的把戏,但这些概念的出现并不是为了给程序员们带来麻烦,相反它们的出现使得程序编写更加可控,通过编译器帮助“粗心”的开发者们先暴露了一波问题。

通过汇编代码来看,常量引用其实引用的并非常量,而是引用了一个变量;右值引用引用的也并非右值,同样是一个保存了右值的变量。这年头常量都能变,还有什么不能变的呢?

来看看下面这段代码,仔细想想常量真的变了吗?运行之后各个变量的值是多少呢?

    const int a = 6;
    int *p = const_cast<int*>(&a);
    *p = 2;

    int b = *p;
    int c = a;

这段代码运行之后的打印结果:a=6, b=2, c=6,变量a作为一个常量没有被改变,貌似常量还是有点用的,哈哈~

这段代码转换成汇编代码如下:

    movl    $6, -28(%rbp)
    leaq    -28(%rbp), %rax
    movq    %rax, -16(%rbp)
    movq    -16(%rbp), %rax
    movl    $2, (%rax)
    movq    -16(%rbp), %rax
    movl    (%rax), %eax
    movl    %eax, -24(%rbp)
    movl    $6, -20(%rbp)

通过汇编来看你会发现,其实变量a的值已经通过指针 p 修改过了,只不过后面引用a变量的地方,因为它是常量,直接使用立即数6替换了。

改写一下代码,将常量6换成一个变量:

    int i = 3;
    const int a = i;
    int *p = const_cast<int*>(&a);
    *p = 2;

    int b = *p;
    int c = a;

转换成汇编代码为:

    movl    $3, -28(%rbp)
    movl    -28(%rbp), %eax
    movl    %eax, -32(%rbp)
    leaq    -32(%rbp), %rax
    movq    %rax, -16(%rbp)
    movq    -16(%rbp), %rax
    movl    $2, (%rax)
    movq    -16(%rbp), %rax
    movl    (%rax), %eax
    movl    %eax, -24(%rbp)
    movl    -32(%rbp), %eax
    movl    %eax, -20(%rbp)

这段代码运行的结果为:i=3, a=2, b=2, c=2,看来常量也禁不住我们这么折腾啊

所以从这一点可以看出C++代码中无常量,只要是定义出的变量都可以修改,而常量只是给编译器优化提供一份指导,比如可以把一些字面量在编译期间替换,但是运行时的常量还是能改的。

总结

  • 左值和右值更像是容器与数据的关系,不过C++11提出的将亡值的概念又模糊这两者的界限,将亡值可以看成是即将失去容器的数据
  • 在Ubuntu16.04、GCC5.4.0的环境下,通过左值引用和指针修改一个变量值生成的汇编代码完全一致
  • C++11中右值引用与常量引用生成的汇编代码一致,与左值引用生成的代码只在初始化时有一点差异
  • 常量并非不可修改,它只是一种“君子协定”,你要知道什么情况下可以改,什么情况下绝对不可以改
  • const_cast 目的并不是让你去修改一个本身被定义为const的值,这样修改后果是可能是无法预期的,它存在的目的是调整一些指针、引用的权限,比如在函数传递参数的时候

==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

身上若无千斤担,谁拿生命赌明天~
世间唯一不变的就是变化

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AlbertS

常来“玩”啊~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值