您现在的位置是:首页 >技术教程 >【详解C++中的引用】网站首页技术教程

【详解C++中的引用】

在肯德基吃麻辣烫 2023-05-26 16:00:02
简介【详解C++中的引用】


一、什么是引用

引用就是给一个变量取别名。

注意:这个引用不会新开辟一块空间,而是和原来的变量公用一块空间。

举个例子:李逵,在家称为"铁牛",江湖上人称"黑旋风"。
在这里插入图片描述

二、引用规则

引用规则:引用实体类型+&+引用别名 = 引用实体。

比如下面:

int main()
{
	int a = 10;
	//引用
	int& ba = a;
	
	ba = 20;
	printf("%d ", a);

	return 0;
}

上面代码为例:

引用对象类型是int + & + 引用别名(ba) = 引用对象(a)

C++中的 “&”符号跟类型在一起是不在是取地址,而是”引用“。

现在ba这个就是a的别名,和a是同一块内存空间,改变了ba的内容,就等于改变了a的内容。

同时,一个变量可以有多个引用。

相当于一个人可以有多个别名一样。

就像是:假如你在家被叫做小红,在外面被叫燕燕。你的妈妈叫小红吃饭,然后小红去吃饭了,那燕燕是不是也吃了,你是不是也吃了呢?

三、引用特性

1.引用类型必须是和引用实体是同一类型。

比如:
在这里插入图片描述
报错的原因是:引用对象的类型和引用实体的类型不一致。

引用的对象a是int类型,而给它取别名却是double类型,这是不允许的。

2.引用在定义时必须初始化

比如:

在这里插入图片描述
这也是不允许的。

3. 引用一旦引用一个实体,就不能引用其他实体。

比如:

int main()
{
	int a = 10;
	//引用
	int& pa = a;

	int x = 20;
	pa = x;

	return 0;
}

这段代码,有错误吗?

没有错误,但是pa = x这行代码,并不是引用更改,因为前面已经提过:

引用一旦引用一个实体,就不能引用其他实体。

所以这行代码的意思是:将x 的值赋值给pa。

此时a的值是20了。

四、使用场景

1.做函数参数

引用可以做函数的参数,作用相当于指针,可以改变变量本身。

比如:

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

