前言
对于左值和右值有一个不太严谨的定义——在赋值表达式 =
左侧是的左值,而在 =
右侧的是右值。通过不断学习和尝试,最近我发现一个新的说法更加贴切,那就是“左值是容器,右值是东西”。对于这个定义我们可以类比一下水杯和水,通过水杯可以操作水杯中的水,操作过程中的中间结果如果想要进一步操作,可以将其放入其他的水杯,如果没有水杯就无法找到曾经操作过的水了,也就无法继续操作了。
1 | int a = 2; |
在这个例子中,变量 a
,b
, c
都是水杯,而 2
、6
、a + b
都是被用来操作的水,只有把这些“水”放到“水杯”中才能被找到,才可以进行下一步操作。
关于左值、右值、左值引用和右值引用的概念可以看看之前的总结:
虽然温故不一定知新,但绝对可以增强记忆,参照着之前的理解,今天来换一种窥探本质的方式。
汇编代码初探
为了熟悉一下汇编代码,我们先写个简单的例子,内容就是上述提到的那一段,新建一个文件 main.cpp
,然后编写如下代码:
1 | int main() |
运行 g++ main.cpp --std=c++11 -S -o main.s
编译这段代码,生成汇编文件 main.s
,打开文件内容如下:
1 | .file "main.cpp" |
其中代表定义变量和做加法的语句转换成汇编代码如下:
1 | movl $6, -12(%rbp) // 把立即数6放到内存地址为-12(%rbp)的位置,也就是变量a中 |
指针变量
首先来看看通过指针来修改变量值的过程,测试代码如下:
1 | int a = 6; |
转换成汇编代码如下:
1 | movl $6, -20(%rbp) // 把立即数6放到内存地址为-20(%rbp)的位置,也就是变量a中 |
通过汇编代码可以发现,通过指针修改变量的值实际上是在指针变量中保存变量的地址值,修改变量时是通过指针变量直接找到变量所在内存,然后直接修改完成的。
左值引用
接着来看下通过引用来修改变量值的过程,测试代码如下:
1 | int a = 6; |
转换成汇编代码如下:
1 | movl $6, -20(%rbp) |
看到这里是不是有点意思了,这几行通过引用修改变量值的代码转换成汇编代码以后,居然和之前通过指针修改变量值的汇编代码一模一样。咦?仿佛发现了引用的本质呀!
常量引用
在传统C++中我们知道,引用变量不能引用一个右值,但是常引用可以办到这一点,测试代码如下:
1 | const int& a = 6; |
转换成汇编代码如下:
1 | movl $6, %eax //把立即数放到寄存器%eax中 |
这段代码的翻译结果与前面指针变量的例子很像,首先有一个变量(匿名变量)来存储值,然后是一个新的内存地址来保存之前变量的地址。
右值引用
右值引用需要C++11才能使用,与常引用对比的优点就是可以修改右值,实际上我认为还是修改的左值!测试代码如下:
1 | int&& a = 6; |
转换成汇编代码如下:
1 | movl $6, %eax //把立即数放到寄存器%eax中 |
这段汇编代码与常量引用相比只缺少赋值的部分,与左值引用相比几乎一样,只有在最开始立即数6的处理上有一点点差异,是不是感觉很神奇?
一点点惊奇
对比了前面这些代码的汇编指令后有没有什么想法?什么常量引用,什么右值引用,这些不过都是“愚弄”程序员的把戏,但这些概念的出现并不是为了给程序员们带来麻烦,相反它们的出现使得程序编写更加可控,通过编译器帮助“粗心”的开发者们先暴露了一波问题。
通过汇编代码来看,常量引用其实引用的并非常量,而是引用了一个变量;右值引用引用的也并非右值,同样是一个保存了右值的变量。这年头常量都能变,还有什么不能变的呢?
来看看下面这段代码,仔细想想常量真的变了吗?运行之后各个变量的值是多少呢?
1 | const int a = 6; |
这段代码运行之后的打印结果:a=6, b=2, c=6,变量a作为一个常量没有被改变,貌似常量还是有点用的,哈哈~
这段代码转换成汇编代码如下:
1 | movl $6, -28(%rbp) |
通过汇编来看你会发现,其实变量a的值已经通过指针 p
修改过了,只不过后面引用a变量的地方,因为它是常量,直接使用立即数6替换了。
改写一下代码,将常量6换成一个变量:
1 | int i = 3; |
转换成汇编代码为:
1 | movl $3, -28(%rbp) |
这段代码运行的结果为:i=3, a=2, b=2, c=2,看来常量也禁不住我们这么折腾啊
所以从这一点可以看出C++代码中无常量,只要是定义出的变量都可以修改,而常量只是给编译器优化提供一份指导,比如可以把一些字面量在编译期间替换,但是运行时的常量还是能改的。
总结
- 左值和右值更像是容器与数据的关系,不过C++11提出的将亡值的概念又模糊这两者的界限,将亡值可以看成是即将失去容器的数据
- 在Ubuntu16.04、GCC5.4.0的环境下,通过左值引用和指针修改一个变量值生成的汇编代码完全一致
- C++11中右值引用与常量引用生成的汇编代码一致,与左值引用生成的代码只在初始化时有一点差异
- 常量并非不可修改,它只是一种“君子协定”,你要知道什么情况下可以改,什么情况下绝对不可以改
const_cast
目的并不是让你去修改一个本身被定义为const的值,这样修改后果是可能是无法预期的,它存在的目的是调整一些指针、引用的权限,比如在函数传递参数的时候
身上若无千斤担,谁拿生命赌明天~
世间唯一不变的就是变化2021-7-5 00:36:29