当前位置: 首页 > news >正文

从外到内理解c++引用

文章目录

  • 前言
  • 一、引用使用
      • 用于定义别名
      • 引用作为函数参数
      • 引用作为函数返回值
  • 二、引用的本质
  • 三、引用和指针的区别,用引用的好处
  • 引用不能绑定到临时数据
  • const引用与转换类型


前言

今日在看侯捷的c++视频时,说到在写c++代码时,传参和返回值时能用引用就用引用,还说了句引用底层就是指针。但是没有往深入的说,可能是还没看到说的哪一集。但是内心充满了疑惑,平常写代码的时候也经常用引用,但是对引用的具体实现,以及效率如何,没有具体的学习过,为了解决心中的疑惑,决定彻底的搞下引用这个知识点。


本文,将在应用层到底层,从外到内的顺序,来进行深究引用

一、引用使用

用于定义别名

type &name = data;

注意:引用必须在定义的同时初始化,并且以后也要从一而终,不能再引用其它数据,这有点类似于常量(const 变量)。
引用和原始变量,指向同一地址

引用作为函数参数

引用作为函数返回值

这里需要注意下:不能返回局部数据(例如局部变量、局部对象、局部数组等)的引用,因为当函数调用完成后局部数据就会被销毁,有可能在下次使用时数据就不存在了,C++ 编译器检测到该行为时也会给出警告。

以上内容并不是我们今天的重点,简单回顾下即可。
下面这些内容,才让我们真正拨开引用那层神秘的面纱

二、引用的本质

为了了解引用变量的底层实现机制,看如下代码:

int i=5;
int &ri=i;
ri=8;

在Visual Studio 2017环境的debug模式调试代码,反汇编查看源码对应的汇编代码的步骤是:调试->窗口->反汇编,即可得到如下原码对应的汇编代码:

int i=5;
00A013DE  mov        dword ptr [i],5    	//将文字常量5送入变量i
int &ri=i;
00A013E5  lea        eax,[i]  	 	    	//将变量i的地址送入寄存器eax
00A013E8  mov        dword ptr [ri],eax  	//将寄存器的内容(也就是变量i的地址)送入变量ri
ri=8;
00A013EB  mov        eax,dword ptr [ri]  	//将变量ri的值送入寄存器eax
00A013EE  mov        dword ptr [eax],8   	//将数值8送入以eax的内容为地址的单元中
return 0;
00A013F4  xor        eax,eax

考查以上代码,在汇编代码中,ri的数据类型为dword,也就是说,ri要在内存中占据4个字节的位置。 所以,ri的确是一个变量,它存放的是被引用对象的地址。由于通常情况下,地址是由指针变量存放的,那么,指针变量和引用变量有什么区别呢?使用指针常量实现上面的代码功能。考查如下代码:

int i=5;
int* const pi=&i;
*pi=8;

按照相同的方式,在VS2017中得到如下汇编代码:

int i=5;
011F13DE  mov         dword ptr [i],5  
int * const pi=&i;
011F13E5  lea         eax,[i]  
011F13E8  mov         dword ptr [pi],eax  
*pi=8;
011F13EB  mov         eax,dword ptr [pi]  
011F13EE  mov         dword ptr [eax],8  

观察以上代码可以看出:
(1)只要将pi换成ri,所得汇编代码与第一段所对应的汇编代码完全一样。所以,引用变量在功能上等于一个指针常量,即一旦指向某一个单元就不能在指向别处。
(2)在底层,引用变量由指针按照指针常量的方式实现。

由上我们知道了,引用就是由指针按照指针常量的方式实现的,并且占用4字节内存。
但是我们用过引用的,此时心中都有了个疑惑,既然引用占内存,为什么我却无法获得引用的地址啊。接下来我就用下面代码来解释这个疑惑:
之所以不能获取引用的地址,是因为编译器进行了内部转换。
如下代码:

int a = 99;
int &r = a;
r = 18;
cout<<&r<<endl;

编译时会被转换成如下的形式:

int a = 99;
int *r = &a;
*r = 18;
cout<<r<<endl;

使用&r取地址时,编译器会对代码进行隐式的转换,使得代码输出的是 r 的内容(a 的地址),而不是 r 的地址,这就是为什么获取不到引用变量的地址的原因。也就是说,不是变量 r 不占用内存,而是编译器不让获取它的地址。

