C++练级之初级:第四篇
引用
- C++练级之初级:第四篇
- 1.引用
- 1.1引用的介绍
- 1.2引用的使用场景
- 1.3常引用
- 2.引用的底层
- 3.引用的与指针的比较
- 总结
1.引用
1.1引用的介绍
🤔首先还是一个问题,引用是解决C语言什么不足?
指针在,这里传参和访问变量有点麻烦, 能不能简单一点? 于是C++的大佬就发明了一种简单的语法:引用;
🤔🤔什么是引用?
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
就像李逵的外号叫“黑旋风”,“铁牛”,“李鬼”等等,他们都是一个人,对别名操作就是对其本身操作;
🤔🤔🤔那么我们在用引用的时候有什么问题要注意的吗?
总结:
- 引用是对某个实体起别名,操作的是同一块空间;
- 引用类型必须和引用实体是同种类型的;
- 引用在定义时必须初始化;
- 一个变量可以有多个引用;
- 引用一旦引用一个实体,再不能引用其他实体,即对这个实体一直引用到最后;
注意: 这里不要和java搞混:java中的引用是可以改变指向的,C++中的引用是改变不了的,即一直是一个实体的别名;
1.2引用的使用场景
🤔既然了解了什么是引用,那么引用一般都用在什么地方呢?
- 引用做参数(输出型参数)
注解: 🤔🤔什么是输出型参数?
输出型参数:形参的改变影响实参;
输入型参数:形参的改变不影响实参;
在C语言中我们要实现这种输出型参数就一定要用指针,但是指针书写太麻烦,于是在C++中我们可以用引用类型作为形参来实现输出型参数;
1.1 引用做参数,可以提高效率(大对象/深拷贝类对象)
#include<iostream>
#include <time.h>
using namespace std;
struct A
{
int a[10000]; //40000byte
};
void TestFunc1(struct A a)
{
}
void TestFunc2(struct A& a)
{
}
void TestRefAndValue()
{
struct A arr;//40000byte
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(arr);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(arr);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
🤔🤔为什么在大对象或者深拷贝类对象,引用或者指针比传值调用的效率更高?
我们知道形参是对实参的一份临时拷贝,那么对于大对象而言,比如40000byte的数组,那么形参就会开销拷贝所用的时间,效率肯定要比传引用或者指针来的慢,因为传引用只需传别名,指针也只要传一个指针变量指向这块空间;
- 引用做返回值(提高效率),返回后可以对返回值进行修改
#include<iostream>
#include <time.h>
using namespace std;
struct A
{
int a[10000];
};
struct A a;
struct A TestFunc1()
{
return a;
}
struct A& TestFunc2()
{
return a;
}
void TestRefAndValue()
{
// 以值作为的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2();
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
🤔🤔为什么在传大对象或者深拷贝类对象时,值返回的效率比引用返回的效率低?
注意:编译器是根据返回值类型来判断是否为值返回,若是值返回则都需要创建临时变量,临时变量可以是寄存器(返回的对象较小)
因为值返回需要创建临时变量,而引用返回不需要创建临时变量,直接返回别名,所以在返回大对象时,引用返回的效率比值返回的效率高;
🤔🤔🤔那么我们在引用返回时有什么注意点吗?
👇这两段代码有什么区别?
😁进入正题:
🤔🤔🤔这是为什么?
这个与这段代码类似:
解析:这里的ret接收到了n的地址,但是n这块空间被销毁了,只是vs并没有清理栈帧中的数据,所以通过n的地址可以访问到n,但这是侥幸访问的,因为右边的图可以看出,我们调用其他函数时,其他函数创建栈帧时就会覆盖那个栈帧的数据(覆盖),所以通过n的地址访问就是随机值,这种问题一般称为野指针的访问;
上面的问题类似(ret2访问的是一个销毁的变量)
- 如果test2函数结束,函数栈帧销毁(使用权还给操作系统),没彻底清理栈帧,那么ret2的结果侥幸是正确的;
- 如果test2函数结束,函数栈帧销毁(使用权还给操作系统),彻底清理栈帧,那么ret2的结果随机的;
总而言之,函数返回值为引用类型时,在vs中,函数栈帧销毁后(还给操作系统),函数栈帧不会清理(变成0xcccccccc),所以用值来接收是侥幸正确的,因为是简单的赋值操作,但在其他编译器中可能是错的(函数栈帧销毁后直接清理),那么用值来接收就会是随机值,在vs中如果用引用来接收更不推荐,因为下面一旦调用了其他函数,其他函数的栈帧就会覆盖上去,那么引用就会是随机值;
总结引用的引用场景:
1.基本上所有情况下都有可以用引用类型传参;
2.谨慎用引用类型做函数的返回值,除了对象出了函数作用域还在的情况下可以用;
下面就举几个正确的使用引用做函数返回值的正确应用:
1.3常引用
🤔常引用是什么?
对引用的对象不能改变的引用,一般写法:在引用类型前加上const,注意:常引用是不能改变的;
错误的引用:
正确的引用:
🤔🤔这段代码是什么意思呢?
从上面的引用的注意点我们知道引用的类型必须相同,所以报错;
所以当两个类型不同时,但是你一定要放进去就会发生类型转换,发生类型转化的时候会创建一个临时变量,这个临时变量存放的是一个转换过后的值(常性),所以要用常引用;
常性和常量是不同的
常性:const修饰的变量,本质是变量,具有长属性(不可改变);
常量:本质是常量,不可改变;
🤔🤔为什么运算符两边的数据类型不一致时要产生临时变量?
因为我们不能改变 i 本身的值,如果不建立临时变量直接进行类型转换的话,i的值就会变;
🤔🤔🤔那如果我就直接在正确的引用前加上const会发生什么?
那我们对a++,b++,c会不会变呢?
总而言之,就是如果在正确的引用前加上const,会导致加上const这个引用的权限缩小(不能改变a),但是本质上还是同一块空间,只不过加上const的那个引用改不了;
2.引用的底层
🤔引用的底层是什么?(现在先简单了解一下)
汇编层面:
底层上引用还是要开空间的;
对a进行取别名本质上就是把a的地址给ra;
注意:但是在写代码时我们要遵循语法,还是要认为引用没开空间;
3.引用的与指针的比较
🤔那么引用和指针有什么不同呢?
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求;
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体;
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节);
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小;
- 有多级指针,但是没有多级引用;
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理;
- 引用比指针使用起来相对更安全(指针有空指针问题引用却没有这个问题);
总结
今天主要学习了什么是引用,引用的场景,引用的底层,引用和值得比较,引用和指针的比较,引用做返回值时的注意点,常引用等,如果喜欢本篇不妨留下一个❤️❤️❤️,下篇见!