从汇编的角度理解什么是引用

C++中引入的引用类型,给我们带来了很大的方便。通过向函数传递引用,我们既可以享受像传递指针一样直接修改变量值的优点,又避免了空指针和野指针造成的问题。在日常开发中我们应该尽量使用引用,避免使用指针。但是引用到底是什么,看起来好像引用跟指针有着千丝万缕的联系,同时两者又有很大的差别,那么引用跟指针到底是什么关系呢?教材上通常会说,引用就是变量的别名,但是光看这句话可能还是不太明白引用的本质。其实按照我的理解引用可以看做一种特殊的指针,在这里做一个总结。

0.指针和引用的区别

指针和引用的区别主要有以下几点:

  1. 指针可以先定义后绑定到(指向)某个对象,并且可以置为NULL;引用必须在定义的时候绑定到某对象。
  2. 指针可以改变指向的对象,引用在不能改变绑定的对象。(有没有觉得1、2两个特点跟const指针很像?)
  3. 通过引用可以像被绑定的对象本身一样操作,指针不可以。
  4. 对指针进行sizeof操作得到的是指针本身占用的内存大小,32位系统是4字节,64位系统是8个字节; 对引用进行sizeof操作得到的是被绑定到的变量占用的内存大小。
  5. 指针可以有二级、三级等多级指针,引用没有。

1.函数传引用参数传递的是什么?

函数传指针参数相信大家都很清楚。那么函数传引用参数,到底传递的是什么呢?来看一个例子:

#include<iostream>
void passByRefrence(int &a, int &b);
using namespace std;
int main()
{
    int a = 1, b = 2;
    printf("%p %p\n", &a, &b);
    passByRefrence(a, b);
    return 0;
}
void passByRefrence(int &a, int &b)
{
    printf("%p %p\n", &a, &b);
}

这段代码的执行结果是:

$ ./a.out
000000000061fe4c 000000000061fe48
000000000061fe4c 000000000061fe48

从结果可以看出来,在passByRefrence函数中的a,b变量地址和main函数中定义的a,b变量地址是一样的,貌似传引用就是传递的变量地址。也许上面的结果不能让你信服,那么我们看一个更有说服力的例子。

#include<iostream>
using namespace std;
int add(int *a, int *b)
{
    return *a + *b;
}
int add(int &a, int &b)
{
    return a + b;
}
int main()
{
    int a = 2, b = 3;
    add(a, b);
    add(&a, &b);
    return 0;
}

我们把这段代码编译后得到的可执行文件进行反编译一下看看结果:

TIPS:反编译结果可以用objdump -d a.exe > a.s得到

反编译得到的文件很长,在这里我们只截取两个add函数反编译的结果

// int add(int *a, int *b);
000000000040164e <_Z3addPiS_>:
  40164e:	55                   	push   %rbp
  40164f:	48 89 e5             	mov    %rsp,%rbp
  401652:	48 89 4d 10          	mov    %rcx,0x10(%rbp)
  401656:	48 89 55 18          	mov    %rdx,0x18(%rbp)
  40165a:	48 8b 45 10          	mov    0x10(%rbp),%rax
  40165e:	8b 10                	mov    (%rax),%edx
  401660:	48 8b 45 18          	mov    0x18(%rbp),%rax
  401664:	8b 00                	mov    (%rax),%eax
  401666:	01 d0                	add    %edx,%eax
  401668:	5d                   	pop    %rbp
  401669:	c3                   	retq  

// int add(int &a, int &b)
000000000040166a <_Z3addRiS_>:
  40166a:	55                   	push   %rbp
  40166b:	48 89 e5             	mov    %rsp,%rbp
  40166e:	48 89 4d 10          	mov    %rcx,0x10(%rbp)
  401672:	48 89 55 18          	mov    %rdx,0x18(%rbp)
  401676:	48 8b 45 10          	mov    0x10(%rbp),%rax
  40167a:	8b 10                	mov    (%rax),%edx
  40167c:	48 8b 45 18          	mov    0x18(%rbp),%rax
  401680:	8b 00                	mov    (%rax),%eax
  401682:	01 d0                	add    %edx,%eax
  401684:	5d                   	pop    %rbp
  401685:	c3                   	retq   

