C++的另一大利器——引用

在这里插入图片描述

当我们学数据结构的时候一定会写单链表,但是很多同学在学习时很难理解为何要用二级指针,什么时候需要用二级指针,因此C++为了简化这种痛苦,提出了引用的概念,并用这个概念扩充出了很多十分方便的使用技巧。

概念

引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟新的内存空间,它和它引用的变量公用同一块内存空间,符号上是使用的 &
在这里插入图片描述

特点

1、别名与原变量地址相同

int main()
{
	int k = 0;
	int& rk = k;
	cout << &k << endl;// &k是取地址 0x0012ZZ60
	cout << &rk << endl;// &rk是取地址 0x0012ZZ60 
}

注意

我们区分取地址和引用是用&跟着的内容,如果 & 是在定义变量时,在类型名后面,那就是引用,如果是&紧跟着变量,那么就是取地址

return &k;//取地址

void Swap(int& a, int& b);//引用

int& ri = &i;//ri前面的是&引用,i前面的&是取地址,这里的含义是为i的地址取别名

2、别名和原变量的值相同

int main()
{
	int i = 0;
	int j = i;//赋值
	int& ri = i;//取别名
	cout << &i << &j << &ri << endl;// j地址不同于i和ri,i和ri地址相同
	
	printf("i = %d j = %d ri = &d\n", i, ++j, ri);// 0 1 0
	printf("i = %d j = %d ri = &d\n", ++i, j, ri);// 1 1 1
	printf("i = %d j = %d ri = &d\n", i, j, ++ri);// 2 1 2
	//对i加ri变化,对ri加i变化
	return 0;
}

3、一块空间可以有多个别名

int i = 0;// 原变量
int& ri = i;// 取别名1
int& k = i;// 取别名2
int& rri = ri;// 对别名取别名

他们的所有性质都相同,但使用场景不多(目前我所接触到的,如果以后学习、项目中发现使用挺多的话,我会修改博客的)

4、引用必须初始化

引用在第一次定义时,必须初始化,也就是说必须说明是引用的谁,这个特性也就保证了引用不会出现“空引用”这样类似于指针上的问题。

调用的情况形参的引用也就对应于实参,也就是类似于给到了一个初始化

int a = 0;
int& ra = a;//correct
int& rb;//err

5、引用对象不能改变

在我们定义引用变量后,引用的对象就确定下来,无法再重新引用其他的变量,这个特性与JAVA不同,这个特性使得,在C++中指针还有用武之地,比如改变节点的指向等情况。而在JAVA中规定,初始化后可以再引用其他变量,使得JAVA完全抛弃了指针。

//博主这里还没有想到什么例子能够用错,因为 & 被认为是引用的场景
//就只有是在类型名后面,也就是定义的时候会用到,是否有其他场景来造出这个错误,我还没想到

具体用法

一般引用都是用在函数传值以及作返回值上,想想一下以前C语言很多需要传递指针的地方都可以用引用来替代,并且在使用别名的时候我们也不需要进行取地址等固定操作,简单来说int a; int& ra = aa是ra,ra也是a,下面我们来简单看几个例子

简单实例

void Swap(int& a, int& b)
{
	int tmp = a;
	a = b;
	b = tmp;
}
void PushFront(Node* &phead, int x)//单链表
{
	Node* newnode = (Node*)malloc(sizeof(Node));
	if (phead == nullptr)
	{
		phead = newnode;
		return;
	}
	newnode->data = x;
	newnode->next = phead;//C语言用**pphead 那么这里就需要*pphead
	phead = newnode;//同上注释
}

作参数

第一种常用情况:作函数的参数,这种情况其实就是上面的简单例子,这些例子里都有个共同特点,是输出型参数,也就是说这些参数在函数外面还需要使用,需要通过这个函数来改变这些参数。
除此之外,和指针相同,都可以提高效率,主要是针对大对象(占用空间很大的struct class),深拷贝的类对象(博主暂时还没学到,后面学到了会进行补充)。
我对后面这种情况补充一个例子:

#include<time.h>
struct BIG
{
	int score[10000];
	char name[20000];
};

void Test(BIG x)
{ ; }
void Test_R(BIG& x)
{ ; }
void Test_P(BIG* x)
{ ; }
int main()
{
	BIG data;
	int begin1 = clock();
	for (int i = 0; i < 500000; i++)
	{
		Test(data);
	}
	int end1 = clock();

	int begin2 = clock();
	for (int i = 0; i < 500000; i++)
	{
		Test_R(data);
	}
	int end2 = clock();

	int begin3 = clock();
	for (int i = 0; i < 500000; i++)
	{
		Test_P(data);
	}
	int end3 = clock();

	printf("Test: %ds %dms\n", (end1 - begin1) / 1000, (end1 - begin1) % 1000);
	printf("Test_R: %ds %dms\n", (end2 - begin2) / 1000, (end2 - begin2) % 1000);
	printf("Test_P: %ds %dms\n", (end3 - begin3) / 1000, (end3 - begin3) % 1000);

	return 0;
}

在这里插入图片描述

因此引用也可以极大的提高效率,其效率和指针几乎相同
在C++中偏向于用引用,代码看着会更简洁,让程序员更加的关注逻辑,而不是在语法上思考良久。

作返回值

前提知识——编译器创建的临时变量

C++在很多情况下编译器会自动产生临时变量,这些临时变量的值都具有常性(诱值),常性可以理解为临时变量是用const修饰过的,而这个特性会在常引用与权限部分用到
并且产生临时变量的阶段是在编译阶段,通过查看汇编可以看到这些临时变量
C++产生临时变量
情况列举:
1、函数的返回值是非引用类型(不管变量是全局的、局部的、静态的)时,会产生一个临时变量,把这个临时变量传值给函数外面进行接收
int Add(int a, int b){return a + b;} int ret = Add(1, 2);
在这里插入图片描述