理解了引用底层就是指针的方式,心中不仅又会产生疑惑,既然引用是指针来实现的,那直接用指针不就好了吗,为什么还要发明引用这么一个东西。

三、引用和指针的区别,用引用的好处

C++ 的发明人 Bjarne Stroustrup 说过,他在 C++ 中引入引用的直接目的是为了让代码的书写更加漂亮,尤其是在运算符重载中,不借助引用有时候会使得运算符的使用很麻烦。
感觉其实,引用的出现纯粹是为了优化指针的使用,而提出的语法层面的处理
(1)非空区别:在任何情况下都不能使用指向空值的引用。 一个引用必须总是指向某些对象。因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候可能不指向任何对象。这时你就应该声明该变量为指针,因为这样你就可以赋空值的。
相反,如果变量肯定指向一个对象,并且设计不允许变量为空,这时就把变量声明为引用。
这意味着,使用引用的效率比使用指针高。
(2)合法性区别:在使用引用之前不需要测试它的合法性。使用指针总是要判空。

(3)可修改区别:指针可以被重新赋值;引用则总指向初始化时被指定的对象,以后不许修改,但是指定的对象的内容可以改变。

(4)应用区别:使用指针:存在不指向任何对象的可能,可以设指针为空;需要改变指向,在不同的事件指向不同的对象。
使用引用:总指向一个对象并且一旦指向,就不回改变。

(5)可以有 const 指针,但是没有 const 引用。也就是说,引用变量不能定义为下面的形式:

int a = 20;
int & const r = a;

因为 r 本来就不能改变指向,加上 const 是多此一举。

(6)指针可以有多级,但是引用只能有一级,例如,int **p是合法的,而int &&r是不合法的。如果希望定义一个引用变量来指代另外一个引用变量,那么也只需要加一个&,如下所示:

int a = 10;
int &r = a;
int &rr = r;

(7)指针和引用的自增(++)自减(–)运算意义不一样。对指针使用 ++ 表示指向下一份数据,对引用使用 ++ 表示它所指代的数据本身加 1;自减(- -)也是类似的道理。请看下面的例子:

#include <iostream>
using namespace std;

int main (){
    int a = 10;
    int &r = a;
    r++;
    cout<<r<<endl;
   
    int arr[2] = { 27, 84 };
    int *p = arr;
    p++;
    cout<<*p<<endl;

    return 0;
}

在这里插入图片描述
明白了以上这些内容,算是对引用的本质了解清楚了。但是在使用引用时,还要注意一下两点:

引用不能绑定到临时数据

指针就是数据或代码在内存中的地址,指针变量指向的就是内存中的数据或代码。这里有一个关键词需要强调,就是内存,指针只能指向内存,不能指向寄存器或者硬盘,因为寄存器和硬盘没法寻址。
注意: 一些我们平时不太留意的临时数据,例如表达式的结果、函数的返回值等,它们可能会放在内存中,也可能会放在寄存器中。一旦它们被放到了寄存器中,就没法用&获取它们的地址了,也就没法用指针指向它们了。
如下所示:

int n = 100, m = 200;
int *p1 = &(m + n);    //m + n 的结果为 300
int *p2 = &(n + 100);  //n + 100 的结果为 200
bool *p4 = &(m < n);   //m < n 的结果为 false

对此主要提醒一点即可: C++ 对引用的要求更加严格,不管临时数据是存储在内存还是寄存器上,在某些编译器下都不能指代。

但是,哈哈哈,毕竟凡事都有特殊对吧,引用也不排除再外啊。

那就是当使用 const 关键字对引用加以限定后,引用就可以绑定到临时数据了。
如下代码所示:

int main(){
    int m = 100, n = 36;
    const int &r1 = m + n;
    const int &r2 = m + 28;
    const int &r3 = 12 * 3;
    const int &r4 = 50;
    return 0;
}

如上代码是正确的,这是因为将常引用绑定到临时数据时,编译器采取了一种妥协机制:编译器会为临时数据创建一个新的、无名的临时变量,并将临时数据放入该临时变量中,然后再将引用绑定到该临时变量。注意,临时变量也是变量,所有的变量都会被分配内存。

