c++引用

文章详细解释了C++中的引用概念,包括它是变量的别名,不占用额外内存,必须在定义时初始化,并且一旦引用后不能改变引用对象。讨论了常引用和类型匹配的规则,以及引用作为输出型参数和返回值的优势。引用返回可以提高效率,但需注意作用域问题。最后,通过示例展示了引用如何简化代码,特别是对于结构体成员的修改。
摘要由CSDN通过智能技术生成

 引用的概念

引用 不是新定义一个变量,而 是给已存在变量取了一个别名 ,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。例如:“李逵”江湖人称“黑旋风”,宋江又叫他“铁牛”。
用代码表示就是
int main()
{
	int a = 10;
	int& b = a;
	int& c = b;
	
	return 0;
}

&在C语言中表示取地址,在c++中可以表示为引用。

如上述代码所示,b是a的别名,c是b的别名,对b改变,那么a也会改变。就相当于,“黑旋风”是李逵的别名,“铁牛”也是李逵的别名,李逵喝水了,那么“黑旋风”也喝了水。

我们来看看a,b,c的地址是不是一样的。

看结果,表示a,b,c都是用同一个地址。

使用引用需要注意的地方

1、一个变量可以有多个引用

就像上面所显示的一样,这里不再赘述。

2、引用必须在定义的时候初始化

 看这里,c在引用的时候没有初始化,出现了错误,显示:变量c许哟啊初始化设定项。你不能说你是别名,你要说的应该是你是谁的别名。举例子:不能说“黑旋风”是外号,可以说 “黑旋风”是李逵的外号 。

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

我们可以把引用理解为终身制,下面进行代码演示。

 但是如果我们这样写:

我们来观察结果,发现是c=x这句代码的意思是:将x的值赋给c,但是c依旧是a,b的别名。而由于c是a,b的别名,所以c改变,a和b也改变了。

 常引用

我们来看下面的代码。

看到这句话报错了,那是因为什么呢?

是因为,a是const int类型,而c是int类型,a是不可修改的,但是c是int类型,是可以修改的,所以出现了权限放大,是错误的。

如果引用的类型不同也会报错,就像下面这样。 

但是如果是下面这两种情况则不会报错。

a和c都是const int类型,属于权限平移,所以不会出错。

a属于int类型,而c属于const int类型,a可以修改,但是c不能修改,现在c是a的别名,这属于权限的缩小。

c是不可以修改的,这里会报错,因为c是const类型。

但是a可以修改,因为a是int类型,int可以修改,而c是a的别名,a改变了,c自然也会改变。

如果对于上面所说有点疑惑,可以参考下面的例子。

举个例子,李逵在梁山上是李逵,可以喝酒吃肉,但是下梁山之后, 因为没有人保护他了,所以不允许他喝酒闹事。意思是,李逵作为李逵的时候可以喝酒吃肉,但是作为“黑旋风”的时候不能喝酒,只能吃肉。但是李逵一旦回到梁山,是不是又可以喝酒了。在本例中,李逵代表的就是a,“黑旋风”代表的就是c。a可以改变--李逵可以喝酒,c不能改变--“黑旋风”不能喝酒。

引用的作用

我们讲了引用,但是或许大家不知道引用有什么作用,接下来我来分析一下为什么c++要使用引用,而不继续使用指针了。

1、作为输出型参数

首先明确一下什么是输出型参数,与之对应的,什么又是输入型参数。

简单来说,输入型参数就是传入函数的参数,给函数用的参数,就是不会被改变的参数。举个例子,一下代码就是输入型参数。

上面的代码中print调用的x就是输入型参数,x是给print用的,调用print并不会改变x的值。

而什么又是输出型参数呢?看下面的代码。

 看上面的代码,a,b传入swap函数,同时改变了a,b的值,所以是输出型参数。而swap函数的实现正是用了引用,以前在c语言中,我们都是用的指针,因为要改变int ,就要使用int*,要改变int*,就要使用int**,现在,使用他的别名即可。

有一个经典的引用使用,很多数据结构的书中都会这样写,如果是学c语言的小伙伴,可能看不懂为什么这样写,现在我们来分析一下。

 如果没有学过c++的小伙伴看到这样的代码估计就懵了,怎么函数的参数还能取地址呢?现在看来,这属于夹带私货。

我们来分析为什么会这样写。如果从c语言的角度理解,首先是对链表要进行插入,ps是struct ListNode*类型,要改变struct ListNode*,函数的参数就要写成struct ListNode**,类比之前的“要改变int ,就要使用int*,要改变int*,就要使用int**”再返回到上面的代码,PNode代表的是struct ListNode*,

而在c++中,要改变int,就对int取别名,比如说,改变a,就写int&b=a,b是a的别名,现在要改变PNode是不是对PNode取别名就行了,所以函数的参数写成PNode&就行了。

