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