此时,我们不禁心中疑惑为什么编译器为常引用创建临时变量是合理的,而为普通引用创建临时变量就不合理呢?

原因如下:
(1)我们知道,将引用绑定到一份数据后,就可以通过引用对这份数据进行操作了,包括读取和写入(修改);尤其是写入操作,会改变数据的值。而临时数据往往无法寻址,是不能写入的,即使为临时数据创建了一个临时变量,那么修改的也仅仅是临时变量里面的数据,不会影响原来的数据,这样就使得引用所绑定到的数据和原来的数据不能同步更新,最终产生了两份不同的数据,失去了引用的意义。
(2) const 引用和普通引用不一样,我们只能通过 const 引用读取数据的值,而不能修改它的值,所以不用考虑同步更新的问题, 也不会产生两份不同的数据,为 const 引用创建临时变量反而会使得引用更加灵活和通用。

明白了这个,下面再看另一个知识

const引用与转换类型

学引用,要先了解下指针,毕竟引用底层还是指针。
指针的类型要与它指向的数据的类型严格对应。
如下代码所示,

int n = 100;
int *p1 = &n;  //正确
float *p2 = &n;  //错误

char c = '@';
char *p3 = &c;  //正确
int *p4 = &c;  //错误

虽然 int 可以自动转换为 float,char 也可以自动转换为 int,但是float *类型的指针不能指向 int 类型的数据,int *类型的指针也不能指向 char 类型的数据。

那为什么编译器禁止指针指向不同类型的数据是合理的呢?
以 int 类型的数据和float *类型的指针为例,我们让float *类型的指针强制指向 int 类型的数据,看看会发生什么。下面的代码演示了这一幕:

#include <cstdio>
using namespace std;
int main(){
    int n = 100;
    float *p = (float*)&n;
    *p = 19.625;
    printf("%d\n", n);

    return 0;
}

在这里插入图片描述
将 float 类型的数据赋值给 int 类型的变量时,会直接截去小数部分,只保留整数部分,本例中将 19.626 赋值给 n,n 的值应该为 19 才对,这是我们通常的认知。但是本例的输出结果是一个毫无意义的数字,它与 19 没有任何关系,这颠覆了我们的认知。
虽然 int 和 float 类型都占用 4 个字节的内存,但是程序对它们的处理方式却大相径庭:
(1)对于 int,程序把最高 1 位作为符号位,把剩下的 31 位作为数值位;
(2)对于 float,程序把最高 1 位作为符号位,把最低的 23 位作为尾数位,把中间的 8 位作为指数位。

引用(Reference)和指针(Pointer)在本质上是一样的,引用仅仅是对指针进行了简单的封装,类型严格一致这条规则同样也适用于引用。下面的例子演示了错误的引用使用方式:

int n = 100;
int &r1 = n;  //正确
float &r2 = n;  //错误
char c = '@';
char &r3 = c;  //正确
int &r4 = c;  //错误 

故事到此就结束了吗,不,下面才真正引出引用的特殊地方

类型严格一致是为了防止发生让人匪夷所思的操作,但是这条规则仅仅适用于普通引用,当对引用添加 const 限定后,情况就又发生了变化,编译器允许引用绑定到类型不一致的数据。请看下面的代码:

int n = 100;
int &r1 = n;  //正确
const float &r2 = n;  //正确

char c = '@';
char &r3 = c;  //正确
const int &r4 = c;  //正确 

当引用的类型和数据的类型不一致时,如果它们的类型是相近的,并且遵守数据类型的自动转换规则,那么编译器就会创建一个临时变量,并将数据赋值给这个临时变量(这时候会发生自动类型转换),然后再将引用绑定到这个临时的变量, 这与将 const 引用绑定到临时数据时采用的方案是一样的。

注意,临时变量的类型和引用的类型是一样的, 在将数据赋值给临时变量时会发生自动类型转换。请看下面的代码:

float f = 12.45;
const int &r = f;
printf("%d", r);

在这里插入图片描述
当引用的类型和数据的类型不遵守数据类型的自动转换规则,那么编译器将报错,绑定失败,例如:

char *str = "https://blog.csdn.net/weixin_52259848?type=lately";
const int &r = str;

char *和int两种类型没有关系,不能自动转换,这种引用就是错误的。

