【C++】一篇文章搞懂C++中的引用

本文详细介绍了C++中的引用概念,包括引用的初始化、权限控制(常引用和权限放大/缩小),类型转换以及引用在函数参数传递和返回值中的使用。同时对比了引用与指针的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引用概念

  引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会给变量开辟内存空间,它和它所引用的变量共用同一块内存空间
  比如,孙悟空、齐天大圣和孙行者。

类型& 引用变量名(对象名) = 引用实体

int main()
{
	int a = 10;
	int& ra = a;//定义引用类型
	cout << "a = " << a << endl;
	cout << "ra = " << ra << endl;
	return 0;
}

在这里插入图片描述
注意:引用类型必须和引用实体是同一种类型的

引用特性

  1. 引用在定义时必须初始化
  2. 一个变量可以用多个引用(就和一个人可以有多个称号一样)
  3. 在当前引用的声明周期内,引用一旦引用一个实体,就再也不能引用其他实体

int main()
{
	int a = 10;
	//int& ra;这条语句编译时会报错,因为该引用在定义时没有初始化
	int& ra = a;
	int& rra = ra;
	printf("a的地址为%p\n", &a);
	printf("ra的地址为%p\n", &ra);
	printf("rra的地址为%p\n", &rra);
	return 0;
}

在这里插入图片描述

可以看到,a、ra和rra虽然名称不同,但都是同一块空间的标识符

常引用

引用中的权限问题

int main()
{
	const int a = 10;
	int& ra = a;
}

当运行时,会出现报错
报错
  这里报错是因为引用(取别名)不能放大权限,那么什么叫做放大权限
  原来用const修饰的变量a拥有了常属性,不能再被任意修改。
  当别的引用变量来引用这个不能被修改的a时,这个引用变量并没有被const修饰,即ra可以被修改,相当于修改权限被放大了(以前不能被修改,但是换了名字就可以被修改)
  这种权限放大的限制只存在于引用,对于指针之间的赋值是不存在的
  所以,要想正确引用,就应该在引用变量前面加上const修饰,避免权限放大

int main()
{
	const int a = 10;
	const int& ra = a;
}

  加上const修饰之后,raa的属性就完全一样了(都具有常属性),都不能被修改
  另外,还要提一点的就是,虽然在使用引用的时候不可以实现权限放大,但是可以实现权限缩小。举个例子:

int main()
{
	int a = 20;
	const int& ra = a;
	return 0;
}

  在这段代码中,ra作为a的别名,但是ra不能被修改,相当于是权限被缩小了,这在引用的使用中是被允许的
  综上,在引用的使用中,权限不能放大,但是权限可以平移和缩小

引用实体为常量

引用也可以给常量取别名,当然,引用变量需要加上const修饰

int main()
{
	const int& a = 10;
	return 0;
}

引用中的类型转换

  先看一段代码

int main()
{
	double d = 12.54;
	int& rd = d;
	return 0;
}

在这里插入图片描述
  可以看到,报错显示出现了类型不匹配的问题
  但是,如果对这个代码稍微修改一下……

int main()
{
	double d = 12.54;
	const int& rd = d;
	return 0;
}

  运行会发现,这次就不会发生报错了,但是这又是为什么呢?
  这里要从类型转换说起

类型转换深度分析

int main()
{
	int x = 10;
	unsigned int y = 1;
	if (x > y)
	{
		//...
	}
	return 0;
}

  在上面代码中,当类型不同的xy进行运算时,类型会首先发生变化(不同类型的对象在进行运算符运算时类型会进行提升)
  我们知道,有符号和无符号的进行运算,有符号会优先提升为无符号的
但是,这里的类型提升并不是直接将x的类型提升为unsigned int类型,而是会生成一个临时变量,x将它的值赋值给这个临时变量,然后将这个临时变量看成是unsigned int类型再和无符号类型y进行运算
  类型转换,无论是隐式类型转换还是强制类型转换,中间都会产生临时变量,而这个临时变量具有常属性

了解到了这些,回到先前的代码

int main()
{
	double d = 12.54;
	const int& rd = d;
	return 0;
}

  与其说是rd引用了d,倒不如说是rd引用的是中间所产生的临时变量
在这里插入图片描述
  为了验证,我分别打印了d和rd的地址

int main()
{
	double d = 12.54;
	const int& rd = d;
	printf("d的地址是%p\n", &d);
	printf("rd的地址是%p\n", &rd);
	return 0;
}

在这里插入图片描述

  看到这里有人可能就要问了

int main()
{
	int a = 10;
	int& ra = a;
	return 0;
}

  为什么这里的引用不需要加上const呢?
  额,因为这里根本就没有发生类型转换啊,就没有产生常属性的临时变量呀

引用的使用场景

做参数

void swap(int& a, int& b)
{
	int tmp = a;
	a = b;
	b = tmp;
}

其效果等同于C语言的:

void swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

做返回值

int& Add(int a, int b)
{
	int c = a + b;
	return c;
}
int main()
{
	int& ret = Add(1, 2);
	cout << ret << endl;
	Add(4, 5);
	cout << ret << endl;
	return 0;
}

在VS2022下运行结果如下:
在这里插入图片描述
  有的读者可能觉得第一次打印结果为3可以理解,但是第二次打印结果为9就有点匪夷所思:明明没有用ret接收,为什么ret的值还会改变?
  其实,这两次打印的结果都应该是随机值,具体分析如下:
  当第一次函数将c的别名返回时,main函数用ret接收,于是ret就是形参c的别名,但是此时c的空间已经被回收了(函数结束,函数所占用的空间(函数的栈帧)被系统回收),所以ret实际上是引用的一块已经被回收了的空间,即第一次打印的结果是不可知的。
  当第二次调用Add函数时,因为两次的Add函数大小相同,且原来的空间并没有被使用,所以第二次Add使用的空间就是第一次Add使用过后的空间,第二次Add函数中的形参c的地址又恰好是原来形参c的地址,这里我做一个验证

  可以看到,两次产生的形参c的地址是完全一样的,又因为ret是c的别名,所以,形参c所对应的内存空间其实也就是ret所对应的内存空间,形参c的值发生什么变化,只要ret是c的引用,那么ret也对应要发生改变
所以,在将引用作为返回值时,需要注意:
  如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回

引用和指针的区别

引用在语法概念上就是一个别名,没有独立空间,和其引用实体共用一块空间
在这里插入图片描述
底层实现上实际上是有空间的,因为引用是按照指针方式来实现
在这里插入图片描述
引用和指针的不同点:

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
  2. 引用在定义时必须初始化,指针没有要求
  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  4. 没有NULL引用,但有NULL指针
  5. 在sizeof中含义不同引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  6. 引用自加即引用的实体增加1指针自加即指针向后偏移一个类型的大小
  7. 有多级指针,但是没有多级引用
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  9. 引用比指针使用起来相对更安全
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值