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

一.引言

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

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++语言层面去理解,由编译器负责实现这些特征。不能将引用放在其对应的汇编实现里去理解,否则会产生困惑。

       也可以换一个角度理解:引用传参时分配的空间是给一个临时变量的,而不是给引用类型变量的(这就对应了不分配内存的特征)。编译器为了实现引用,自动产生了一个临时指针变量。

  • 30
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
符合类型是指由基本数据类型组成的数据类型,常见的符合类型有数组、结构体和联合体等。其中,数组、指针和引用C++中常用的符合类型,在下面的内容中将介绍它们的知识点和示例代码。 1. 数组 数组是一种存储相同类型数据的集合,其中每个元素都有唯一的下标。数组的定义方式为:数据类型 数组名[数组长度],例如: ```c++ int a[5]; // 定义一个由5个int类型元素组成的数组a ``` 数组的下标从0开始,可以通过下标访问数组中的元素,例如: ```c++ a[0] = 1; // 给数组a的第一个元素赋值为1 cout << a[0]; // 输出数组a的第一个元素 ``` 数组的长度必须是一个常量表达式,即在编译时就可以确定数组长度,例如: ```c++ const int len = 5; // 定义一个常量 int b[len]; // 可以使用常量表达式作为数组长度 int c[len + 2]; // 也可以使用常量表达式计算得到数组长度 int d[5 + 3]; // 同样可以在定义时直接使用常量表达式 ``` 2. 指针 指针是一种特殊的变量,存储的是内存地址。可以通过指针间接访问内存中的数据。指针的定义方式为:数据类型 *指针变量名,例如: ```c++ int *p; // 定义一个int类型指针变量p ``` 指针变量必须初始化,否则指向的内存地址是不确定的,可能会导致程序崩溃。指针的初始化方式有以下几种: ```c++ int a = 1; int *p = &a; // 指针p指向变量a的内存地址 int *q = new int; // 动态分配一个int类型内存,指针q指向该内存地址 int *r = new int(10); // 动态分配一个int类型内存并初始化为10,指针r指向该内存地址 ``` 通过指针访问内存中的数据需要使用解引用运算符*,例如: ```c++ int a = 1; int *p = &a; // 指针p指向变量a的内存地址 *p = 2; // 修改指针p指向的内存中的数据 cout << a; // 输出2 ``` 指针还可以进行算术运算,例如: ```c++ int a[5] = {1, 2, 3, 4, 5}; int *p = a; // 指针p指向数组a的第一个元素 p++; // 指针p指向数组a的第二个元素 cout << *p; // 输出2 ``` 3. 引用 引用是指已存在变量的别名,可以看作是变量的一个别名,与指针的区别在于引用不需要使用解引用运算符*访问内存中的数据。引用的定义方式为:数据类型 &引用变量名 = 原变量名,例如: ```c++ int a = 1; int &r = a; // 引用r是变量a的别名 ``` 引用必须在定义时初始化,不能改变引用的指向,例如: ```c++ int a = 1; int b = 2; int &r = a; // 引用r是变量a的别名 r = b; // 修改了a的值,a的值变为2 ``` 引用与原变量是同一个变量,修改引用的值会同时修改原变量的值,例如: ```c++ int a = 1; int &r = a; // 引用r是变量a的别名 r = 2; // 修改r的值,a的值也变为2 cout << a; // 输出2 ``` 引用还可以作为函数参数传递,可以避免复制大量数据的开销,例如: ```c++ void swap(int &a, int &b) { // 引用作为函数参数 int tmp = a; a = b; b = tmp; } int main() { int a = 1, b = 2; swap(a, b); // 传递引用参数 cout << a << " " << b; // 输出2 1 return 0; } ``` 以上就是数组、指针和引用的知识点和示例代码。需要注意的是,指针和引用都是C++中的高级特性,需要掌握它们的正确使用方法,避免出现潜在的错误。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈工程师修炼日记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值