目录
概念
- 引⽤不是新定义⼀个变量,⽽是给已存在变量了⼀个别名,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间。
比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。【水浒108将各个有称号】
引用的操作符和c语言中的按位与或者取地址操作符相同
格式:
类型& 引用变量名(对象名) = 引用实体;
他的基本用法如:
c
int a = 10;
int& b = a;
- 这里就突出我们前面说的,引用变量并没有为被引用对象重新开辟空间,而是同用一块空间
引用的五大特性
引用在定义时必须初始化
- 首先来看第一个,若是定义了一个引用类型的变量int&,那么就必须要去对其进行一个初始化,指定一个其引用的对象,否则就会报错
cpp
int a = 10;
int& b = a;
int& c;
一个变量可以有多个引用
- 对于第二个特定,通俗一点来说就是b引用了a,那么b等价于a;此时c也可以引用a,那么c也等价于a,此时a == b == c
cpp
int a = 10;
int& b = a;
int& c = a;
其实就相当是一个人有很多小名,比如我们在家里父母叫我们儿子,外面老师叫我们同学,你兄弟叫你兄弟,但是这些名字对应的都是一个人。
一个引用可以继续有引用
- 对于第三个特性而言,其实就是一个传递性。当一个变量引用了另一个变量之后,其他变量还可以再对其进行一个引用。通过运行就可以看出它们也都是属于同一块空间
c
int a = 10;
int& b = a;
int& c = b;
引用了一个实体就不能再引用另一个实体
- 我们对一个变量进行引用后就不能进行另外一个变量也进行引用了,所以我们引用必须初始化,初始化引用的变量就不能改变
cpp
int a = 10;
int c = 20;
int& b = a;
int& b = c;
可以对任何类型做引用(包括指针)
cpp
int a = 10;
int* p = &a;
int*& q = p;
- 指针p存储了a的地址,q原本是一个指针加了一个&就是表示引用,对p这个指针就行引用,所以p就是q,q就是p,p指向a,q也指向a
int*&的这个写法要认识一下
引用使用的两种使用场景
做参数
交换两数
- 我们在C语言中学的交换两个数字通常需要在调用这个函数的时候传地址才能通过形参改变实参,那么在设计参数的时候就要把参数类型设计为指针才行
- 但是在C++中我们有了引用就可以直接把参数类型设计为引用,然后调用的时候传变量就行了(这也算使用引用的目的)
- 有人就问了,指针不一样吗?干嘛要弄些这些东西出来,是因为,指针变量也是需要开辟空间来存储地址的。但是引用不用,他是同一块地址。
指针:
cpp
void swap1(int* px, int* py)
{
int t = *px;
*px = *py;
*py = t;
}
引用:
cpp
void swap2(int& x, int& y)
{
int t = x;
x = y;
y = t;
}
cpp
swap2(a,b)//只需要进行传值调用
单链表头结点的修改
在讲解引用的特性时,我说到了引用的类型不仅仅限于普通变量,还可以是指针 。但上面说的是普通指针,接下去我们来说说结构体指针,也涉及到了引用类型在做参数时的场景
cpp
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;
}
- 在以前学习单链表的时候,要想改变实参,我们必须要使用二级指针来接受一级指针来改变
- 但现在学习了引用之后,我们就不需要去关心传入什么指针的地址了,只需要将这个链表传入即可,在函数形参部分对其做一个引用,那么内部的修改也就一同带动了外部的修改
cpp
void PushFront(SLNode*& SList, int x);
- 此时PushFront()内部我们也可以去做一个修改,直接使用形参SList即可,无需考虑到要对二级指针进行解引用变为一级指针
cpp
void PushFront(SLNode*& SList, int x)
{
SLNode* newNode = BuyNode(x);
newNode->next = SList;
SList = newNode;
}
最后再补充一下:很多教材书并不是上面用引用的写法而是:
cpp
typedef struct SingleNode {
struct SingleNode * next;
int val;
}SLNode, *PNode;
这里的*PNode就是:
struct SingleNode*做了一个typedef,也就是对这个结构体指针的类型做了一个重命名叫做【PNode】,那后面如果要使用这个结构体指针的话直接使用的【PNode】即可
cpp
typedef struct SingleNode* PNode
于是对于头插的形参部分又可以写成下面这种形式,与SLNode*& SList是等价的
cpp
void PushFront(PNode& SList, int x);
做返回值
我们在设计函数的时候,有很多函数要进行返回一个值。那么他是如何返回一个值的呢?
进入正题前我们要补充一点:
只有是建立函数就要开辟函数栈帧,他一般是存放在栈区。当调用完一个函数后就要对这块函数栈帧进行销毁。销毁了还怎么返回呢?所以他返回是先把返回值存储到一个临时变量中假设这个临时变量是ret
如:
cpp
int Count()
{
int n = 0;
n++;
// ...
return n;
}
还需要注意一点的是,编译器他不管你是存储在栈,或者是存储在静态区里,他都是用临时变量进行返回
cpp
int Count()
{
static int n = 0;
n++;
// ...
return n;
}
注意:
- 当我们定义变量 / 创建函数 / 申请堆内存的空间时,系统会把这块空间的使用权给到你💪,那么这块空间你在使用的时候是被保护的🛡,被人无法轻易来访问、入侵你的这块空间。但是当你将这个空间销毁之后,它并不是不存在了、被粉碎了,只是你把对于这块空间的使用权还给操作系统了,不过这块空间还是存在的,因此你可以通过某种手段访问到这块空间🗡,由于操作系统又收回了这块空间的使用权,继而它便可以对其进行再度分配给其他的进程,那它就可能又属于别人了
- 所以你通过某种手段去访问这个空间的时候其实属于一种非法访问⚠,可是呢这种非法访问又不一定会报错,就像之前我们说到过的数组越界、访问野指针都不一定会存在报错。为什么?因为编译器对于程序的检查是一种【抽查行为】,不一定能百分百查到,所以你在通过某些手段又再次访问到这块空间后所做的一些事都是存在一种【随机性】的
注意:
若是返回空间小一点的变量时使用的就是【寄存器】
若是返回空间大一点的内容时使用的就是【临时变量】,这个临时变量会提前在main函数栈帧中提前开好
- 总结:当需要将函数中的临时变量返回时,无论这个变量是在栈区、堆区或者静态区开辟空间,都会通过一个临时变量去充当返回值【小一点的话可能是寄存器eax,大一点可能是在上一层栈帧开好的】然后再返回给外界的值做接受
优化传递返回值
有人说,这样编译器太傻了,静态区的变量是整个程序结束才销毁,编译器还要使用临时变量来存储。真是浪费空间。
- 那有什么办法可以免去这种拷贝的过程,直接将得出的结果返回回去呢?那就是引用返回
cpp
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
- 这样写编译器就不会把返回值存储到一个临时变量中进行返回了,而是直接返回n的别名。又因为引用是和被引用的对象同用一块地址,所以说他不会进行内存的拷贝。(这也是用引用的目的之一)
对于传引用返回除了可以减少拷贝之外,我们还可以通过去接收这个返回值去修改返回的对象
- 这里举一个例子,对于顺序表而言我们在数据结构中有学习过,现在是要去修改固定位置上的值,这里如果使用C语言来实现的话就会比较繁琐,首先我们要先去获取到这个位置上的值,然后再对这个值进行修改,分别为SLGet()和SLModify()函数
cpp
typedef struct SqList {
int a[100];
size_t sz;
}SeqList;
// 获取当前位置上的数据
int SLGet(SeqList* ps, int pos)
{
assert(pos >= 0 && pos < 200);
return ps->a[pos];
}
// 修改当前位置上的值
void SLModify(SqList* ps, int pos, int x)
{
assert(pos >= 0 && pos < 200);
ps->a[pos] = x;
}
- 那么此时当我们需要将0这个位置上的值 + 5的话就需要像下面这样去进行调用,你是否觉得这样非常繁琐呢?
cpp
SqList s;
// 对第0个位置的值 + 5
int ret = SLGet(&s, 0);
SLModify(&s, 0, ret + 5);
此时当我们学习了引用之后就可以将代码修改成下面这样,将当前pos的值直接使用引用返回,那么外界在进行接收时相当于为其取了一个别名,此时再去操作的话就相当于是在操作这一块上的值
cpp
int& PostAt(SqList& s, int pos)
{
assert(pos >= 0 && pos < 200);
return s.a[pos];
}
- 在进行调用的时候就可以写成这样下面这样,形参部分也是因为有了引用所以不需要传递地址。而且我这一个函数代替了上面的两个函数,具备【查找】和【修改】的功能
cpp
PostAt(s, 0) = 1;
cout << PostAt(s, 0) << endl;
PostAt(s, 0) += 5;
- 总结:返回值引用的好处就是减少了空间的拷贝,而且调用者可以轻易获取并修改返回值
但是要注意:
cpp
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) is :" << ret << endl;
return 0;
}
- 虽然我们这个函数的返回类型是一个引用,就相当于是把c的别名返回给main函数栈帧中,但是C这个变量是在Add函数栈帧中创建的,在调用完Add函数后,这块函数栈帧已经销毁,我们没有再对这块空间使用的权限。
这个时候我们再去打印:
就会打印一个随机值。
- 现在你要知道的是因为Add函数做了一个【引用返回】,即返回了c的别名,但此时呢ret又使用引用接收了c的引用,所以可以说【ret又引用了c的引用】,那此时也就可以说ret与c就融为一体了,那么ret也就是c这块空间的别名
这时候ret就还和被销毁Add函数栈帧中的C一样的空间。
最后打印一个随机值
- 为什么呢?这一点我在上面也有提到过。因为当Add函数栈帧销毁的时候,其空间还是在的,只是使用权不是你的了,可是呢它被操作系统回收了,操作系统就还可以把它分配给其他进程,那此时就可以说这块空间被重复利用了,下一次的函数调用可能还是在这块空间上建立栈帧,但是上一次的栈帧是否清理取决于编译器,可能清理了,也可能没清理
常引用
权限放大
cpp
int a = 1;
int& b = a;
const int c = 2;
int& d = c;
b对a的引用没有什么问题,但是d对c的引用就有问题了。const关键字我们在C语言中提到过,被const修饰的变量是不能修改的。原变量C被const修饰不能修改了。但是d对C这个变量进行引用,他们就是同一块内存,他前面可没有const修饰,所以具有修改权限。
- 这就违规了,我原来的变量都没有权利,你一个复制品凭什么有权利(也可以想象原变量是管理员,别名是用户,用户的权限不能超过管理员)
这时候进行权限持平
- 那我们修改一下,不要让权限放大了,给变量d也加上一个常属性,让他俩一样,看看是否可行
cpp
const int c = 2;
const int& d = c;
权限缩小
- 那既然权限保持不会出现问题,若是我现在将权限缩小会不会出现问题呢?
cpp
int c = 2;
const int& d = c;
- 可以看到,也是不会出现问题。你呢允许我修改,但是我加上了常属性不去修改,那也是说得通的
注意
cpp
const int m = 1;
int n = m; //普通变量不受约束
有人看到,哈哈这一定是权限放大。绝对会报错。
- 答案是错的,我们回顾引用特性,引用是对一个变量取别名,所以他没有创建一块新的内存空间。而是和原变量同一块内存空间
可是这里m和n是两块内存空间,就不会存在权限冲突
也就是说:对于权限放大只适用于引用和指针类型(他们都是 指向同一块内存空间)
临时变量具有常属性
需要注意的是类似 int& rb = a3; double d = 12.34; int& rd = d; 这样⼀些场
景下a 3的和结果保存在⼀个临时对象中, int& rd = d 也是类似,在类型转换中会产⽣临时对
象存储中间值,也就是时,rb和rd引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥
就触发了权限放⼤,必须要⽤常引⽤才可以。
cpp
// 编译报错: "初始化": ⽆法从"int"转换为"int &"
// int& rb = a * 3;
const int& rb = a*3;
double d = 12.34;
// 编译报错:"初始化": ⽆法从"double"转换为"int &"
// int& rd = d;
const int& rd = d;
引用与指针的区别
- 在学习了这么多有关引用的知识之后,相信读者也知道了。在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间
cpp
int a = 10;
int& ra = a;
cout<<"&a = "<<&a<<endl;
cout<<"&ra = "<<&ra<<endl;
- 而对于指针来说,指针变量本身需要开辟一块内存空间来存储别人的地址
cpp
int a = 10;
int* pa = &a;
不过从【汇编层面】来看,其实二者是一样的,引用也是用指针去实现的,也会开空间
cpp
int main()
{
int a = 10;
// 语法层面:不开空间,是对a取别名
int& ra = a;
ra = 20;
// 语法层面:开空间,存储a的地址
int* pa = &a;
*pa = 20;
return 0;
}
- 通过将这段代码转到【反汇编】,就可以发现引用和指针在底层的实现竟然是一样的
- 指针的可以指向另一个变量的空间,而引用只能指向初始化的空间
引出引用的目的
总结一下:
- 引用本质上就是对指针的功能进行了复制。让程序员在进行操作的时候更加方便安全(如作为参数类型来交换两个数)
- 还有一个帮助就是如果只使用一个变量就不必拷贝内存到形参