对比两段代码可以看到,两个add函数的汇编代码一模一样,没有任何区别。也就是说传指针和传引用在汇编层面上的实现是一样的,传引用就相当于传指针。第二段代码是add函数传引用版本的实现,rbp + 0x10位置存储的是变量a的内存地址,rbp + 0x18位置存储的是变量b的内存地址。

2. 引用是否占用内存?

这个问题貌似有点奇怪,引用是变量的别名,怎么会占用内存呢?说不清楚,还是看看汇编代码吧!我们把add函数的传引用版本修改一下:

int add(int &a, int &b)
{
    int &p = a;
    return p + b;
}

重新反编译得到add函数的反汇编实现,为了方便对比,我把上一个版本也放在这里 :

// 版本1
000000000040166a <_Z3addRiS_>:
  40166a:	55                   	push   %rbp
  40166b:	48 89 e5             	mov    %rsp,%rbp
  40166e:	48 89 4d 10          	mov    %rcx,0x10(%rbp)
  401672:	48 89 55 18          	mov    %rdx,0x18(%rbp)
  401676:	48 8b 45 10          	mov    0x10(%rbp),%rax
  40167a:	8b 10                	mov    (%rax),%edx
  40167c:	48 8b 45 18          	mov    0x18(%rbp),%rax
  401680:	8b 00                	mov    (%rax),%eax
  401682:	01 d0                	add    %edx,%eax
  401684:	5d                   	pop    %rbp
  401685:	c3                   	retq   

// 版本2
000000000040166a <_Z3addRiS_>:
  40166a:	55                   	push   %rbp
  40166b:	48 89 e5             	mov    %rsp,%rbp
  40166e:	48 83 ec 10          	sub    $0x10,%rsp           // 分配16字节的内存
  401672:	48 89 4d 10          	mov    %rcx,0x10(%rbp)
  401676:	48 89 55 18          	mov    %rdx,0x18(%rbp)
  40167a:	48 8b 45 10          	mov    0x10(%rbp),%rax      
  40167e:	48 89 45 f8          	mov    %rax,-0x8(%rbp)     // 保存变量a
  401682:	48 8b 45 f8          	mov    -0x8(%rbp),%rax      
  401686:	8b 10                	mov    (%rax),%edx
  401688:	48 8b 45 18          	mov    0x18(%rbp),%rax
  40168c:	8b 00                	mov    (%rax),%eax
  40168e:	01 d0                	add    %edx,%eax
  401690:	48 83 c4 10          	add    $0x10,%rsp
  401694:	5d                   	pop    %rbp
  401695:	c3                   	retq   

对比两个版本的汇编实现,可以看到第二个版本多了几条指令。第3条指令sub $0x10, %rsp将栈指针减小16,也就是在栈中分配了16个字节的内存(其实8个字节已经足够,但是为了内存对齐,申请了16个字节内存,这个暂不讨论)。第6条指令将rbp + 0x10内存的内容送入rax,这个内容也就是变量a的地址,第7条指令将寄存器rax的值送入rbp - 0x8位置,也就是说这个内存位置保存了变量a的地址,rbp - 0x8其实就是引用p。从这个结果可以看出来,引用保存的就是地址,是需要占用内存的。

THE END

全部评论

相关推荐

10-13 17:47
门头沟学院 Java
wulala.god:图一那个善我面过,老板网上找的题库面的
点赞 评论 收藏
分享
ArisRobert:统一解释一下,第4点的意思是,公司按需通知员工,没被通知到的员工是没法去上班的,所以只要没被通知到,就自动离职。就是一种比较抽象的裁员。
点赞 评论 收藏
分享
1 3 评论
分享
牛客网
牛客企业服务