一.引言
函数的参数传递方式主要有传值和传指针。
1.传值
在函数域中为参数分配内存,而把实参的数值传递到新分配的内存中。它的优点是有效避免函数的副作用。
例如:
#include <iostream>void swap_val(int x,int y)
{int tmp;tmp = x;x = y;y = tmp;
}int main(int argc, char** argv)
{int a = 20;int b = 30;swap_val(a,b);return 0;
}
2.传指针
这里有两种传递方式。
(1)指针传递
例如:
#include <iostream>void swap_pointer(int *x,int *y)
{int tmp;tmp = *x;*x = *y;*y = tmp;
}int main(int argc, char** argv)
{int a = 20;int b = 30;swap_pointer(&a,&b);return 0;
}
(2)引用传递。
例如:
#include <iostream>void swap_ref(int &x,int &y)
{int tmp;tmp = x;x = y;y = tmp;
}int main(int argc, char** argv)
{int a = 20;int b = 30;swap_ref(a,b);return 0;
}
这里将引用也归类为指针,是有依据的。下面详细分析、寻找引用的本质。
二.什么是引用?
引用(reference)是C++中一种新的导出型数据类型,它又称别名(alias)。
引用不是定义一个新的变量,而是给一个已经定义的变量重新起一个别名,也就是C++系统不为引用类型变量分配内存空间。引用主要用于函数之间的数据传递。
定义的格式为:
类型 &引用变量名 = 已定义过的变量名;
例如:
int main(void)
{int a;int &b = a;
}
三.指针传参和引用传参比较
引用本质上也是指针。
int val = 10;
int *a = &val;
int &b = val;
对于上面代码,大家可能会有这样的疑惑:指针变量a保存了变量val的地址,而声明为引用类型的变量b也保存了val的地址,它们不是一样的吗?究竟区别在哪?引用又称为别名,别名又如何理解?
有这种疑惑,是因为我们的思维一直停留在高级语言的层面。要想彻底明白它们的区别,我们的思维要下沉到汇编语言层面。
3.1 调试分析
3.1.1 引用类型变量的大小
测试代码如下:
3.1.2 变量的地址
为了更好区别,调试代码中没有使用重载,而是在函数名上做了区分。
3.1.2.1 指针传参
代码如下:
#include <iostream>using namespace std;void swap_pointer(int *x,int *y)
{int tmp;tmp = *x;*x = *y;*y = tmp;
}void swap_ref(int &x,int &y)
{int tmp;tmp = x;x = y;y = tmp;
}int main(int argc, char** argv)
{int a = 20;int b = 30;swap_pointer(&a,&b);return 0;
}
调试结果如下图所示。
左侧窗口中显示了实参a、b的地址和函数域内x、y的地址,由图可知,它们地址不同。
3.1.2.2 引用传参
代码如下:
#include <iostream>using namespace std;void swap_pointer(int *x,int *y)
{int tmp;tmp = *x;*x = *y;*y = tmp;
}void swap_ref(int &x,int &y)
{int tmp;tmp = x;x = y;y = tmp;
}int main(int argc, char** argv)
{int a = 20;int b = 30;swap_ref(a,b);return 0;
}
调试结果如下图所示。
左侧窗口中显示了实参a、b的地址和函数域内x、y的地址,由图可知,它们地址相同。
3.1.2.3 结论
从地址来看,指针传参与引用传参的确有区别。从这一角度来看,引用的确就是已存在变量的别名。
但要注意,调试器显示的地址是C++语言级别的地址,即它是虚拟地址。即引用在C++开发人员看来,a和x、b和y使用的是同一个虚拟地址。
至此,可以回答以下两个问题。
1.为什么称引用为别名?
这是C++语言层面的概念。因为引用类型变量与被引用的对象使用同一个虚拟地址空间,所以称为别名。
2.为什么说C++系统不为引用类型变量分配内存空间?
这也是在C++语言层面的概念。与第1个问题一样,因为引用类型变量与被引用的对象使用同一个虚拟地址空间,不需要重新开辟空间。
但是,引用真的不用分配空间吗?下面继续分析。
3.1.3 汇编代码
3.1.3.1 指针传参
C++源码见3.1.2.1节。汇编代码如下。
0x0000000000401598 <+0>: push %rbp0x0000000000401599 <+1>: mov %rsp,%rbp0x000000000040159c <+4>: sub $0x30,%rsp0x00000000004015a0 <+8>: mov %ecx,0x10(%rbp)0x00000000004015a3 <+11>: mov %rdx,0x18(%rbp)0x00000000004015a7 <+15>: callq 0x40e7b0 <__main>0x00000000004015ac <+20>: movl $0x14,-0x4(%rbp)0x00000000004015b3 <+27>: movl $0x1e,-0x8(%rbp)0x00000000004015ba <+34>: lea -0x8(%rbp),%rdx0x00000000004015be <+38>: lea -0x4(%rbp),%rax0x00000000004015c2 <+42>: mov %rax,%rcx0x00000000004015c5 <+45>: callq 0x401530 <swap_pointer(int*, int*)>0x00000000004015ca <+50>: mov $0x0,%eax0x00000000004015cf <+55>: add $0x30,%rsp0x00000000004015d3 <+59>: pop %rbp0x00000000004015d4 <+60>: retq 0x0000000000401530 <+0>: push %rbp0x0000000000401531 <+1>: mov %rsp,%rbp0x0000000000401534 <+4>: sub $0x10,%rsp0x0000000000401538 <+8>: mov %rcx,0x10(%rbp)0x000000000040153c <+12>: mov %rdx,0x18(%rbp)0x0000000000401540 <+16>: mov 0x10(%rbp),%rax0x0000000000401544 <+20>: mov (%rax),%eax0x0000000000401546 <+22>: mov %eax,-0x4(%rbp)0x0000000000401549 <+25>: mov 0x18(%rbp),%rax0x000000000040154d <+29>: mov (%rax),%edx0x000000000040154f <+31>: mov 0x10(%rbp),%rax0x0000000000401553 <+35>: mov %edx,(%rax)0x0000000000401555 <+37>: mov 0x18(%rbp),%rax0x0000000000401559 <+41>: mov -0x4(%rbp),%edx0x000000000040155c <+44>: mov %edx,(%rax)0x000000000040155e <+46>: add $0x10,%rsp0x0000000000401562 <+50>: pop %rbp0x0000000000401563 <+51>: retq
3.1.3.2 引用传参
C++源码见3.1.2.2节。汇编代码如下。
0x0000000000401598 <+0>: push %rbp0x0000000000401599 <+1>: mov %rsp,%rbp0x000000000040159c <+4>: sub $0x30,%rsp0x00000000004015a0 <+8>: mov %ecx,0x10(%rbp)0x00000000004015a3 <+11>: mov %rdx,0x18(%rbp)0x00000000004015a7 <+15>: callq 0x40e7b0 <__main>0x00000000004015ac <+20>: movl $0x14,-0x4(%rbp)0x00000000004015b3 <+27>: movl $0x1e,-0x8(%rbp)0x00000000004015ba <+34>: lea -0x8(%rbp),%rdx0x00000000004015be <+38>: lea -0x4(%rbp),%rax0x00000000004015c2 <+42>: mov %rax,%rcx0x00000000004015c5 <+45>: callq 0x401564 <swap_ref(int&, int&)>0x00000000004015ca <+50>: mov $0x0,%eax0x00000000004015cf <+55>: add $0x30,%rsp0x00000000004015d3 <+59>: pop %rbp0x00000000004015d4 <+60>: retq 0x0000000000401564 <+0>: push %rbp0x0000000000401565 <+1>: mov %rsp,%rbp0x0000000000401568 <+4>: sub $0x10,%rsp0x000000000040156c <+8>: mov %rcx,0x10(%rbp)0x0000000000401570 <+12>: mov %rdx,0x18(%rbp)0x0000000000401574 <+16>: mov 0x10(%rbp),%rax0x0000000000401578 <+20>: mov (%rax),%eax0x000000000040157a <+22>: mov %eax,-0x4(%rbp)0x000000000040157d <+25>: mov 0x18(%rbp),%rax0x0000000000401581 <+29>: mov (%rax),%edx0x0000000000401583 <+31>: mov 0x10(%rbp),%rax0x0000000000401587 <+35>: mov %edx,(%rax)0x0000000000401589 <+37>: mov 0x18(%rbp),%rax0x000000000040158d <+41>: mov -0x4(%rbp),%edx0x0000000000401590 <+44>: mov %edx,(%rax)0x0000000000401592 <+46>: add $0x10,%rsp0x0000000000401596 <+50>: pop %rbp0x0000000000401597 <+51>: retq
3.1.3.3 汇编代码比较
我们使用Compare工具比较一下两者的汇编代码。如下图所示。
可以看到,它们的汇编代码实现方法是一样的。
3.1.3.4 结论
由汇编代码比较结果可知,指针传参和引用传参在汇编实现上是一样的。
所以,引用本质上也是指针,为了实现它也要分配空间存储变量地址。
因为C++并没有规定汇编语言如何实现引用,它只是提出一个逻辑上的概念,具体实现不在C++语言本身。
四.引用的本质
引用本质上是对一个const类型指针的封装,如下:
int a = 10;int &b = a;
等价于
int *const b = &a;
引用有如下特点:
1.引用没有定义,是一种关系型声明。声明它和原有某一变量(实体)的关系。
2.引用的类型与原类型保持一致,且不分配内存。与被引用的变量有相同的地址。
3.声明的时候必须初始化,一经声明,不可变更。
4.可对引用,再次引用。多次引用的结果,是某一变量具有多个别名。
总的来说,引用的特征要放在C++语言层面去理解,由编译器负责实现这些特征。不能将引用放在其对应的汇编实现里去理解,否则会产生困惑。
也可以换一个角度理解:引用传参时分配的空间是给一个临时变量的,而不是给引用类型变量的(这就对应了不分配内存的特征)。编译器为了实现引用,自动产生了一个临时指针变量。