0
点赞
收藏
分享

微信扫一扫

C++数据类型(一):一文看懂引用的本质

张宏涛心理 03-31 19:00 阅读 1

一.引言

        函数的参数传递方式主要有传值和传指针。

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   %rbp
   0x0000000000401599 <+1>:	mov    %rsp,%rbp
   0x000000000040159c <+4>:	sub    $0x30,%rsp
   0x00000000004015a0 <+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),%rdx
   0x00000000004015be <+38>:	lea    -0x4(%rbp),%rax
   0x00000000004015c2 <+42>:	mov    %rax,%rcx
   0x00000000004015c5 <+45>:	callq  0x401530 <swap_pointer(int*, int*)>
   0x00000000004015ca <+50>:	mov    $0x0,%eax
   0x00000000004015cf <+55>:	add    $0x30,%rsp
   0x00000000004015d3 <+59>:	pop    %rbp
   0x00000000004015d4 <+60>:	retq  
      
   0x0000000000401530 <+0>:	push   %rbp
   0x0000000000401531 <+1>:	mov    %rsp,%rbp
   0x0000000000401534 <+4>:	sub    $0x10,%rsp
   0x0000000000401538 <+8>:	mov    %rcx,0x10(%rbp)
   0x000000000040153c <+12>:	mov    %rdx,0x18(%rbp)
   0x0000000000401540 <+16>:	mov    0x10(%rbp),%rax
   0x0000000000401544 <+20>:	mov    (%rax),%eax
   0x0000000000401546 <+22>:	mov    %eax,-0x4(%rbp)
   0x0000000000401549 <+25>:	mov    0x18(%rbp),%rax
   0x000000000040154d <+29>:	mov    (%rax),%edx
   0x000000000040154f <+31>:	mov    0x10(%rbp),%rax
   0x0000000000401553 <+35>:	mov    %edx,(%rax)
   0x0000000000401555 <+37>:	mov    0x18(%rbp),%rax
   0x0000000000401559 <+41>:	mov    -0x4(%rbp),%edx
   0x000000000040155c <+44>:	mov    %edx,(%rax)
   0x000000000040155e <+46>:	add    $0x10,%rsp
   0x0000000000401562 <+50>:	pop    %rbp
   0x0000000000401563 <+51>:	retq   
 3.1.3.2 引用传参

         C++源码见3.1.2.2节。汇编代码如下。

   0x0000000000401598 <+0>:	push   %rbp
   0x0000000000401599 <+1>:	mov    %rsp,%rbp
   0x000000000040159c <+4>:	sub    $0x30,%rsp
   0x00000000004015a0 <+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),%rdx
   0x00000000004015be <+38>:	lea    -0x4(%rbp),%rax
   0x00000000004015c2 <+42>:	mov    %rax,%rcx
   0x00000000004015c5 <+45>:	callq  0x401564 <swap_ref(int&, int&)>
   0x00000000004015ca <+50>:	mov    $0x0,%eax
   0x00000000004015cf <+55>:	add    $0x30,%rsp
   0x00000000004015d3 <+59>:	pop    %rbp
   0x00000000004015d4 <+60>:	retq   
   
   0x0000000000401564 <+0>:	push   %rbp
   0x0000000000401565 <+1>:	mov    %rsp,%rbp
   0x0000000000401568 <+4>:	sub    $0x10,%rsp
   0x000000000040156c <+8>:	mov    %rcx,0x10(%rbp)
   0x0000000000401570 <+12>:	mov    %rdx,0x18(%rbp)
   0x0000000000401574 <+16>:	mov    0x10(%rbp),%rax
   0x0000000000401578 <+20>:	mov    (%rax),%eax
   0x000000000040157a <+22>:	mov    %eax,-0x4(%rbp)
   0x000000000040157d <+25>:	mov    0x18(%rbp),%rax
   0x0000000000401581 <+29>:	mov    (%rax),%edx
   0x0000000000401583 <+31>:	mov    0x10(%rbp),%rax
   0x0000000000401587 <+35>:	mov    %edx,(%rax)
   0x0000000000401589 <+37>:	mov    0x18(%rbp),%rax
   0x000000000040158d <+41>:	mov    -0x4(%rbp),%edx
   0x0000000000401590 <+44>:	mov    %edx,(%rax)
   0x0000000000401592 <+46>:	add    $0x10,%rsp
   0x0000000000401596 <+50>:	pop    %rbp
   0x0000000000401597 <+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++语言层面去理解,由编译器负责实现这些特征。不能将引用放在其对应的汇编实现里去理解,否则会产生困惑。

举报

相关推荐

0 条评论