int main()
{
	int a = 10;
	int b = 20;
	printf("交换前:a = %d ,b = %d
", a, b);
	Swap(a, b);
	printf("交换后:a = %d ,b = %d
", a, b);
	return 0;
}

传参传一个实参过去,但是接收时使用引用,此时该引用就是形参的别名,形参的别名改变会改变形参本身。

这个是使用引用一个好的地方。

做返回值的作用:
1.做输出型参数,节省空间。
我们传形参时,用引用接收,也不再需要使用地址,不再需要开辟指针空间来接收,直接改变引用即可改变形参。

2. 提高效率,当传过来的形参是大对象/深拷贝对象时,能够极大地提高效率。

2.做返回值

先看不用引用做返回值,用普通的返回值:


案例1,错误代码
int test()
{
	int n = 10;
	n++;

	return n;
}

int main()
{
	int ret = test();

	printf("%d
", ret);
	return 0;
}

这样的返回值是我们常见的返回值。
这里需要提一点:
test函数并不是直接返回n的。

因为test函数在调用结束后会销毁它所在的栈帧,连同n会一起销毁,所以编译器会先保存n的值到一个寄存器中,再销毁栈帧,然后返回寄存器的值给ret。

过程如下:
在这里插入图片描述
所以就出现了一个问题。

当我们用上面的代码,返回的是n的引用(别名)时,这就不安全了。因为返回的是n的引用,不会创建临时空间给n,而是直接返回n。 但是返回之后n所在的函数栈帧会被销毁,所以连同n一起销毁了,但是此时ret是n这块已经不属于自己的空间的拷贝,所以ret是违法的。

打印出来的ret,可能是随机值,也可能是n原来的值,但如果是n原来的值,这只是侥幸,因为n原来的空间暂时没有被使用。但如果n这块空间被其他函数使用了,此时ret就有可能是随机值。

所以在上面的例子中,不能使用引用来返回。

再看下面的例子:

案例2 ,正确代码
int& test()
{
	static int n = 10;
	n++;
	return n;
}

int main()
{
	int ret = test();
	printf("%d
", ret);
	return 0;
}

注意,n是被static修饰过的变量。此时可以用引用进行返回了,因为函数test的销毁不会销毁n,n是在静态区开辟的空间,而函数是在栈区开辟的空间,两者互不影响。

返回n的引用是绝对安全的。

再看下面:

案例3,错误代码
int& test()
{
	int n = 10;
	n++;
	return n;
}

int main()
{
	int& ret = test();
	printf("%d
", ret);
	return 0;
}

此时与案例二相似,但是n不是被static修饰过的,而且ret也是引用,相当于返回n的引用后,再用引用接收n的引用,此时ret也还是n的别名,而n是在栈区开辟的空间,销毁后,此时n的空间不再属于自己,打印ret,相当于打印不属于自己的n,这是违法的行为。

当这段栈空间被其他东西用之后,n的值可能是随机值了。
在这里插入图片描述

再看下面的案例:

案例4,正确代码
int& test()
{
	static int n = 10;
	n++;
	return n;
}

int main()
{
	int& ret = test();
	printf("%d
", ret);
	return 0;
}

此时n 是被static修饰过的,所以栈帧空间的销毁不会影响n,打印ret(ret是n的引用),会正确打印出来。

总结:
1.使用引用做返回值时,引用返回不会开辟临时空间保存返回值
2.而不管改变量是在栈区还是在静态区,不用引用都会开辟临时空间保存返回值。
但是使用引用必须保证返回值是绝对的安全。

五、常引用

1.在引用的过程中,权限不能放大,只能缩小或平移。
1.在引用的过程中,权限不能放大,只能缩小或平移。
1.在引用的过程中,权限不能放大,只能缩小或平移。

案例1:
错误示例

int main()
{
	//权限不能放大,不正确
	const int a = 0; // 常变量,不可修改
	int& b = a; // 起了一个别名,必须也是常引用
	
	return 0;
}

a是一个被const修饰后的常变量,不可修改。
而b是a的引用,此时b并没有被const修饰,意味着b可以修改,但是a已经不能被修改了,b是a的别名同样不可修改,这是规定。

所以权限不能放大

案例2:
正确实例

int main()
{
	const int a = 0; // 常变量,不可修改
	//权限的平移
	const int& b = a;
	return 0;
}

变量a 和引用b都被const修饰了,它们的权限是一样的,所以权限可以平移。

案例3:
正确案例:

int main()
{
	//权限可以缩小
	int g = 0;
	const int& h = g;
	return 0;
}

g是一个变量,h是g的引用,但是h被const修饰后,意味着h不能修改。
所以这是一个权限的缩小。

2.只要发生类型转换,都会产生临时变量。
而临时变量不可修改,具有常性。

2.只要发生类型转换,都会产生临时变量。
而临时变量不可修改,具有常性。

2.只要发生类型转换,都会产生临时变量。
而临时变量不可修改,具有常性。

类型转换包括:隐式类型转换,截断,整型提升等。

比如:

int main()
{
	double d = 1.1;
	int a = d;

	return 0;
}

这里会发生隐式类型转换。d是double类型拷贝给a ,会在中间生成一个int类型的临时变量,然后把d放入该临时变量中,然后该临时变量再拷贝给a。所以把d拷贝给a是不会改变d本身的。
在这里插入图片描述
再看下面的代码:

错误示例
int main()
{
	double d = 1.1;
	int& a = d;
	return 0;
}

为什么a不能作为d的引用?
本质上还是权限放大的问题。
d是double类型的变量,a是int类型的引用,中间创建一个int类型的临时拷贝,而临时拷贝具有常性,给了a之后,a是引用,不具有常性,就相当于权限的放大。

正确示范
int main()
{
	double d = 1.1;
	const int& a = d;
	return 0;
}

加了const修饰后,引用a就具有了常性,相当于权限的平移了。

ps:为什么类型转换会产生临时变量?

看下面的例子:

int main()
{
	int a = 1;
	double b = 1.1;
	if (b > a)
	{
		cout << "hehe" << endl;
	}
	return 0;
}

此时会打印hehe,因为表达式左右两边如果类型不同,会发生整型提升或截断。

这里b和a相比,a会发生整型提升到double类型再与b比较。

而整型提升的过程会生成一个临时变量,这个临时变量就是a提升后的结果(并不是a本身提升)。

所以比较前后a和b的值都不会发生改变。

六、引用和指针的区别

从汇编的角度看,引用的底层也是用指针实现的。
在这里插入图片描述

但是在语法层面,引用不开空间,指针会开辟一块空间。

总结

引用和指针的不同点:

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
int main()
{
	int a = 10;
	//指针存储a的地址
	int* pa = &a;
	
	//b是a的引用(别名)
	int& b = a;
	return 0;
}
  1. 引用在定义时必须初始化,指针没有要求。
int main()
{
	int a = 1;

	//引用必须初始化
	int& b = a;

	//指针可不初始化
	int* p;
	return 0;
}
  1. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
int main()
{
	int a = 10;
	int b = 20;
	int& d = a;

	//指针可以改变指向的对象
	int* p = &a;
	p = &b;

	//但是引用不可改变指向的对象
	//这个并不是改变引用d的实体对象,而是把b拷贝给d
	d = b;
	return 0;
}
  1. 没有NULL引用,但有NULL指针。
int main()
{
	//可以存在空指针
	int* p = NULL;
	//不存在空引用,引用必须初始化一个实体
	//错误代码
	int& b;
	return 0;
}
  1. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
int main()
{
	double a = 10;
	
	double* p = &a;
	cout << sizeof(p) << endl;

	double& b = a;
	cout << sizeof(b) << endl;
	return 0;
}

在这里插入图片描述

  1. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
    在这里插入图片描述

  2. 有多级指针,但是没有多级引用。

  3. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。

  4. 引用比指针使用起来相对更安全。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。