对指针和引用不熟悉的可以看我前面的两篇文章指针 和引用 ,顺便也可以看看加入const关键字后的作用 const关键字 。本身对指针和引用非常熟悉的就可以直接阅读下文。
本文默认是用的编译器是gcc 4.8.4 。
首先将一下实现的细节,在实现上,引用实际和指针是一样的。至少在gcc的编译器上是一样的。事实上,C++之父Bjarne Stroustrup 也建议使用指针实现引用,对于初学者来说可能有点奇怪,下面我们来看代码:
相信读者一定能够马上读懂上述代码要干一件什么事,重载后无论是接受int指针的f函数还是接受int引用的f函数功能都是一样的,就是把传入的指针指向的值(或引用的值)置0 。那么我们来看一下在汇编上是如何实现的:
上图的汇编代码正好对应两个f函数,其中名为 _Z1fPi 的函数对应 void f (int* pi) , _Z1fRi 的函数对应 void f (int& ri) 。对应的规则很简单 , 前面的 _Zn (n为对应的数字)代表返回值,中间的f代表函数名, 后面的 Pi 和 Ri 则是参数名,用于区别函数重载。
接下来我们来看汇编的代码,因为两个函数的代码完全一样(毕竟引用就是用指针实现的),所以讲解以上面一个为准,首先是第一条指令:
push %rbp #这是一条函数都有的指令,我们知道调用函数的时候要在栈上建立这个函数,所以要把调用函数的栈底指针压入栈,而这个指针是放在rbp寄存器中。
接下来的指令:
mov %rsp,%rbp #这条指令是建立新的栈帧,将被调函数的栈顶指针放入rbp寄存器中
mov %rdi,-0x8(%rbp) #这条指令实际上就是我们熟悉的参数传递,参数放在寄存器rdi中,把它压入栈中,偏移量为 -0x8(%rbp寄存器旁的括号是指取该寄存器中的值作为地址指向的位置)。
mov -0x8(%rbp),%rax #把刚刚压入栈的那个参数再传到寄存器rax中。
movl $0x0,(%rax) #这条指令只把0这个值赋给rax寄存器中的值作为地址指向的值(括号的作用上面已经讲过了)。
pop %rbp #函数执行完成需要退栈,把原来的值放回到rbp中。
retq #这条就不用我多讲了,把ip等重要的信息改回来
讲了这么多,回归主题,指针和引用在函数调用的参数传递实现上是一样的,那么在函数内部呢?下面我们来看看main函数的汇编代码:
这次我们从40073c这段看起:
movl $0x5,-0x14(%rbp) #把5这个值压入栈中,偏移量是-0x14,即指的是 int i = 5;
lea -0x14(%rbp),%rax #把栈中偏移量为-0x14位置的地址传给寄存器rax
mov %rax,-0x10(%rbp) #把寄存器rax中的值压入栈中,偏移量为-0x10,即指的是int * pi = &i;
lea -0x14(%rbp),%rax #同上
mov %rax,-0x8(%rbp) #同上,只是指的是int & ri = i;
到这已经能看出指针和引用在函数中创建也是以一样的形式存在,即引用是以指针来实现的。
对于二级指针和指针的引用也是如出一辙:
两个重载函数的汇编代码还是一样的,只是比一级指针多了一个mov调用(把rax指向的值放入rax中),本质上指针的引用是用二级指针来实现的。所以以前在C中要用到二级指针的地方都可以用指针的引用取代。(比如要在函数中改变指针的值而不是指向的值,因为函数通过rdi寄存器传递参数,真正的指针还在调用函数的栈里。)
接下来讨论什么时候改用引用什么时候用指针,在多数情况下,他们可以替换使用,只是有一些需要注意的事项,比如指针可以置空,而引用不行。但我们依然可以进行一些绕开编译器检查的违规操作:
这种情况下引用就是一个空引用(ri = NULL ,伪代码) ,那么编译器是怎么通过这段代码的呢:
这段汇编代码的意思是把一个0值压入栈,偏移量为-0x10,(即指针pi) 再把这个值放到寄存器rax中,再把rax中的值压入栈,偏移量为-0x8,(即引用或者说另一个指针ri)。
总所周知,引用出现的其中一个原因是提高指针的抽象级别,使得程序员不需要在操作指针这种级别较低,容易出错的量,而综上所述,引用用指针来实现,则可以把引用理解为 *pi , 即指针的解引,之所以不存在空引用,是因为空引用就相当于对一个空指针解引后操作一样,就像
int *pi = 0; *pi = 100;
很明显是错误的,pi本身并没有指向一个有效值,就不存在解引后赋值了。所以我们应该避免写出上述代码。
指针和引用还有一点明显的区别就是指针可以改变指向的对象而引用不行,尽管从汇编代码上看我们可以改变引用所引的值,但实际情况却是编译器不通过,因为对于引用做去地址&操作返回的是一个右值,这也就告诉我们,当我们需要灵活改变指向对象的时候,就不得不选用指针。
对于引用而言有个很重要的功能就是const Type & ,这个常量引用使能够接受右值的,我们写出下列代码:
结果是通过编译的,那么他的汇编代码是什么呢:
从汇编的角度来看,const引用在函数的参数传递上实际上是把要传入的右值转化成一个符合要求的量(这里是int)压入栈中,再当成参数传给函数。所以这个临时变量的生命周期是调用函数前和调用函数后的阶段。
最后,希望上文能够让各位以前对指针和引用感性的认识上升到理性的认识。