👉引用👈
引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间。
我们通过下面的代码来初步了解一下引用。
#include <iostream> using namespace std; int main() { int a = 10; int& ra = a;//<====定义引用类型 printf("%p\n", &a); printf("%p\n", &ra); return 0; }
注意:引用类型必须和引用实体是同种类型的。
引用特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体(引用无法完全替代指针的原因)
#include <iostream> using namespace std; void TestRef() { int a = 10; // int& ra; // 该条语句编译时会出错 int& ra = a; int& rra = ra; // ra++或者rra++,都是a++ ra++; rra++; printf("%d %d %d\n", a, ra, rra); printf("%p %p %p\n", &a, &ra, &rra); } int main() { TestRef(); return 0; }
使用场景
引用和指针的关系
1.做参数
因为引用是一个变量的别名,所以对引用进行操作就是对变量进行操作。因此引用能够做到指针能做到的事情。比如:交换两个变量的值和修改头指针等等。
有了引用,链表的尾插和头插函数都不再需要传结构体的二级指针了,只需要将头插和尾插的函数的形参设置为一级指针的引用就可以了,这样就可以修改到头指针了。引用做参数的一个作用就是作为输出型参数,函数中修改别名的值,实参的值也就修改了。
引用做参数还有另一个作用就是减少拷贝,提升效率。见下面的代码:
#include <time.h> struct A { int a[10000]; }; void TestFunc1(A a) {} void TestFunc2(A& a) {} void TestRefAndValue() { A a; // 以值作为函数参数 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc1(a); size_t end1 = clock(); // 以引用作为函数参数 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc2(a); size_t end2 = clock(); // 分别计算两个函数运行结束后的时间 cout << "TestFunc1(A)-time:" << end1 - begin1 << endl; cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl; } int main() { TestRefAndValue(); return 0; }
2.做返回值
学习引用做做返回值之前,我们先来分析一下一下的代码。
#include <iostream> using namespace std; int Count() { int n = 0; n++; // ... return n; } int main() { int ret = Count(); return 0; }
了解上一段代码,我们再来看一下这一段代码。
#include <iostream> using namespace std; // 引用返回 int& Count() { static int n = 0; n++; // ... return n; } int main() { int ret = Count(); return 0; }
注意,这时候Count函数的返回值是int&,所以Count函数返回的是n的别名。因为n是关键字static修饰的变量,所以n不会随着Count函数的函数栈帧销毁而销毁。也就是说,我们可以通过该返回值n的别名来访问n。
现在我们已经知道了,如果一个函数的返回值为某个变量引用,那么该变量不能是在栈区上申请的,可以是在栈区或者静态区申请。如果返回一个局部变量的引用且再去访问这块空间,那么访问的结果是不可知的。那为什么会这样呢?见下图:
为了说明返回局部变量的引用是不可取的,我们来看下面几个例子。
在上面的例子中,我们用ret做Count函数返回值的别名,相当于访问ret就是访问局部变量n的空间,但这个空间已经被销毁了。从上面的打印结果可以看出,第二和第三次打印的结果都是随机值。这就是用局部变量的引用做返回值带来的后果。
我们再来看一个例子。
可以看到,三次打印的结果分别是 1、随机值和 100。那为什么会是这样的结果呢?第一次打印的时候,虽然局部变量n的空间被销毁了,但是系统没有使用这块空间,数据也没有清理掉,所以第一次打印的结果是 1。而第二次的结果是一个随机值,就更好理解了,就是系统已经用了这块空间并将其存储的数据置成了随机值(cout也是一次函数调用,需要建立函数栈帧,建立栈帧时刚好用到了这块空间)。而第三次打印呢,就是建立Func函数的函数栈帧时,x的地址刚好是之前n的地址,那么该地址存储的数据就变成了 100。并且Func函数的函数栈帧销毁后,存储x的空间还没有被系统使用,该空间存储的数据还是 100。所以打印ret时,ret访问的空间刚好就是存储x的空间,所以就打印出了 100。是不是真的就这样呢?我们将它们的地址都打印出来看一下,如下图所示:
结论
- 出了函数作用域,返回变量不存在了,不能用引用返回,因为引用返回的结果是未定义的。
- 出了函数作用域,返回变量存在,才能使用引用返回。
知道了传引用返回需要注意的问题后,我们再来看一个程序。我们给之前写的顺序表增加两个函数接口SeqListSize和SeqListAt就可以替换掉打印顺序表和修改pos位置的值的函数接口了。
size_t SeqListSize(SL* psl) { assert(psl); return psl->size; } SLDataType& SeqListAt(SL* psl, size_t pos) { assert(psl); assert(pos < psl->size); return psl->a[pos]; }