从汇编角度分析左值引用 右值引用

## 背景:

有天躺在床上开始回忆c++经典问题:左值,右值,左值引用,右值引用。 果不其然,我又忘了。。。
网上总结的啥 左值是有别名的,可取地址的这些特点,真的很容易让我忘记。。。(这种方式,真的是容易忘。。2333)
为了了解其本质,我们从汇编的角度透彻的理解一遍吧,为了方便回顾,写博客是最直接最有效的方式。让我们开始:

左值引用

## 实验代码:

首先写一个左值引用的简单代码,然后查看它对应的无任何优化的汇编
(强烈推荐 [https://godbolt.org/])

image

c++代码语句与汇编代码是根据颜色一一对应的,很方便分析,以下将根据行号来做分析

## 语句一:

int a = 200; -> mov DWORD PTR[rbp-12], 200
200 是立即数,[rbp-12] (基址+偏移量)寻址(变量a的地址),DWORD PTR[rbp-12]表示地址指向的双字(DWORD) 数据,类似于 c/c++ 的*p.
该条指令的含义非常清晰:立即数200作为数据写入地址rbp-12

## 语句二:

int &b = a -> mov rax, [rbp-12] mov QWORD PTR[rbp-8], rax
[rbp-12] (基址+偏移量)寻址.
mov rax [rbp-12] 含义是将地址写入到寄存器rax。
mov QWORD PTR[rbp-8], rax含义是将 rax寄存器中的值(也就是a的地址)作为数据写入到rbp-8的地址。
发现了什么? QWORD PTR[rbp-8] 存的是a的地址(rbp-12)啊!!!,不是200!!!!

## 语句三:

让我们来仔细看看 std::cout << b << "\n" 这条语句对应的汇编,请注意汇编代码的第10行和第11行:
mov rax, QWORD PTR[rbp-8] rax 寄存器存的是a的地址(rbp-12
mov eax DWORD PTR[rax] 这才是重要的,DWORD PTR[rax] 从rax存的地址中读取数据,也就是200.
因此,真正使用变量b的时候,实际上是两条mov 指令完成:先获取地址,再从地址中获取数据。

## 左值引用总结:

各位胖友们,是不是明白了几个现象:

  1. 改变a的值,b的值也变了。因为a地址中存放的数据变了,所以当使用b的值做其他操作时,实际上是访问a地址中存放的数据 (仔细琢磨琢磨上述语句二和语句三)
  2. 左值引用,等号右边不能是立即数,必须是能获取到地址的。why??因为汇编告诉我们,左值引用等号左边的内存地址存的是等号右边地址!!like int &b = a

常量引用

## 实验代码:

image

image

## 语句一:

const int &a = 200; -> mov eax, 200 mov DWORD PTR [rbp-12], eax lea rax, [rbp-12] mov QWORD PTR [rbp-8], rax
mov eax, 200 mov DWORD PTR[rbp-12], eax含义是把立即数写入到地址rbp-12中。有意思的是,为啥这里不是一条汇编指令mov DWORD PTR[rbp-12], 200 呢?别急,让我们往后面看:
lea rax, [rbp-12] 把rbp-12 放入rax寄存器
mov QWORD PTR [rbp-8], rax, 把rbp-12 作为数据写入到rbp-8地址中。

胖友们,肯定有疑问了,相比于左值引用,为啥常量引用多了好几条指令。

  1. mov eax, 200 mov DWORD PTR[rbp-12], eax, 而不是 mov DWORD PTR[rbp-12], 200 . 因为rbp-12是操作系统分配的一块临时地址,不对外暴露的,也就是说你是不可能通&200得到地址的~~(语法规则也不允许你这样)
  2. lea rax, [rbp-12] , 而不是 mov rax, [rbp-12]. 其实我觉得这里用mov 和lea没啥区别,也有可能我没想明白23333,总之其实这里没有啥玄机哈哈哈哈
  3. mov QWORD PTR [rbp-8], rax 与左值引用一样的配方,rbp-8地址存的可是临时地址哦

## 常量引用总结:

  1. 为啥常量引用初始化时还可以绑定右值?因为编译器可以为右值分配一个临时地址哈哈哈

  2. 为啥常量引用,改不了值(不能直接赋值), like b = 200;。其实只从汇编指令角度看,是不区分const和非const,因此const实际上就是c/c++语法规则层面的意思,由编译器负责检查语法错误。(我是这么理解的,因为从汇编代码看不出和左值引用任何不一样的地方)。

  3. 来看一个有意思的例子:
    image

输出是多少? 哈哈哈哈哈是300!!!! 惊不惊讶~,想到了啥捏🤦‍♀️.... 是不是想到了一句咬文嚼字的话:“不可以通过常量引用来修改它所指向的对象的值。” 哈哈哈哈,当你理解了汇编本质之后,你会发现so easy!!! 因为常量引用也不过指向的是引用对象的地址哎~~,所以常量引用无法阻止改变引用对象地址中的数据的行为,它只能是保证引用对象的地址是不能改滴~

右值引用

## 实验代码:

image

image

## 语句一:

发现了啥!!! 这汇编指令是不是和const int &b = 200 一毛一样啊!!
更进一步的印证了我上面的猜想,汇编指令是不区分const/非const的,这些全是上层语言自定义的语法规则,是由编译器负责分析和检查语法规则的!!!

再看个好玩的例子:
image
image

## 右值引用总结:

所以其实本质上右值引用和左值引用对应的汇编指令没有啥大的区别。而c++引入右值引用是为了移动语义和完美转发。因此为了区分左值引用和右值引用,语法规则上加入了很多限制:比如右值引用初始化的时候不可以绑定一个左值,我的理解是因为左值不是一个临时地址,而右值引用是为了优化代码而引入,优化的就是那些临时地址,而不是使用mov 挪来挪去。。因此需要编译器来辅助优化汇编指令。

## 个人感想:

(我感觉这c/c++ 语法规则中的一些corner case, 实际上是为了解决语言和汇编指令之间gap定义的~~),有没有啥c++语言发展历史的讲解啊~~哈哈哈哈,指不定真是我猜想的这样哈哈哈哈哈哈

## Flag:

下一篇专门讲一下移动语义和完美转发,编译器是怎么优化的,以及他们对应的汇编指令是什么。

posted @ 2022-09-18 22:40  Cheney_1016  阅读(451)  评论(0编辑  收藏  举报