特此提醒,当引用作为函数参数时,如果在函数体内部不会修改引用所绑定的数据,那么请尽量为该引用添加 const 限制。即引用类型的函数形参请尽可能的使用 const
概括起来说,将引用类型的形参添加 const 限制的理由有三个:
(1)使用 const 可以避免无意中修改数据的编程错误;
(2)使用 const 能让函数接收 const 和非 const 类型的实参,否则将只能接收非 const 类型的实参;
(3)使用 const 引用能够让函数正确生成并使用临时变量。

至此,感觉终于把引用给能清楚了,如果各位发现还有那些内容没有到位的,可以告知下啊,让我们一起再往深的学习学习。

推荐一个零声学院免费公开课程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:服务器课程

相关文章:

  • [极客大挑战 2019]BabySQL
  • vue3+vite+windicss+element-plus+axios+router+cookies 搭建
  • ElasticSearch docker 方式安装
  • Spring——IOC 操作 Bean 管理(FactoryBean,作用域以及bean生命周期)
  • java毕业设计成品源码网站基于javaWeb停车场车辆管理系统的设计与实现|车位
  • 【Python零基础入门篇 · 5】:占位符和格式化输入输出、标识符和保留字
  • L77.linux命令每日一练 -- 第11章 Linux系统管理命令 -- vmstat和mpstat
  • 如何安装GCC?
  • latex(overleaf)制作ppt(演示文稿)笔记
  • C#:实现将整数转换为二进制表示形式的字符串算法(附完整源码)
  • .NET C#版本和.NET版本以及VS版本的对应关系
  • C#:实现KochSnowflake科赫雪花算法(附完整源码)
  • vim YouCompleteMe报错:version `GLIBCXX_3.4.20‘ not found 解决方法
  • 数字藏品和NFT有什么区别?
  • 【Python零基础入门篇 · 4】:字符串的运算符、下标和切片
  • [译] 怎样写一个基础的编译器
  • Android 初级面试者拾遗(前台界面篇)之 Activity 和 Fragment
  • ES6语法详解(一)
  • Hibernate最全面试题
  • HTML5新特性总结
  • SpringCloud(第 039 篇)链接Mysql数据库,通过JpaRepository编写数据库访问
  • vuex 笔记整理
  • Wamp集成环境 添加PHP的新版本
  • 反思总结然后整装待发
  • 基于Mobx的多页面小程序的全局共享状态管理实践
  • 基于阿里云移动推送的移动应用推送模式最佳实践
  • 记一次用 NodeJs 实现模拟登录的思路
  • 以太坊客户端Geth命令参数详解
  • 用Python写一份独特的元宵节祝福
  • ​LeetCode解法汇总2583. 二叉树中的第 K 大层和
  • ​ssh免密码登录设置及问题总结
  • ​批处理文件中的errorlevel用法
  • ###51单片机学习(2)-----如何通过C语言运用延时函数设计LED流水灯
  • $var=htmlencode(“‘);alert(‘2“); 的个人理解
  • (ISPRS,2023)深度语义-视觉对齐用于zero-shot遥感图像场景分类
  • (libusb) usb口自动刷新
  • (Redis使用系列) SpirngBoot中关于Redis的值的各种方式的存储与取出 三
  • (Redis使用系列) Springboot 使用redis实现接口幂等性拦截 十一
  • (八)五种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (南京观海微电子)——COF介绍
  • (完整代码)R语言中利用SVM-RFE机器学习算法筛选关键因子
  • (一)pytest自动化测试框架之生成测试报告(mac系统)
  • (转)Google的Objective-C编码规范
  • .[hudsonL@cock.li].mkp勒索加密数据库完美恢复---惜分飞
  • .\OBJ\test1.axf: Error: L6230W: Ignoring --entry command. Cannot find argumen 'Reset_Handler'
  • .bat批处理(三):变量声明、设置、拼接、截取
  • .NET 5.0正式发布,有什么功能特性(翻译)
  • .net Application的目录
  • .NET Core 实现 Redis 批量查询指定格式的Key
  • .Net FrameWork总结
  • .NET NPOI导出Excel详解
  • .net2005怎么读string形的xml,不是xml文件。
  • .Net的C#语言取月份数值对应的MonthName值
  • .NET委托:一个关于C#的睡前故事
  • .NET学习全景图