一、前言
Hello,大家好,今天我们就来聊聊有关C++中的引用知识:book:
- 回忆一下我们曾经在写C语言的时候因为指针所引发的种种难题,特别是对于【两数交换】的时候因为函数内部的概念不会引发外部的变化,使得我们需要传入两个需要交换数的地址,在函数内部进行解引用才可才可以交换二者的值
- 另一块就是在数据结构中的【单链表】,面对二级指针的恐惧😱是否还伴随在你的身边,因为考虑到要修改单链表的头结点,所以光是传入指针然后用指针来接受还不够,面对普通变量要使用一指针来进行修改,那对于一级指针就需要用到二级指针来进行修改,此时我们就要传入一级指针的地址,才可以在函数内部真正得修改这个单链表的结构
- [x] 对前面所学知识做了一个回顾,另一目的也是为了引入C++的一大特性 —— 【引用】,若是你学习了引用之后,就不需要担心是否要传入变量的地址还是指针的地址,让我们一起进入学习吧!
二、概念介绍
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。【水浒108将各个有称号】
==那要怎么去“引用”呢?==
- 此时需要使用到我们在C语言中学习到的一个操作符叫做
[&]
,它是【按位与】,也是【取地址】,但是在C++中呢,它叫做【引用】
==它的语法是怎样的呢?==
类型& 引用变量名(对象名) = 引用实体;
int a = 10; int& b = a;
- 通过运行我们可以看到变量a和变量b的地址是一样的,这是为什么呢?就是因为b是a的引用,那b就相当于a,所以它们共用一块地址
- 那既然他们公用一块地址的话,内容也是一样的。此时若是我去修改b的值,a是否和跟着改变呢?
- 可以看到,若是去修改b的话,a也会跟着一起变化
三、引用的五大特性
- [x] 引用在定义时必须初始化
- [x] 一个变量可以有多个引用
- [x] 一个引用可以继续有引用
- [x] 引用一旦引用一个实体,再不能引用其他实体
- [x] 可以对任何类型做引用【变量、指针...】
1、引用在定义时必须初始化
- 首先来看第一个,若是定义了一个引用类型的变量
int&
,那么就必须要去对其进行一个初始化,指定一个其引用的对象,否则就会报错
int a = 10; int& b = a; int& c;
2、一个变量可以有多个引用
- 对于第二个特定,通俗一点来说就是b引用了a,那么b等价于a;此时c也可以引用a,那么c也等价于a,此时
a == b == c
- 你可以无限对a进行引用,直到把操作系统的内存申请光为止(应该没那么狠吧)
int a = 10; int& b = a; int& c = a;
3、一个引用可以继续有引用
- 对于第三个特性而言,其实就是一个传递性。当一个变量引用了另一个变量之后,其他变量还可以再对其进行一个引用。通过运行就可以看出它们也都是属于同一块空间
int a = 10; int& b = a; int& c = b;
4、引用一旦引用一个实体,再不能引用其他实体
- 这个特性很重要【⭐】,要牢记。因为上面有说到对于引用而言==在定义时必须初始化==,那么在定义结束完后它就已经引用了一个值,无法在对其去进行修改了,这是非法的!
int a = 10; int c = 20; int& b = a; int& b = c;
- 这里我要做一个辨析,因为对于引用来说它和指针非常得类似,也有着千丝万缕般的关系,后面我也会对【指针】和【引用】做一个对比分析
- 看下图就可以知道,对于指针而言一旦指向了一块地址后是可以继续修改其指向的【这点也是指针和引用最大的不同】
这个特性和常量指针与指针常量很类似,可以一起记忆
5、可以对任何类型做引用【变量、指针....】
- 最后一点特性作为拓展。上面我们介绍了对于变量而言可以有引用,当然除了整型之外其他类型也是可以的
- 看到下面c1是
double
类型,c2引用c1,所以c2也是double
类型的。其他类型可以自己试试看
double c1 = 3.14; double& c2 = c1;
然后我们重点来说说有关指针这一块的引用【⭐】
int a = 10; int* p = &a; int*& q = p;
- 通过代码可以看出,指针p指向了a所在的这块地址,接着我用q引用了p,那么指针q就相当于是指针p,q也指向了a所在的这块地址。来分解一下
int*
代表q是一个指针类型,&
则表示指针q将会去引用另一个指针
- 通过下图就可以看出对于指针【p】和指针【q】来说,它们都是同一块空间,因为q引用了p,p指向了a所在的地址,那么q也指向a所在的地址。这么看应该是非常清楚了,只是
int*&
的这个写法要认识一下,我在下面还会讲到
以上就是有关C++中的引用所要介绍的特性,还望读者牢记😁
四、引用的两种使用场景
知道了引用的基本特性后,接下去我们来聊聊有关它的使用场景
1、做参数
a.案例一:交换两数
- 还记得我们在C语言中学习过的【交换两数】吗?需要传入两个变量的地址,从而可以在函数内部通过指针的解引用来访问到所指向变量的那块地址从而对里面的内部进行一个修改
- 相信这也是我们在初次学习指针时接触的一个东西,也是最经典的一块内容,那除了使用【指针】的这种形式,你还有没有其他的方法呢?没错,就是使用我们刚学的==引用==
void swap1(int* px, int* py) { int t = *px; *px = *py; *py = t; }
swap1(&a, &b);
- 我们来看看下面这种引用的方式,相信在学习了引用的基本语法和特性之后你一定很快看懂下面的代码。因为x引用了a,y引用了b,所以它们是等价的,在函数内部使用临时变量对二者进行交换就可以带动外界的变化
void swap2(int& x, int& y) { int t = x; x = y; y = t; }
通过运行结果来看确实也可以起到交换两数的功能
- 接下去再普及一点:book:,如果你了解C++中的函数重载就可以知道,若是我将这个两个函数的函数名改成一样,但是形参部分的数据类型不同,是可以构成重载的
void swap(int* px, int* py) { int t = *px; *px = *py; *py = t; } void swap(int& x, int& y) { int t = x; x = y; y = t; }
通过调试来看一下吧💻
b.案例二:单链表的头结点修改【SLNode*& p】
在讲解引用的特性时,我说到了引用的类型不仅仅限于普通变量,还可以是指针。但上面说的是普通指针,接下去我们来说说==结构体指针==,也涉及到了引用类型在做参数时的场景
- 看到如下一段代码,我定义了一个链表结点的结构体,还记得我们在链表章节学习过的头插,因为涉及到会修改链表的头结点,因此函数内部的修改不会导致外部一起修改,继而我们需要传入这个链表的地址,然后使用二级指针来进行接收,相信这一块一定令很多小伙伴非常头疼🤦
typedef struct SingleNode { struct SingleNode * next; int val; }SLNode; void PushFront(SLNode** SList, int x) { SLNode* newNode = BuyNode(x); newNode->next = *SList; *SList = newNode; } int main(void) { SLNode* slist; PushFront(&slist, 1); return 0; }
- 但现在学习了引用之后,我们就不需要去关心传入什么指针的地址了,只需要将这个链表传入即可,在函数形参部分对其做一个引用,那么内部的修改也就一同带动了外部的修改
- 看了上面讲到的【普通指针】的引用,相信你对下面这种写法一定不陌生,内部的形参
SList
也就相当于是外部函数外部传入的实参slist
。==这就是很多学校《数据结构》的教科书中统一的写法==,说是使用了纯C实现,但却利用了C++中的【引用】,如果没有学习过C++的小伙伴一定是非常难受😖
void PushFront(SLNode*& SList, int x);
- 此时
PushFront()
内部我们也可以去做一个修改,直接使用形参SList
即可,无需考虑到要对二级指针进行解引用变为一级指针
void PushFront(SLNode*& SList, int x) { SLNode* newNode = BuyNode(x); newNode->next = SList; SList = newNode; }
最后再来补充一点,很多教科书不仅仅是像上面这种写法,而且还会更加精简,它们将结构体定义成这种形式👇
typedef struct SingleNode { struct SingleNode * next; int val; }SLNode, *PNode;
- 来解释一下为什么这么定义,看过结构体章节的小伙伴一定都知道这是
typedef
的作用,对于SLNode而言其实就是对这个结构体的类型由进行了一个typedef,也就是对其进行一个重命名,这样我们在使用这个结构体定义变量的时候就不需要再去写struct SingleNode slist
了,直接写成SLNode slist
即可
typedef struct SingleNode SLNode
- 那又有同学说:“前面这个我知道,但是后面的
*PNode
是什么意思呀❓”。==这也是我重点要说明的部分==,其实这就相当于是对struct SingleNode*
做了一个typedef,也就是对这个结构体指针的类型做了一个重命名叫做【PNode】,那后面如果要使用这个结构体指针的话直接使用的【PNode】即可
typedef struct SingleNode* PNode
于是对于头插的形参部分又可以写成下面这种形式,与SLNode*& SList
是等价的
void PushFront(PNode& SList, int x);
c.案例三:二叉树递归遍历
最后,我们来讲讲有关引用做参的第三个场景,也就是在递归调用的时候
- 还记得二叉树高频面试题中的最后一题 —— 二叉树的遍历,需要通过题目给出的字符数组去重构一下这棵树。我们是通过递归来实现的,又因为要在每次的递归层中访问到后面的字符,所以需要传入一个变量做数组的遍历,可是在我们去通过画递归展开图做模拟的时候发现随着递归的层层深入,可函数内部的参数变化不会使上一层也跟着变化,所以就发生了覆盖的现象,出现了随机值的情况。所以后来通过指针接受遍历变量的地址,在每一层的递归调用过程中,通过
指针的解引用
来带动外部的变化
BTNode* ReBuildTree(char* str, int* pi)
char str[20] = "abc##de#g##f###"; int i = 0; BTNode* root = ReBuildTree(str, &i); InOrder(root);
- 上面是对这一题的描述,如果想仔细了解得就去看看那篇文章,这里我们主要是通过所学的【引用】来对这个题目做一个优化
- 其实不用我说你应该都会了,就是将这个指针类型的pi改成引用类型即可,内部也不需要什么解引用了,直接使用这个pi即可(为了对照也叫
pi
好了),因为pi
就是i
的引用,两者是等价的,属于同一块空间,==因此无论函数递归调用多少层,内部参数的变化会也会带动外部的变化==
BTNode* ReBuildTree(char* str, int& pi) { if (str[pi] == '#') { pi++; return NULL; } BTNode* root = (BTNode*)malloc(sizeof(BTNode)); root->val = str[pi++]; root->left = ReBuildTree(str, pi); root->right = ReBuildTree(str, pi); return root; }
BTNode* root = ReBuildTree(str, i);