文章目录
- 概述
- 引用的概念
- 引用特性
- 引用的作用
- **引用做参数**
- **引用作为函数返回值**
- 常引用
- 引用的底层实现
- 总结一下引用和指针的不同点
概述
本篇博客将讲述c++相对于c新增的一个重要的内容——引用,深入研究其语法细节以及其需要注意的一些要点。
引用的概念
竟然要学习这个新的语法,那么首先就要知道它的概念是什么,那么引用到底是什么呢?
引用不是新定义一个变量,而是给已经存在的变量取一个别名,编译器不会为引用变量开辟空间,它和它引用的变量公用一块空间。
(引用就好像我们现实世界中给一个物或者人取外号是一样的道理)。
语法使用如下:
类型&引用变量名(对象名)= 引用实体
看下面代码:
#include<iostream>
int main()
{
int a = 10;
int& ra = a;
printf("%p\n", &a);
printf("%p\n", &ra);
return 0;
}
该段代码运行结果如下:
可以看到,这里ra
和a
两个变量的地址是相同的,也就是说他们两个指向的是同一块空间。
这里需要注意的是:引用类型必须和引用实体是同种类型,至于原因会在下面解释常引用时讲解。
引用特性
- 引用在定义时必须初始化
例如:
int& ra;
是不能编译通过的,这也是引用和指针的差别之一 - 一个变量可以有多个别名
int a = 10;
int& ra = a;
int& raa = a;
这段代码是成立的,在现实世界中,这就类似于一个人可以有多个外号。
3. 引用一旦引用一个实体,再不能引用其他实体。
关于这一点,我们可以看看下面一个例子:
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int b = 20;
int& ra = a;
ra = b;
cout << ra << " " << a << endl;
printf("%p\n%p\n%p\n", &a, &ra, &b);
//C++兼容C语言
return 0;
}
大家可以先猜猜这段代码运行的结果是什么?
是的,a和ra的值都被改变了,但是看看下a和ra的地址以及b的地址,我们发现ra依然是a的别名,因此,这里ra仍然是指向a,相当于是把b的值给了a,而并没有发生指向的改变。
这是引用和指针的第二大特点: 引用不能改变指向的对象!。
那么说了这么多,引用到底有什么作用呢?接下来就来看看引用的作用
引用的作用
其实引用的作用更多是用在形参和实参的连接上的。
引用做参数
回忆一下c语言阶段想让两个变量交换值我们会用什么函数。
//C语言的写法
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//函数调用时:
sawp(&a, &b);
那么当我们学习了引用之后,我们就可以使用引用来进行这个操作:
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
//函数调用时:
swap(a, b);
怎么样,是不是感觉引用方便了许多呢?
那么这里的原理是什么呢?
简单来说,就是这里的形参是实参的别名,也就是说他们都是指向同一块内存空间的变量,因此可以直接进行操作。
引用做参数时的另一个特点是:可以减少拷贝提升效率。
其实这一点指针也能做到,就是在传递一个很大的结构体的时候,如果我们直接使用穿值调用,将会消耗很大的开销,而如果是用传址调用或者传递引用调用,就会大大提升效率,并且相比起来,传引用更加方便。
引用作为函数返回值
我们就拿c语言实现顺序表数据结构的代码进行一下对比:
如果还不了解顺序表数据结构是什么的可以看一下下面csdn上一位大佬的博客:
C语言实现顺序表(数据结构)
现在假设我要修改顺序表指定位置元素的值,用c语言的方式该如何实现呢?
void SLmodify(SeqList*sl, int pos, int x)
{
sl->a[pos] = x;
}
int SLfind(SeqList sl, int pos)
{
return sl->a[pos];
}
int main()
{
SeqList ls;
//...
//往顺序表内添加值
//修改位置为5的元素,让其变成2倍
int e = SLfind(&ls, 5);
e+= 5;
SLmodify(&ls, 5, e);
return 0;
}
是不是相当繁琐?
而如果使用的是c++的引用,将会简单很多。
inr& SLat(SeqList* ls, int pos)
{
return ls->a[pos];
}
int main()
{
SeqList ls;
//...
//同样对5位置的值*2
SLat(&ls,5)*=2;
//一步到位!
}
那么,原理是什么呢?
当引用作为返回值时,返回的相当于是你返回变量的别名,也就是说返回的仍然是那个对象,而不是其拷贝。
从而我们就可以总结出引用左返回值的两大特点:
- 能够减少拷贝提高效率
- 能够修改返回值+获取返回值
注意:
由于引用的特殊性,我们不能返回函数内的局部变量,这是由于函数内的局部变量在出函数后就直接销毁,虽然编译器不会报错,但是有可能得到的值不是我们想要的值。
请谨慎使用引用做返回值!!!
最后,我们来讲讲常引用是什么?
常引用
其实就是对引用变量前加上const
修饰符,代表这个引用变量的值不能修改。
int main()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
}
这里编译不同过的原因都是一个:权限放大
引用变量有一个问题就是只能平移权限或者缩小权限,而不能放大权限。
例如:int类型的右值不能赋给一个int&类型的引用变量。
其实很好理解为什么不行,如果能够进行这种操作,那么就有可能通过该引用变量来修改右值,这是不可以的!
那么这里就有一个有意思的点了,看上面的最后一个例子:
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
有人可能会有疑惑,这里d并不是右值啊,为什么第二行代码不能编译通过呢?
这里就又涉及到一个知识点了,c++将一个与变量类型不同的另一个变量赋值给其的过程会先生成临时变量,而这里的临时变量是一个右值,因此编译不通过。
这也是为什么引用类型必须和引用实体是同种类型的原因,就是为了避免权限放大的问题!
引用的底层实现
看下面一段代码:
int main()
{
int a = 10;
int* pa = &a;
int b = 10;
int& rb = b;
return 0;
}
我们利用反汇编的功能查看转换后的汇编后的代码:
可以看到,引用和指针的底层实现是一模一样的,引用其实是对指针使用的一种封装。
总结一下引用和指针的不同点
下面的不同点建议了解,而不是死记硬背!
- 引用概念上定义一个变量的别名,指针存储一个变量地址。 (底层实现相同)
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占 4个字节)
- 引用自加(++)即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全(不会出现空引用的情况)
以上便是关于c++引用的解释,如果博主哪里写的有问题或者大伙有疑惑还请在评论区指出😁