右值引用着实让人很挠头,而他又很重要,看多了“右值是短暂的,左值是持久的”这种正确但是让人摸不到头脑的话,我觉得很有必要从右值引用和左值引用实现的层面进行一次区分。
首先明确一点,左值引用和右值引用,都是指针。
其实说白了他们都是编译器给我们提供的“功能”,也就是说,编译器在我们看不到的地方做了很多别的事情,让一个指针的用法变得如此丰富多彩。但是不管多么丰富多彩,他也只是一个指针而已。
先来看左值引用。编译器对他做的操作是“自动提领”,就是自动加个*操作。
dword ptr[x] = *x
int a;
int&x=a;
009913DE lea eax,[a] lea是取地址指令,把a的地址放到eax寄存器里
009913E1 mov dword ptr [x],eax 把eax寄存器里的值放到
int*p=&a;
009913E4 lea eax,[a]
009913E7 mov dword ptr [p],eax
来看看汇编代码,编译器做了什么一览无遗。可以说,完 全 一 致。不管是x还是p,都只不过是一个指向a的指针罢了。
所以左值引用更像一个常量指针,int *const,它和常量指针唯一的区别就在于他不用每次都让我们自己写*了,除此以外真的没什么区别。
值得一说的还有常量引用直接由右值初始化的事情,右值是什么等会再说,总之下面的1就是一个右值。
int a=1;
00EF13DE mov dword ptr [a],1
const int& x=1;
00EF13E5 mov dword ptr [ebp-20h],1
00EF13EC lea eax,[ebp-20h]
00EF13EF mov dword ptr [x],eax
在这里首先编译器把1赋值给了一个无名的变量,然后又把这个无名变量的地址给了x。再想想onst int&x就是const int* const x,变量的值不会改变,指针值的变量不会改变,等于说这个x就和1彻底的绑在了一起,中间的那个无名变量永远不会被外界接触到。
再来说说右值的概念,判断右值和左值的最好方法就是,看它能不能取地址。
int a=1;
010913BE mov dword ptr [a],1
int b=1;
010913C5 mov dword ptr [b],1
int c=a+b;
010913CC mov eax,dword ptr [a]
010913CF add eax,dword ptr [b]
010913D2 mov dword ptr [c],eax
在这里a+b就是一个右值,它是活在寄存器里的一个值,他在内存里根本没有存在的位置,你无法对它取地址,这就是个右值。
int f()
{
int a=3;
return a;
}
int main ()
{
int x=f();
}
int x=f();
009517DE call f (09511D6h)
009517E3 mov dword ptr [x],eax
函数的返回值是存在寄存器里的,只要是活在寄存器里的变量,都是右值。所以回想一下计算机是怎么在汇编层面进行运算的,就很容易判断什么是右值,什么是左值。
现在来看看右值引用,这里先说结论右值引用就是一个指向一个匿名变量的指针。还记得const int&x吗,刚才说那个中间的匿名变量永远不会被接触,现在右值引用就是给了外界接触这个匿名变量的机会。
int&& x=f();
00C729EE call f (0C711D6h)
00C729F3 mov dword ptr [ebp-14h],eax
00C729F6 lea eax,[ebp-14h]
00C729F9 mov dword ptr [x],eax
可见初始化一个右值引用其实是开辟了两块空间,一块是右值引用类型那么大的匿名变量,一块是指向这个匿名变量的指针。初始化的时候先从一个返回右值的寄存器中把对应的右值赋值给匿名变量,再把匿名变量的地址取出来,赋值给x这个指针。
x=3;
009829FC mov eax,dword ptr [x] 取x中存的地址,放到eax
009829FF mov dword ptr [eax],3 把3赋值给eax存的地址指向的内存空间
改变右值引用值的过程也分为两步,取出指针x的值,也就是匿名变量的地址,把右值赋值给地址所指的匿名变量。
看看汇编代码,和const int&何其相似,改变右值引用的过程就是改变匿名变量值的过程。现在想想在书本上看的那些话“右值引用延长了右值的生命周期”,直到我们看透了右值引用的本质才能理解这话到底是什么意思。右值被放到了一个变量里,那当然就是避免了被从寄存器里扔出去就消失的命运了,他已经是一个有内存空间的变量了。