2、引用做返回值

首先我们来看传值返回。

看这个传值返回,这是没有任何问题的。 但是请思考,在count函数中返回的x是直接赋值给ret的吗?显然不是,为什么,因为count作为一个函数,一出作用域栈帧就销毁了,怎么可能还保留着x呢。那既然不是直接赋值给ret,又是怎样实现传值返回的呢?接下来我们来画图分析一下。

 

 可以简单理解成这样,在count中的值会有一个寄存器保存,(但是也不一定是寄存器,因为寄存器能保存的空间非常小,在这里我们就粗糙地认为是寄存器保存了)在ret接收的时候,寄存器再把保存的值给ret,这样就完成了一次传值调用。

观察上述代码,又能不能完成传值调用呢?答案是可以,因为x在静态区,就算函数栈帧销毁了,静态区的变量没有销毁,所以还是可以完成传值调用的。

上述所讲都是没有没有引用的,那么传引用调用可以这样用吗?

如果是传引用返回,返回的就是x的别名,不会有寄存器来存储,所以:调用函数之后打印,如果

栈帧没有被清理,那么打印的值侥幸是正确的,如果被清理了,那么函数的值是随机值。 

大家来看下面这种情况呢?

首先我们来辨析一下,上面两张图中,int ret=count(10)和int& ret = count(10)的区别是什么。

在第一个代码中,count返回的是x的引用,而用的是ret这个值接收,意味着,ret改变和x没有任何关系,ret和x也没有用同一块地址,我们打印出来看看。

 在上述代码中,可以明显看到x和count的地址不同,而第二次调用count函数的时候,传入的参数是20,再次打印ret,ret的值还是11,表示之前int ret = count(10);这句代码只是代表了赋值的意思,在之后可以对ret进行赋值,ret和x没有任何关系。

 而在另一个代码中,count 返回的是x的引用,而ret也用引用来接收,意思是x和ret用的同一块地址,他们是同样的,如果ret改变,那么count中x的值也会改变,我们打印出来看看。

 从上面的图可以看到,ret和x确实是同一个地址,而且在下面的代码count(20)中,调用了count函数,并且传入的参数为20,我们再次打印ret,发现ret的值变成了21,表示ret就是x的引用,x改变就会引起ret的改变。

 回到之前所说的,在引用返回时,无论是int ret = count(10);还是int& ret = count(10);都是很危险的,因为我们仰仗的只有函数栈帧不销毁,这样值才不会改变,但是如果函数栈帧销毁了呢?或者被覆盖了,像下面这样。

最后一次打印count的时候,我们发现变成了随机值,为什么呢?因为我们在此之前调用了函数printf,而printf覆盖了之前的栈帧,x的使用权被剥夺了,ret还是和x一样,所以ret就变成了随机值。

总结:1、基本任何场景都可以引用传参。2、谨慎用引用做返回值,出了作用域,对象不在了,就不能用引用返回,还在就可以用引用返回

引用返回的效率高

#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
	// 以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc1();
	size_t end1 = clock();
	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2();
	size_t end2 = clock();
	// 计算两个函数运算完成之后的时间
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
}

int main()
{
	TestReturnByRefOrValue();
	return 0;
}

执行上述代码,运行结果如下,证明引用返回比传值返回效率高

 说了这么多,那引用返回除了能提高效率,还有什么用?我们来看下面的代码。

typedef struct SqList
{
	int val;
	int a[100];
}SqList;

void SLModify(SqList* ps,int pos,int x)//修改pos位置的值
{
	ps->a[pos] = x;
}

int SLGet(SqList* ps,int pos)
{
	return ps->a[pos];
}

int main()
{
	SqList ps;
	SLModify(&ps, 5, 10);
	cout << SLGet(&ps, 5) << endl;
	return 0;
}

 在c语言中,如果需要修改顺序表中pos位置的值,需要写两个函数,而在c++中,如果用上引用返回,就会非常方便。

typedef struct SqList
{
	int val;
	int a[100]={0};
}SqList;

int& SLAT(SqList& ps,int pos)
{
	return ps.a[pos];
}

int main()
{
	SqList ps;

	//3号位置的值+3
	SLAT(ps, 3) += 3;
	cout << SLAT(ps, 3) << endl;


	return 0;
}

只需要一个函数,就可以直接修改顺序表中的数据。那么能这样做的原理是什么呢?我们来分析一下。

这是一个结构体,所以如果函数销毁了,但是栈帧还是在的,所以可以使用引用返回。

使用引用返回的好处是,最后得到的ps.a[pos]的引用,在外面改变ps.a[pos],里面的值也会跟着改变,这样就不用像之前的c语言一样想要修改pos位置的值得先修改pos位置的值,再找到pos位置表示出来。

最重要的成果,在外部即可修改内部的值。当然如果c语言指针也可以实现,但是比较别扭。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值