🌠 作者:@阿亮joy.
🎆专栏:《吃透西嘎嘎》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间。
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。
我们通过下面的代码来初步了解一下引用。
#include
using namespace std;int main()
{int a = 10;int& ra = a;//<====定义引用类型printf("%p\n", &a);printf("%p\n", &ra);return 0;
}
注意:引用类型必须和引用实体是同种类型的。
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体(引用无法完全替代指针的原因)
#include
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;
}
引用和指针的关系
因为引用是一个变量的别名,所以对引用进行操作就是对变量进行操作。因此引用能够做到指针能做到的事情。比如:交换两个变量的值和修改头指针等等。
有了引用,链表的尾插和头插函数都不再需要传结构体的二级指针了,只需要将头插和尾插的函数的形参设置为一级指针的引用就可以了,这样就可以修改到头指针了。引用做参数的一个作用就是作为输出型参数,函数中修改别名的值,实参的值也就修改了。
引用做参数还有另一个作用就是减少拷贝,提升效率。见下面的代码:
#include
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;
}
学习引用做做返回值之前,我们先来分析一下一下的代码。
#include
using namespace std;int Count()
{int n = 0;n++;// ...return n;
}int main()
{int ret = Count();return 0;
}
了解上一段代码,我们再来看一下这一段代码。
#include
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];
}
引用做返回值的一大作用就是可以修改返回值。
以值作为参数或者返回值类型,在传参和返回间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。使用引用做返回值的另一个作用就是减少拷贝,提高效率。
#include
using namespace std;
#include // 4w byte
struct A { int a[10000]; };A a;
// 值返回
A TestFunc1()
{return a;
}
// 引用返回
A& TestFunc2()
{return a;
}void TestRefAndValue()
{// 以值作为函数参数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(A)-time:" << end1 - begin1 << endl;cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}int main()
{TestRefAndValue();return 0;
}
通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
void Swap(int& x, int& y)
{int tmp = x;x = y;y = tmp;
}void Swap(int x, int y)
{int tmp = x;x = y;y = tmp;
}
注意:以上的两个Swap
函数也构成函数重载,因为引用也是一种数据类型,根据函数名修饰规则就可以区分两个Swap
函数。但是在调用Swap
函数时,会产生调用函数的二义性,所以这种写法是不可取的。
const
关键字也可以用来修饰引用。被const
修饰的引用就表示该引用为常引用,只能读取数据不能写入数据。因为在指针和引用的赋值中,权限可以缩小,但是不能放大。
#include
using namespace std;int main()
{int a = 0;// 权限平移int& ra = a;// 指针和引用的赋值中,权限可以缩小,但是不能放大// 我引用你,权限缩小,可以const int& rra = a;// rra++; // 不行a++; // 可以// 我引用你,我的权限放大,不行const int b = 1;int& rb = b; // 编译失败
}
注意:只有指针和引用的赋值才涉及权限的放大和缩小,值拷贝并不涉及权限的放大和缩小。如下图:
所以,引用作为参数只能权限平移或者权限缩小,无法权限放大。通常来说,引用作为参数时都会用const
修饰引用的。
当某个引用的右值为常数时,此时的引用一定需要const
修饰。因为常数不可被修改。
引用与缺省参数结合
void Func(const int& N = 10)
{//...
}
当两个类型不同的变量给对方赋值时,赋值的过程中会产生临时变量,而临时变量具有常属性,不能被修改。为了说明这个问题,我们来看一个例子:
所以,引用与某个变量类型不匹配时,需要用const
修饰该引用。
当函数的返回值是值返回时,如果用引用来接收该函数的返回值,那么该引用也想要用const
修饰。因为值返回值时也会产生临时变量。
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
// 语法上,ra是a的别名,不开空间
// 底层上,引用是使用指针实现的
int main()
{int a = 10;int& ra = a;ra = 20;int* pa = &a;*pa = 20;return 0;
}
我们来看下引用和指针的汇编代码对比:
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
本篇博客主要讲解了引用,引用对于后面的学习非常的重要,希望大家能够掌握。以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家啦!💖💝❣️
上一篇:阿里Java面试神级文档+脑图