【C++】引用
创始人
2024-03-31 19:24:02
0

​🌠 作者:@阿亮joy.
🎆专栏:《吃透西嘎嘎》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
在这里插入图片描述

目录

    • 👉引用👈
      • 引用概念
      • 引用特性
      • 使用场景
        • 1.做参数
        • 2.做返回值
      • 传值、传引用效率比较
      • 引用与函数重载
      • 常引用
      • 右值为常数的引用
      • 引用和指针的区别
    • 👉总结👈

👉引用👈

引用概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间

比如:李逵,在家称为"铁牛",江湖上人称"黑旋风"。

在这里插入图片描述

我们通过下面的代码来初步了解一下引用。

#include 
using namespace std;int main()
{int a = 10;int& ra = a;//<====定义引用类型printf("%p\n", &a);printf("%p\n", &ra);return 0;
}

在这里插入图片描述
注意:引用类型必须和引用实体是同种类型的

引用特性

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体(引用无法完全替代指针的原因)
#include 
using namespace std;void TestRef()
{int a = 10;// int& ra; // 该条语句编译时会出错int& ra = a;int& rra = ra;// ra++或者rra++,都是a++ra++;rra++;printf("%d %d %d\n", a, ra, rra);printf("%p %p %p\n", &a, &ra, &rra);
}int main()
{TestRef();return 0;
}

在这里插入图片描述

使用场景

引用和指针的关系
在这里插入图片描述

1.做参数

因为引用是一个变量的别名,所以对引用进行操作就是对变量进行操作。因此引用能够做到指针能做到的事情。比如:交换两个变量的值和修改头指针等等。

在这里插入图片描述

在这里插入图片描述
有了引用,链表的尾插和头插函数都不再需要传结构体的二级指针了,只需要将头插和尾插的函数的形参设置为一级指针的引用就可以了,这样就可以修改到头指针了。引用做参数的一个作用就是作为输出型参数,函数中修改别名的值,实参的值也就修改了

引用做参数还有另一个作用就是减少拷贝,提升效率。见下面的代码:

#include 
struct A { int a[10000]; };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;
}

在这里插入图片描述

2.做返回值

学习引用做做返回值之前,我们先来分析一下一下的代码。

#include 
using namespace std;int Count()
{int n = 0;n++;// ...return n;
}int main()
{int ret = Count();return 0;
}

在这里插入图片描述

了解上一段代码,我们再来看一下这一段代码。

#include 
using namespace std;// 引用返回
int& Count()
{static int n = 0;n++;// ...return n;
}int main()
{int ret = Count();return 0;
}

注意,这时候Count函数的返回值是int&,所以Count函数返回的是n的别名。因为n是关键字static修饰的变量,所以n不会随着Count函数的函数栈帧销毁而销毁。也就是说,我们可以通过该返回值n的别名来访问n

在这里插入图片描述

现在我们已经知道了,如果一个函数的返回值为某个变量引用,那么该变量不能是在栈区上申请的,可以是在栈区或者静态区申请。如果返回一个局部变量的引用且再去访问这块空间,那么访问的结果是不可知的。那为什么会这样呢?见下图:

在这里插入图片描述

为了说明返回局部变量的引用是不可取的,我们来看下面几个例子。

在这里插入图片描述
在上面的例子中,我们用retCount函数返回值的别名,相当于访问ret就是访问局部变量n的空间,但这个空间已经被销毁了。从上面的打印结果可以看出,第二和第三次打印的结果都是随机值。这就是用局部变量的引用做返回值带来的后果。

我们再来看一个例子。

在这里插入图片描述
可以看到,三次打印的结果分别是 1、随机值和 100。那为什么会是这样的结果呢?第一次打印的时候,虽然局部变量n的空间被销毁了,但是系统没有使用这块空间,数据也没有清理掉,所以第一次打印的结果是 1。而第二次的结果是一个随机值,就更好理解了,就是系统已经用了这块空间并将其存储的数据置成了随机值(cout也是一次函数调用,需要建立函数栈帧,建立栈帧时刚好用到了这块空间)。而第三次打印呢,就是建立Func函数的函数栈帧时,x的地址刚好是之前n的地址,那么该地址存储的数据就变成了 100。并且Func函数的函数栈帧销毁后,存储x的空间还没有被系统使用,该空间存储的数据还是 100。所以打印ret时,ret访问的空间刚好就是存储x的空间,所以就打印出了 100。是不是真的就这样呢?我们将它们的地址都打印出来看一下,如下图所示:

在这里插入图片描述

