C++中引用的本质到底是什么?

C++的引用到底是什么?用了这么久,还不知道它居然也是个指针…

相关文章
C++编程之命名空间、const常量的总结
C++编程之运算符重载

前段时间写过一篇《C++编程之引用的详细总结》 ,看过就知道,哦,原来引用是对象/变量的一个别名,在使用时,是直接操作对象本体,因此通过引用传参,不需要拷贝内存,效率很高。但是最近有人私下问我:“你写的倒是挺全面的,但是引用的本质到底是什么?”

因此,今天决定再深入解释一下引用。

其实 引用的本质在C++内部实现是一个指针常量。C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小和指针相同,这个过程是编译器内部实现,用户不可见。

#include<iostream>
using namespace std;

// 编译器判断是引用,会将入参自动转换成int* const ref = &a;
void Test(int &ref)
{
	ref = 10;    // ref是引用,此处会转换为 *ref = 10;
}

int main()
{
	int a = 100;
	int & b = a; //自动转换为int* const b = &a;这就说明为什么引用必须要初始化。
	b = 20; //编译器判断b是引用,自动转换为*b = 20;
	Test(a);
	return 0 ;
}

这就是引用为什么必须要初始化,因为内部转换为常指针时,需要拿到它的地址,所以必须要先初始化。
接着通过反汇编,验证两种情况是等价的。

使用引用传参

#include<iostream>

using namespace std;


void Test(int& a)
{
	a = 100;
}

int main()
{
	int b = 10;
	Test(b);
	cout << "b = " << b << endl;
	return 0;
}

运行结果
在这里插入图片描述
断点调试,进入汇编

00F52AF8 mov eax,dword ptr [a]   //dword表示的是双字,四字节。a中保存的是内存中的地址。将该地址处的4字节数据传送到eax中。
00F52AFB mov dword ptr [eax],64h //将64h的值传递给 [eax] 所指示的内存单元,也就是a的本体

在这里插入图片描述
如图显示断点到 a = 100 之前,a的值是10.,执行之后,如下图所示。
在这里插入图片描述
这样就通过操作引用,实际修改了本体的值。

使用常指针传参

#include<iostream>
using namespace std;

void Test1(int* const a)
{
	*a = 1000;
}

int main()
{
	int b = 10;
	Test1(&b);
	cout << "b = " << b << endl;
	system("pause");
	return 0;
}

运行结果
在这里插入图片描述
同样,断点调试到汇编中看看结果

001D2A98 mov eax,dword ptr [a]       //a中保存的是内存中的地址。将该地址处的4字节数据传送到eax中。同样是双字节dwod,前面引用也是双字节,说明引用内部就是常指针。
001D2A9B mov dword ptr [eax],3E8h    // 将3E8h的值传递给 [eax] 所指示的内存单元,也就是a的本体

如下图所示
在这里插入图片描述
当执行 a=1000 后,a的值也变为1000,如下图汇编代码:
在这里插入图片描述
因此,从汇编层看,引用在内部的确被转换为常指针。这就解释了引用必须要初始化的原因,初始化后,不能再修改指向。因此可以通过引用, 间接代替指针 ,比如一级指针可以直接用引用代替,二级指针可以用一级指针的引用代替,三级指针可以用二级指针的引用代替等。

总结完引用的本质,接着补充几个实际的例子。

数组的引用

void Test()
{
	int arr[10];
	int (&pArr)[10] = arr;
	for(int i = 0; i< 10;i++)
	{
		arr[i]=i;
	}
	for(int i =0; i<10;i++)
	{
		cout<<pArr[i]<<endl;
	}
}

输出结果:
0
1
2
3
...
9

指针的引用

#include<iostream>
using namespace std;

class Person
{
public:
	int age;
}

// 使用二级指针给一级指针分配内存。可以通过指针的引用简化为一级指针,如下AllocSpace2
void AllocSpace1(Person **p)
{
	*p = (Person*)malloc(sizeof(Person));
	return;
}

// 使用指针的引用传参
void AllocSpace2(Person* &p)
{
	p = (Person*)malloc(sizeof(Person));
	return;
}

int main()
{
	Person* p = NULL;
	AllocSpace1(&p);  // 取地址,传入二级指针
	AllocSpace2(p);   // 引用,直接传入一级指针本体。
	return 0;
}

// 因此,可以使用引用简化指针,即二级指针可以用一级指针的引用代替,一级指针直接用引用代替,减少指针操作。

常量的引用
如何定义常量的引用,如下代码解释

void Test()
{
	//int &a = 10; //非法操作,无法给非常量的引用赋值
	int const &b = 10; //合法操作,编译器在内部会做类似的转换。int temp=10; int const &b = temp;
}

常量的引用的应用场景是什么呢?

void Test1(int &b)
{
	cout << "b = " << b << endl;
}

int main()
{
	int a = 100;
	Test1(a);
	return 0;
}

如上代码所示,使用常量作为参数,在函数内部不会开辟新内存,节省内存空间。但是,如果在 Test1 中做如下操作,有什么影响呢?

void Test1(int &b)
{
	cout << "b = " << b << endl;
	b = 10;
}

很明显可以看出,b的值在新函数中被修改了,在main函数中再次使用时,值已经变了,这就造成了很大的问题,因此出现了常引用。

在这里插入图片描述
引入常量的引用目的是为了防止误操作,但是常量的引用能不能被修改值呢?答案是肯定可以的,如下:

void Test2()
{
	int const &ref = 10;

	int *p = (int*)&ref;
	*p = 10000;
	cout << "ref =" << ref << endl;
}

int main()
{
	Test2();
	system("pause");
	return 0;
}

在这里插入图片描述
因此大家在编写C/C++代码时,常识性的在入参位置做好const保护,除非你的入参是要被修改的,这是C/C++编码界默认的一条铁律,记住这个,才不会被大佬们蔑视哦。

  • 10
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值