本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一、引用的基本介绍
什么是引用?通俗的讲,引用就是"取别名",我们看下面的例子: 可以看到,引用并不会开辟新的空间。
二、引用的基本特性
特性①:必须有初始化
特性②:一个变量可以有多个引用
可以给别名取别名,但本质上都是对a的引用
特性③:一旦引用了一个实体,就不能引用其他实体
可以看到,之后b不会再引用实体c,只能对b做赋值修改
三、引用的基本原则
①引用类型和引用实体类型相同
引用的一个极其重要的基本原则是:对变量实体的读写权限只能缩小不能放大。我们在接下来的例子中来说明这个原则。 引用可以引用常量吗?答案是可以的。但是我们需要加上const来保证读写权限没有被放大:
✪代码分析 我们来分析这段代码我们需要注意到,引用就是给实体取的别名,二者在地址上是一样的,对别名的修改也会改变实体的值,所以我们在取别名的时候就要保证对引用的读写权限没有扩大。就像上面的例子,10是一个只读变量,如果不加上const,那么就意味着可以通过a来修改常变量10,这显示是不行的。
②引用类型和引用实体类型不同
引用类型必须和引用实体是同种类型吗?不一定!怎么理解这个问题呢?我们可以类比我们在C语言中学到的隐式类型转化,我们来分析中间的过程:
int main()
{
double d = 6.66;
int a = d;
}
为什么可以将类型为double的变量赋值给类型为int的变量呢?实际上这个赋值过程不是一步到位的,而是存在一个 "中间变量"。这个中间变量接收d的整数部分,最后再把这个中间变量拷贝给a。需要注意的是,这个中间变量具有常属性。引用也是同样的道理:
✪代码分析 a并不是直接引用d,而是引用这个中间变量。由于中间变量具有常属性,所以前面必须加上const修饰避免扩大读写权限。
通过取地址我们可以确认a没有引用d,从而也间接说明了中间临时变量的存在。
四、引用的基础应用
①对标传址操作
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 10;
int b = 20;
swap(a, b);
return 0;
}
正是由于引用和引用实体存在互相改变的关系,所以我们可以用引用来替代传值操作。拿我们C语言中常写的swap函数举例,这样写起来是不是更爽了呢? 但如果设计的函数中需要传入常数,那么就必须在参数前面加上const,否则存在扩大实体读写权限的风险。经过实际测试,引用的效率和传址的效率是差不多的。 相应的,一些输出型参数也可以用引用来实现,例如:
int* sortArray(int* nums, int numsSize, int* returnSize){
}
returnSize是我们的输出型参数,在C语言中我们只能通过传址的方式实现,现在用引用也可以实现这样的效果。
②作为返回值
int& count()
{
static int n = 0;
n++;
return n;
}
用引用作为返回值和传统的方式相比有什么好处呢?我们先来谈谈传统的方式是如何传递返回值的:
- 首先将预备返回的值存储到临时变量,再将临时变量拷贝给用于接收函数返回值的变量
- 预备返回的变量小则用寄存器存储,变量大则则创建函数栈帧的时候预先为返回值创建存储空间
如果我们使用引用,那我们根本就不需要额外的临时空间和额外的拷贝,因为我们是通过引用直接对实体进行操作的,这可以提供程序的效率。
但是引用作为返回值只适合返回值出作用域不销毁的情况,如上面static修饰的n变量,如果我们我们引用的变量出函数作用域销毁,那我们实际上通过引用创建了间接的“野指针”。
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2): " << ret << endl;
cout << "Add(1, 2): " << ret << endl;
return 0;
}
ret的值可能是随机值也可能不是,取决于这块空间有没有被重新覆盖和使用。cout函数栈帧的创建覆盖了Add函数的函数栈帧,所以ret的值变成随机值了
再来看几个变式:
int& Add(int a, int b)
{
static int c;
c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2): " << ret << endl;
cout << "Add(1, 2): " << ret << endl;
return 0;
}
变量c用static修饰后,这块空间出函数作用域后不会被销毁,不用担心被其他函数栈帧所覆盖
int& Add(int a, int b)
{
static int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2): " << ret << endl;
cout << "Add(1, 2): " << ret << endl;
return 0;
}
注意static语句只在初始化时执行一次