结论

  • 出了函数作用域,返回变量不存在了,不能用引用返回,因为引用返回的结果是未定义的。
  • 出了函数作用域,返回变量存在,才能使用引用返回。

在这里插入图片描述

知道了传引用返回需要注意的问题后,我们再来看一个程序。我们给之前写的顺序表增加两个函数接口SeqListSizeSeqListAt就可以替换掉打印顺序表和修改pos位置的值的函数接口了。

size_t SeqListSize(SL* psl)
{assert(psl);return psl->size;
}SLDataType& SeqListAt(SL* psl, size_t pos)
{assert(psl);assert(pos < psl->size);return psl->a[pos];
}

在这里插入图片描述

引用做返回值的一大作用就是可以修改返回值

传值、传引用效率比较

以值作为参数或者返回值类型,在传参和返回间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。使用引用做返回值的另一个作用就是减少拷贝,提高效率

#include 
using namespace std;
#include // 4w byte
struct A { int a[10000]; };A a;
// 值返回
A TestFunc1() 
{return a;
}
// 引用返回
A& TestFunc2() 
{return a;
}void TestRefAndValue()
{// 以值作为函数参数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(A)-time:" << end1 - begin1 << endl;cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}int main()
{TestRefAndValue();return 0;
}

在这里插入图片描述

通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大

引用与函数重载

void Swap(int& x, int& y)
{int tmp = x;x = y;y = tmp;
}void Swap(int x, int y)
{int tmp = x;x = y;y = tmp;
}

注意:以上的两个Swap函数也构成函数重载,因为引用也是一种数据类型,根据函数名修饰规则就可以区分两个Swap函数。但是在调用Swap函数时,会产生调用函数的二义性,所以这种写法是不可取的。

在这里插入图片描述

常引用

const关键字也可以用来修饰引用。被const修饰的引用就表示该引用为常引用,只能读取数据不能写入数据。因为在指针和引用的赋值中,权限可以缩小,但是不能放大。

#include 
using namespace std;int main()
{int a = 0;// 权限平移int& ra = a;// 指针和引用的赋值中,权限可以缩小,但是不能放大// 我引用你,权限缩小,可以const int& rra = a;// rra++; // 不行a++; // 可以// 我引用你,我的权限放大,不行const int b = 1;int& rb = b; // 编译失败
}

在这里插入图片描述

注意:只有指针和引用的赋值才涉及权限的放大和缩小,值拷贝并不涉及权限的放大和缩小。如下图:

在这里插入图片描述
所以,引用作为参数只能权限平移或者权限缩小,无法权限放大。通常来说,引用作为参数时都会用const修饰引用的。

右值为常数的引用

当某个引用的右值为常数时,此时的引用一定需要const修饰。因为常数不可被修改。

引用与缺省参数结合

void Func(const int& N = 10)
{//...
}

当两个类型不同的变量给对方赋值时,赋值的过程中会产生临时变量,而临时变量具有常属性,不能被修改。为了说明这个问题,我们来看一个例子:

在这里插入图片描述

所以,引用与某个变量类型不匹配时,需要用const修饰该引用。

在这里插入图片描述

当函数的返回值是值返回时,如果用引用来接收该函数的返回值,那么该引用也想要用const修饰。因为值返回值时也会产生临时变量。

在这里插入图片描述

引用和指针的区别

语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

// 语法上,ra是a的别名,不开空间
// 底层上,引用是使用指针实现的
int main()
{int a = 10;int& ra = a;ra = 20;int* pa = &a;*pa = 20;return 0;
}

我们来看下引用和指针的汇编代码对比:

在这里插入图片描述

引用和指针的不同点:

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址
  2. 引用在定义时必须初始化,指针没有要求
  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体
  4. 没有NULL引用,但有NULL指针
  5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节)
  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  7. 有多级指针,但是没有多级引用
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  9. 引用比指针使用起来相对更安全

👉总结👈

本篇博客主要讲解了引用,引用对于后面的学习非常的重要,希望大家能够掌握。以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家啦!💖💝❣️

相关内容

热门资讯

不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
安卓文字转语音tts没有声音 安卓文字转语音TTS没有声音的问题在应用中比较常见,通常是由于一些设置或者代码逻辑问题导致的。本文将...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
APK正在安装,但应用程序列表... 这个问题可能是由于以下原因导致的:应用程序安装的APK文件可能存在问题。设备上已经存在同名的应用程序...
报告实验.pdfbase.tt... 这个错误通常是由于找不到字体文件或者文件路径不正确导致的。以下是一些解决方法:确认字体文件是否存在:...