【C++系列】引用与临时变量

前言


  • 🔖 分享每一次的学习,期待你我都有收获。
  • 🎇 欢迎🔎关注👍点赞⭐️收藏✉评论,共同进步!
  • 🌊 “要足够优秀,才能接住上天给的惊喜和机会”
  • 💬 博主水平有限,如若有误,请多指正,万分感谢!

请添加图片描述

☁️引用的概念

引用不是新定义一个变量,
而是给已存在变量取了一个别名。

编译器不会为引用变量开辟内存空间,
它和它引用的变量共用同一块内存空间。

说人话就是,引用就是取小名,平常说的二狗子啥的都是小名。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DAZRUOci-1646457643041)(C:\Users\Z-zp\Desktop\v2-7a1532baefa24160e68b85307754199f_r.jpg)]

就比如这个人,他的伙伴喜欢亲切地叫他绿藻头,当然也可以直接叫他索隆,但无论是叫他绿藻头还是索隆,叫的都是这个人,两种称呼,一个身体。引用也是这样的,在语法上,虽然称呼不同,但是它们都是指同一块内存空间

我们来观察一下这一段代码

#include<iostream>
using namespace std;

int main()
{
	int a = 10;
	int& b = a;

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-58igHgG3-1646457643041)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220123135154226.png)]

可以看到,我们定义的b与a具有相同的值,且他们的地址也是相同的,再次印证了b只是a的别名,b就是a

引用特性:

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体

常引用


int main()
{
	const int a = 10;
	int& x = a;         // ①  错误     正确写法:const int& x = a;
	
	int b = 10;
	const int& y = b;   // ②  正确

    int c = 0;
    double z = c  //不同类型之间的计算——>隐式转换,正确
    
	int c = 0;
	double& z = c;      // ③  错误

	int d = 0;
	const double& s = d;// ④  正确
	


	return 0;
}

① a为const修饰的常变量,x为a的别名却没有用const加以修饰,也就是说,a不能改变自己的值,但是用它的别名却能改变它的值,属于权限的放大,因此该语句无法通过编译。

②b为变量,可读可写的权限,它的别名只使用了读的权限,属于权限的缩小,这是允许的,就好比权利与义务的关系,有些权利我们虽然有,但是我们可以选择放弃,前提是我们拥有这种权利。 ①则体现了没有的权利不能随意扩大。

③与④是一组对比

我们先来看这样一段代码

int main()
{
	int a = 10;

	double b = 0;

	double c = 0;

	b = a;   //int ->double  隐式转换
	
	c = (double)a;  //强制类型转换
	

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cKA3hx9X-1646457643041)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220127113346238.png)]

不同类型之间的计算会发生隐式转换,一个变量可以进行强制类型转换,但无论是这两种中的哪一种,都是借由临时变量进行的,不会改变变量本身

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nvUFIA0D-1646457643041)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220127114652747.png)]

临时变量具有常性,其值无法被修改。

同理

	int c = 0;
	double& z = c;      // ③  错误

这里也是先将c的值交给临时变量存储,再给z,也就是说z不是c的别名,而是临时变量的别名,而临时变量无法被修改,因此z必须用const修饰。

	int c = 0;
	const double& z = c;      //  正确

我们不妨再看一下c的地址和z的地址。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2KGYfBXx-1646457643042)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220127115839508.png)]

再次印证了转换的过程产生了临时变量,z是临时变量的别名,而不是c的别名,因此z的地址是临时变量的地址。

☁️引用的作用
☁️1. 作为函数参数,更加简洁,能减少内存的消耗。

在c语言中,我们学习了指针作为函数参数,其实引用效果就类似于指针。

C语言版

#include<stdio.h>

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


int main()
{
	int a = 10;
	int b = 20;

	Swap(&a, &b);

	return 0;
}

C++版

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


int main()
{
	int a = 10;
	int& b = a;

	return 0;
}

  • 函数传值和传引用的效率比较

我们知道,引用只是一块内存空间的别名,

因此函数传引用,它并不会耗费额外的内存空间。

而实参传值时,形参只是实参的一份临时拷贝,它会在内存中开辟一块临时空间,然后拷贝实参的内容。

并且实参所占空间越大,则开辟的临时空间就越大,对性能的消耗也就越大。

#include<iostream>
#include<time.h>
using namespace std;

struct Node
{
	int a[10000];
};

void Func1(Node a)  //传值
{
	;
}

void Func2(Node& a)  //传引用
{

}

void TestEffic()
{
	Node a; 

	int begin1 = clock();

	for (int i = 0; i < 100000; i++)
	{
		Func1(a);  //传值
	}
    
	int end1 = clock();


	int begin2 = clock();
    
	for (int i = 0; i < 100000; i++)
	{
		Func2(a);  //传引用
	}
    
	int end2 = clock();

	cout << "Func1(Node) = " << end1 - begin1 << endl;
	cout << "Func2(Node&) = " << end2 - begin2 << endl;

}


int main()
{
	TestEffic();

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z6HmoVtQ-1646457643042)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220123143225343.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wPlpDbnW-1646457643042)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220123142705425.png)]

从数字上就可以清晰看到他们运行所花费的时间的差距了。

这就是由于每次引用并不需要再开空间对A进行拷贝,而传值时每次都要建立临时空间对A进行拷贝。

需要开的数组越大,则他们的差距会越大。


在讲第二个作用前,要先讲一件事——

☁️编译器是如何检查越界访问的

编译器对越界访问的检查是——抽查。

