引用的概念
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语言指针也可以实现,但是比较别扭。