2、显式类型转换:强制类型转换,用临时变量存储强制转换后的值,然后把临时变量用来赋值int a = (int)dd;或者使用printf(“%d”, (int)dd);
3、隐式类型转换:算数转换(int + double -> double + double)、整形提升(char + int -> int + int)、类型提升( float a = 1.2f; double b = a; )都会创建临时变量
4、截断,临时变量会存储截断后的值,然后把截断后的值进行赋值或者使用
5、初始化操作:创建一个默认值为0的临时对象,int value = int();

用法细则

在函数返回值时,如果我们不想生成临时变量,就用引用做返回值(一般需要是全局、静态、malloc、new、常量 或者 输入型参数,这些情况下,离开函数后,变量地址还是我们的,生命周期还没有结束,还没有还给操作系统,还能够正常使用)

因此引用的作用一:减少拷贝、提高效率

#include<time.h>

typedef struct BIG
{
	int score[10000];
	char name[20000];
}BIG;

struct BIG Test(BIG a)
{
	return a;
}
struct BIG& Test_R(BIG& a)
{
	return a;
}
struct BIG* Test_P(BIG* a)
{
	return a;
}
int main()
{
	BIG data;
	int begin1 = clock();
	for (int i = 0; i < 500000; i++)
	{
		Test(data);
	}
	int end1 = clock();

	int begin2 = clock();
	for (int i = 0; i < 500000; i++)
	{
		Test_R(data);
	}
	int end2 = clock();

	int begin3 = clock();
	for (int i = 0; i < 500000; i++)
	{
		Test_P(&data);
	}
	int end3 = clock();

	printf("Test: %ds %dms\n", (end1 - begin1) / 1000, (end1 - begin1) % 1000);
	printf("Test_R: %ds %dms\n", (end2 - begin2) / 1000, (end2 - begin2) % 1000);
	printf("Test_P: %ds %dms\n", (end3 - begin3) / 1000, (end3 - begin3) % 1000);

	return 0;
}

在这里插入图片描述

但如果返回值是局部变量时,在离开函数后,局部变量被销毁,此时函数外部接收到的引用值不确定,所以我们需要避免这种情况,以下为一些错误用例及分析:

int& Count()
{
	int n = 0;
	n++;
	return n;
}
int main()
{
	int ret = Count();//ret和n是拷贝关系
	//这里打印ret的值不确定:
	//如果Count函数结束后,栈帧销毁,编译器未清理栈帧,那么ret就是n
	//但是编译器清理了栈帧,那么ret是随机值
	cout << ret << endl;

	return 0;
}
int& Count(int x)
{
	int n = x;
	n++;
	return n;
}
int main()
{
	int& ret = Count(10);//ret和n是引用关系
	cout << ret << endl;//11/随机值,理由同上
	cout << ret << endl;//21/随机值

	return 0;
}

也就是说主要看编译器平台的处理

实例

//任意插
int& Insert(SeqList& ps, int pos)
{
	assert(pos < MAX && pos >= 0);
	return ps->a[pos];//返回位置
}
int main()
{
	SeqList s;
	Insert(&s, 0) = 1;//给第一个位置赋值1
	cout << Insert(&s, 0) << endl;
	Insert(s, 1) += 1;//给第二个位置加1
	return 0;
}

常引用和权限

C++中的常引用是指被声明为const的引用类型。在函数参数传递和返回值时,使用常引用可以避免不必要的内存复制和修改操作,同时也能保证函数不会对原始数据进行修改。常引用通常用于访问非本地变量或临时对象,并将其传递给函数。与普通引用相比,常引用被声明成const,不能用于修改所引用的对象的值

在引用中,会涉及到:权限放大、权限平移、权限缩小三种情况,只有后两者被允许,而放大是被禁止的,其原因可以理解为,别名后的变量天然的权限低于原始变量。

int main()
{
	int a = 1;
	int& b = a;//权限平移,a可以影响b,b可以影响a
	return 0;
}
int main()
{
	const int a = 0;
	int& b = a;//err, 权限放大,不被允许
	return 0;
}
int main()
{
	int x = 0;
	int& y = x;//权限平移,x影响y,y影响x
	const int& z = x;//权限缩小,x可以影响z,z不能影响x (x是管理员 z是普通用户)
	x++;
	z++;//err
	return 0;
}
int main()
{
	const int& x = 10;//权限平移,10是常量,无法改变,x是const修饰的,也无法改变
	int& y = 20;//err,权限放大
	return 0;
}
int main()
{
	double d = 1.1;
	int i = d;//隐式类型转换
	int& ri = d;//err,这里发生了隐式类型转换,编译器会创建一个具有常性的临时变量,让这个临时变量传值给ri,因此这里就是权限放大
	const int& rri = d;//权限平移
	return 0;
}
int test()//注意这里返回值不是引用
{
	static int x = 0;
	return x;
}
int main()
{
	int& ret1 = test();//err,权限放大,test返回的是非引用类型,所以会创建临时变量,临时变量具有常性,常性变量放到可以变化的ret1中,err
	const int& ret2 = test();//权限平移,常性到常性
	int ret3 = test();//普通的拷贝,没有权限问题
	return 0;
}
int& test()
{
	static int n = 0;
	return n;
}
int main()
{
	int& ret1 = test();//权限平移
	const int& ret2 = test();//权限缩小
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

失去梦想的小草

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

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

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

打赏作者

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

抵扣说明:

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

余额充值