👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨
前言
本章是补充C语言语法的不足,以及C++是如何对C语言设计不合理的地方进行优化的。
目录
- 前言
- 一、问题引入
- 二、引用的概念
- 二、引用的特性
- 三、引用的使用场景
- 四、 常引用问题
- 五、引用和指针的区别(面试常考点)
- 六、总结
一、问题引入
现在一起来回顾C语言中指针的玩法:
在C
语言的指针中,我们需要开辟一块新的空间来存在变量的地址;若要通过指针间接改变变量a
,则需要对p
进行解引用(*p
)进行对象的间接访问。然而,C++
创始人Bjarne Stroustrup
大佬就觉得指针(间接访问)用起来贼不舒服。因此,创建了一个新的语法 — 引用。
二、引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
- 引用的表示方式
#include <iostream>
using namespace std;
int main()
{
int a = 0;
//对a取别名为b
//语法表示:引用类型& 别名 = 引用对象
int& b = a;
return 0;
}
注意:引用类型必须和引用对象是同种类型的
接下来我们对b
赋值是否会改变变量a
- 验证引用不会额外开辟内存空间
二、引用的特性
1. 引用在定义时必须初始化
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再也不能引用其他实体
三、引用的使用场景
- 实现一个交换函数
在C语言指针章节中,我们通常都是传变量地址给形参,这样才能对两个变量进行了交换。那么接下来看看引用版本:
引用做参数的意义:
- 做输出型参数 — 形参的改变要影响实参
- 提高效率
以下是引用做参数和未用引用做参数的性能比较
#include <iostream>
#include <time.h>
using namespace std;
struct A { int a[100000]; };
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;
}
- 数据结构 — 单链表
【C语言版本】
在单链表的插入中,当一开始链表为空时,需要改变头指针的指向,则要传地址。而如果用引用则是下面的代码
解释红色框框:由于引用类型必须和引用对象是同种类型,所以引用类型和引用对象的类型是
ListNode*
,然后再加上引用符(&),说明pplist
是phead
的别名。
- 引用做返回值
在讲引用之前先来回顾传值返回
#include <iostream>
using namespace std;
int func()
{
int a = 0;
a++;
return a;
}
int main()
{
int ans = func();
cout << ans << endl;
return 0;
}
变量
a
其实并不是直接返回给ans
,中间会生成一个临时变量(可能是寄存器),变量a
会拷贝给临时变量,然后返回给ans
。那为啥会生成临时变量呢?原因是:变量a
出了作用域,其栈帧就销毁了。而这个临时变量就是为了存储a
的值,然后再返回给ans
那么如果不想要生成这个临时变量,有没有办法呢?我的回答是当然有,使用引用作为返回值,就不会产生临时变量。这也是传引用返回好处:减少拷贝,提高效率。
以上就是引用作为返回值的代码。但代码还是存在一些问题:类似野指针的访问。为什么呢?原因是:引用返回的是变量a的别名,当值传给
ans
时,变量a
所在的空间就已经被销毁了(注意:销毁不是把空间整没了,而是把其使用权限还给了操作系统),那么func
函数要作为这个表达式的返回值去赋值给ans
,相当于又去访问了这个空间,所以会导致数据没有保障,也就是说其实ans
的值其实是不确定的。总之,如果func
函数结束,栈帧销毁,但没有清理栈帧(取决于编辑器,很明显VS是没有清理栈帧的),那么ans
的结果是侥幸正确的;如果func
函数结束,栈帧销毁,清理栈帧,那么ans
就是随机值。
那么如何解决以上问题呢?用static修饰变量a
就行了
为什么加个
static
就不会出现问题呢?因为static
修饰的变量是在静态区的,即使func
函数结束,栈帧销毁,也不会对其变量造成影响
总结:
1. 基本任何场景都可以用引用传参
2. 谨慎用引用做返回值,出了函数作用域,对象不在了,就不能用引用返回,还在就可以用引用返回
最后,我们可以测试值返回和引用返回的效率
#include<iostream>
#include <time.h>
using namespace std;
struct A
{
int a[10000];
};
A a;
// 值返回
A TestFunc1()
{
return a;
}
// 引用返回
A& TestFunc2()
{
return a;
}
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
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 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
可见使用传引用返回确实可以减少拷贝,提高效率
四、 常引用问题
const
修饰引用对象
以上代码是错误的。原因是
const
修饰变量是常变量,不能被修改。自身都无法修改,再对其取别名,也就自然不能被修改了。这里也说明了一点:引用过程中,权限不可以放大
const
修饰别名
以上的代码是正确的。
const
修饰的是x的别名y,也就是缩小的y的权限,不能对y进行修改,但对x修改还是可以的。这里也说明了一点:引用过程中,权限可以平移或者缩小
const
修饰不同类型
首先来看看一下代码:
以上代码大部分人都认为是错的,其原因是:引用类型必须和引用对象是同种类型。其实只说对了一半,那如果我对别名变量前加上
const
是否还会报错呢?
编译器居然没报错。这是为什么呢?原因是:它们之间发生了类型转换。发生类型转换时,中间会产生临时变量,而这个临时变量具有常属性,并且这个临时变量对应转变的目标类型。
再举2个例子
- 第386行代码正确的原因是:前面说过了,这种返回不是之间返回x的值,而是返回一个临时变量,这个过程相当于拷贝。
- 第388行代码错误的原因是:由于返回的是一个临时变量,临时变量是具有常属性的,这导致类型不匹配,因此加上const即可解决
- 410行代码解释:前面说过了,这种返回不是之间返回x的值,而是返回x别名,对别名再取别名是ok的
- 412行代码解释:const修饰别名,缩小了res2的权限
五、引用和指针的区别(面试常考点)
1. 在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
2. 在底层实现上实际是有空间的,因为引用是按照指针方式来实现的
我们来看下引用和指针的汇编代码对比
3. 引用概念上定义一个变量的别名,指针存储一个变量地址。
4. 引用在定义时必须初始化,指针没有要求
5. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
6. 没有NULL引用,但有NULL指针
7. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节/64位是8个字节)8. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小------->点击跳转查看
9. 有多级指针(一级指针、二级指针…),但是没有多级引用
10. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
11. 引用比指针使用起来相对更安全。因为指针更容易出现空指针、野指针等等
六、总结
引用的出现是为了解决一些问题,主要包括:
- 避免拷贝构造函数创建临时对象所带来的消耗。当将一个对象作为函数参数传递或返回时,如果使用传值方式,则会调用拷贝构造函数创建一个临时变量,这会带来额外的开销。
- 让我们在函数内部修改传入的对象。如果不使用引用,则只能通过指针来实现这一点。