我们看这么一段代码:

int main()
{
	int a[6] = { 1,2,3,4};

	a[4] = 10;


	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ufL5n5E-1646457643043)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220127093916090.png)]

这段代码很明显越界访问了,编译器也确实检查到了,于是程序就崩了。但是如果我们再往更后面一点的内存访问又会发生什么呢?


int main()
{
	int a[4] = {1,2,3,4};  //数组下标范围 0~3

	//a[4] = 10;

	a[6] = 16;   //这次往更后面访问内存块,访问a[6]

	printf("a[6] = %d\n", a[6]);

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8ZJVMyuq-1646457643043)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220127094309450.png)]

我们看到,程序非但没有崩,我们不但越界将内存空间中的值更改,甚至还将它打印出来了,编译器也只是给了我们一个警告,并没有报错。因此,编译器对于内存的越界访问检查,确实是采取抽查的方式。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LrhssrD5-1646457643043)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220127095216813.png)]

编译器通常只会在合法内存块后面,一两个内存块的位置设立检查,因为正常情况下,这里是最容易发生越界访问的地方,如果在每个内存空间都设立检查,那样对性能的消耗太大,不现实。

就好比交警检查酒驾,不可能一天24小时设立检查点,那样对于社会的运转也有很大的消耗和不便,不科学。

因此通常会在半夜,凌晨,或是利用酒驾人的心理,在小路上设立检查点,针对这些最容易查到酒驾的情况,设立检查点。


我们继续讲引用的第二个作用。

☁️2. 引用作为返回值

如果内存空间在函数结束时不会被销毁,才可以作为返回值引用返回这块内存空间本身

我们看这么一段代码

int Add(int a, int b)   
{
	int c = a + b;
	return c; 
}


int main()
{
	int a = 1;
	int b = 2;

	int& ret = Add(a, b);  //如果返回的是c,那我们直接引用别名接收c

	cout << "ret = " << ret << endl;

	return 0;
}

可能有些同学会存在这样的误区,当我们调用Add函数后,返回的是c这个变量。

如果这样的结论成立,那么这段代码就应该是能通过编译的,但其实它不能,编译器给我们报了错误

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JWzA2Ibx-1646457643044)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220127120707397.png)]

如果我们借助前面介绍的常引用,稍微把代码改一下

	//int& ret = Add(a, b);

	const int& ret = Add(a,b);

编译就通过了,这说明返回的不是c本身,而是一个临时变量。

实际在内存中是这么个过程:

有这么两块栈帧。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dyLRsnlk-1646457643045)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220123144751877.png)]

当我们调用完函数时,main函数上开辟了一块临时空间,用来接收c返回来的值,然后c内存空间被销毁。

被销毁后,c的内存空间会被赋上随机值,如果这时候系统还没来得及给c一个随机值,那c就还保留着原来的值。

看看这些代码的运行结果是什么

int& Add(int a, int b)  //注意:这里我们的返回值用了引用,因此返回的是c的内存空间
{
	int c = a + b;
	return c;  //返回c本身
}


int main()
{
	int a = 1;
	int b = 2;

	int& ret = Add(a, b);  // c的别名。

	cout << "ret = " << ret << endl;

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hay3I3ux-1646457643045)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220127121504440.png)]

虽然c已经被回收,但其中的值还未被赋上随机值


int& Add(int a, int b)  //注意:这里我们的返回值用了引用,因此返回的是c的内存空间
{
	int c = a + b;
	return c;  //返回c本身
}


int main()
{
	int a = 1;
	int b = 2;

	int& ret = Add(a, b);  // c的别名。
	
	Add(6, 8);

	cout << "ret = " << ret << endl;

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bTpmI2cU-1646457992399)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220127121632213.png)]

再次调用Add函数,c的内存空间中所保留的值被改变,且出函数后c的内存空间还未被赋上随机值。

这里要解释一下,为什么第二次调用函数,还是用原来c的那块内存空间,而不是去用其他的内存空间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sSMk6h7r-1646457643046)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220128090305663.png)]

因为销毁后,没有进行其他需要创建栈帧执行代码的行为,所以第二次调Add函数的时候,调用的还是同一块栈帧,所以改变的还是原来c那块空间的内容。


int& Add(int a, int b)  //注意:这里我们的返回值用了引用,因此返回的是c的内存空间
{
	int c = a + b;
	return c;  //返回c本身
}


int main()
{
	int a = 1;
	int b = 2;

	int& ret = Add(a, b);  // c的别名。

	Add(6, 8);

	cout << "hello world" << endl;   //至此,原来调用Add的那块栈帧被修改

	cout << "ret = " << ret << endl;

	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cTzxQXle-1646457643046)(C:\Users\Z-zp\AppData\Roaming\Typora\typora-user-images\image-20220128092827964.png)]


上述几种情况都涉及到了越界访问,但都可以运行,这是因为越界访问其实是不容易被检查出来的,因为是抽查。

实际上,在上述几种情况中,c所在的栈帧在函数调用完成时就已经被回收了,不应该引用返回,再次印证,符合引用返回的条件是函数调用完成时,返回对象还未还给系统,才可以引用返回

因此,如果用static修饰变量c——c被转移到静态区,Add函数调用完后,c的内存空间还在,我们在调用其它函数,开辟栈帧时,才不会改变c的数据,满足引用返回的使用条件,这时候才可以使用引用返回。

在这里插入图片描述

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

阿波呲der

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

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

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

打赏作者

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

抵扣说明:

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

余额充值