当前位置: 首页 > news >正文

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

文章目录

  • 前言
  • 汇编代码初探
  • 指针变量
  • 左值引用
  • 常量引用
  • 右值引用
  • 一点点惊奇
  • 总结

前言

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

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

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

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

  • 简单聊聊C/C++中的左值和右值
  • C++11在左值引用的基础上增加右值引用

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

汇编代码初探

为了熟悉一下汇编代码,我们先写个简单的例子,内容就是上述提到的那一段,新建一个文件 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的值,这样修改后果是可能是无法预期的,它存在的目的是调整一些指针、引用的权限,比如在函数传递参数的时候

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

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

相关文章:

  • C/C++中的数据类型转换()/static_cast/dynamic_cast/const_cast/reinterpret_cast
  • C++11中std::move和std::forward到底干了啥
  • 使用box2dweb做一个下落的小球,宝宝玩的不亦乐乎
  • C++中使用std::sort自定义排序规则时要注意的崩溃问题
  • 从一个小题中的应用来体会下std::tie的便利之处
  • Floyd-Warshall——仅用4行代码就能解决多源最短路径问题的算法
  • Dijkstra——通过不断松弛来解决单源最短路径问题的算法
  • C++11中的std::atomic保证的原子性是什么
  • .bat批处理(十):从路径字符串中截取盘符、文件名、后缀名等信息
  • linux环境下从路径字符串中截取目录和文件名信息
  • MD5是用来加密的吗?BCrypt又是什么呢
  • 树的带权路径长度和哈夫曼树
  • 完全图与强连通图的那些坑
  • linux环境下恢复rm误删的文件
  • 记一次使用Valgrind查找解决内存问题的玄幻旅程
  • .pyc 想到的一些问题
  • 【刷算法】从上往下打印二叉树
  • 【跃迁之路】【699天】程序员高效学习方法论探索系列(实验阶段456-2019.1.19)...
  • 345-反转字符串中的元音字母
  • avalon2.2的VM生成过程
  • ES6系统学习----从Apollo Client看解构赋值
  • gcc介绍及安装
  • httpie使用详解
  • MySQL QA
  • PAT A1120
  • React组件设计模式(一)
  • Redis学习笔记 - pipline(流水线、管道)
  • spark本地环境的搭建到运行第一个spark程序
  • Xmanager 远程桌面 CentOS 7
  • 面试总结JavaScript篇
  • 优化 Vue 项目编译文件大小
  • - 语言经验 - 《c++的高性能内存管理库tcmalloc和jemalloc》
  • 阿里云服务器购买完整流程
  • #{}和${}的区别是什么 -- java面试
  • (C语言)共用体union的用法举例
  • (Redis使用系列) SpringBoot中Redis的RedisConfig 二
  • (三维重建学习)已有位姿放入colmap和3D Gaussian Splatting训练
  • (十三)Java springcloud B2B2C o2o多用户商城 springcloud架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)...
  • (已更新)关于Visual Studio 2019安装时VS installer无法下载文件,进度条为0,显示网络有问题的解决办法
  • (已解决)vue+element-ui实现个人中心,仿照原神
  • (转)chrome浏览器收藏夹(书签)的导出与导入
  • **PyTorch月学习计划 - 第一周;第6-7天: 自动梯度(Autograd)**
  • .equal()和==的区别 怎样判断字符串为空问题: Illegal invoke-super to void nio.file.AccessDeniedException
  • .Family_物联网
  • .NET 8 中引入新的 IHostedLifecycleService 接口 实现定时任务
  • .NET Standard 的管理策略
  • .Net 高效开发之不可错过的实用工具
  • .NET 设计模式初探
  • .NET/C# 的字符串暂存池
  • .net经典笔试题
  • .NET开发不可不知、不可不用的辅助类(一)
  • .NET使用存储过程实现对数据库的增删改查
  • .NET下的多线程编程—1-线程机制概述
  • .net中我喜欢的两种验证码
  • /dev/VolGroup00/LogVol00:unexpected inconsistency;run fsck manually