我的C++奇迹之旅:值和引用的本质效率与性能比较

请添加图片描述

文章目录

  • 📝引用
  • 🌠引用概念
    • 🌉引用特性
  • 🌠使用场景
    • 🌉做参数(传值与传地址)
    • 🌉传值、传引用效率比较
  • 🌠引用做返回值
    • 🌉引用和指针的区别
  • 🌠常引用
  • 🚩总结


📝引用

🌠引用概念

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

定义:类型&引用变量名(对象名) = 引用实体;
例如以下代码,在变量名前加一个&,意思是一个引用类型,ba的别名,也就是a有了一个外号,但还是a本身:

int a = 70;
int& b = a; //引用:b是a的别名

我们接下来看看引用后的地址是否会发生改变:
例如以下例子:

int main()
{int a = 70;int& b = a; //引用:b是a的别名int& c = a; //引用:c是a的别名c = 80;cout << a << endl;cout << &a << endl;cout << &b << endl;cout << &c << endl;return 0;
}

代码运行图:在这个代码中,定义了一个变量a70int& b = a; 这里ba的引用,给a取了一个外号bint& c = a; 这里ca的引用,又给a取了一个外号是c,因此我们对c还是对b进行修改,a都会发生改变,这是因为编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间
在这里插入图片描述

注意:引用类型必须和引用实体是同种类型的

🌉引用特性

  1. 引用必须在定义时初始化:
    引用必须在定义时初始化,不能在之后单独为它赋值
int a = 10; 
int& ra = a; // 正确,ra初始化为a
int& ra; // 错误,引用必须在定义时初始化
  1. 一个变量可以有多个引用
int a = 10;int& ref1 = a; 
int& ref2 = a;ref1++; // a的值变为11
cout << a << endl; // 输出11ref2--; // a的值变为10
cout << a << endl; // 输出10
  1. 引用一旦引用一个实体,再不能引用其他实体
    引用本质上就是给原变量添加一个别名,它的内存地址就是原变量的地址。所以对引用赋值或修改,实际上就是修改原变量。而指针不同,指针可以改变指向的对象:一级指针可以改变指向,如p可以从指向a改为指向其他变量,二级指针可以改变一级指针指向的地址,如pp可以改变p指向的地址

而引用更像一个const指针:定义后不能改变指向的对象,就像const指针定义后不能改变指向
但可以通过这个“const指针”来修改原对象,就像通过const指针可以修改原对象

int a = 10;
int b = 20;int& ref = a; 
ref = b; // 错误!引用ref已经引用a,不能再引用bcout << ref << endl; // 输出10,ref依然引用a

如图:ref引用了a,这里的值发生改变是因为b赋值给了ref
在这里插入图片描述

🌠使用场景

🌉做参数(传值与传地址)

当引用用来做参数时将会对代码进行大大的优化,并且具有可读性,如:当我们看了很多遍的交换了两个数的函数:

void Swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}int main()
{int ra = 88;int rb = 99;Swap(&ra, &rb);return 0;
}

形参是实参的一份临时拷贝,所以如果想交换需要,传地址,不能传值。

void Swap(int& a, int& b)
{int tmp = a;a = b;b = tmp;
}
int main()
{int ra = 88;int rb = 99;Swap(ra, rb);return 0;
}

ab分别是rarb的别名,当你调换ab的纸时,其实是修改了rarb的地址的值,这样的好处就是,当你看代码时,引用ab给人一种感觉,就是操作rarb本身。这隐藏了底层是通过地址操作原变量rarb的实现细节。从使用者的角度看,代码读起来就像直接交换rarb,而不是通过复杂的地址操作实现

这里使用了引用挺好的,不用担心指针的解引用,地址相关操作,但是,前面我们知道,引用一旦指向一个实体,就无法改变指向,例如,有关链表操作,当我们要删除一个节点,是不是要改变前面节点的指针,让他指向后面节点,而引用恰恰不能改变,因此,引用也不是完全替代指针的
在这里插入图片描述
回归正题,这里还有一个小注意点:作用域的不同,因此,在Swap函数里,取别的名字都可以,任由发挥,结果都相同。

void Swap(int& x, int& x)
{int tmp = x;x = y;y = tmp;
}
int main()
{int ra = 88;int rb = 99;Swap(ra, rb);return 0;
}

🌉传值、传引用效率比较

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

#include <time.h>
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 < 10000; ++i)TestFunc1(a);size_t end1 = clock();// 以引用作为函数参数size_t begin2 = clock();for (size_t i = 0; i < 10000; ++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;
}

按值传递(TestFunc1):
TestFunc1(a)时,会将a进行拷贝,生成一个临时对象**a_copy**。**a_copy**作为参数传递给TestFunc1TestFunc1内部操作的实际上是a_copy,对a_copy的修改不会影响实参aTestFunc1返回时,临时对象a_copy会被销毁。TestFunc1以值方式传递结构体A作为参数。这会导致每次调用都会对A进行值拷贝,对于一个包含10000int成员的大结构体,拷贝开销很大。

按引用传递(TestFunc2):
调用TestFunc2(a)时,不会进行值拷贝,直接传递a的引用。TestFunc2内部操作的仍然是实参a本身。TestFunc2返回时,不需要销毁任何对象。TestFunc2以引用方式传递A。这种方式下,函数内直接操作的就是实参a本身,不会有任何拷贝开销。

总结:
TestFunc1值传递,效率低是因为值拷贝开销大
TestFunc2引用传递,效率高是因为避免了值拷贝,直接操作的就是实参a本身

在这里插入图片描述

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

🌠引用做返回值

int& Count()
{static int n = 0;n++;// ...return n;
}

我们先看看下面代码会输出什么结果?

int& Add(int a, int b)
{int c = a + b;return c;
}
int main()
{int& ret = Add(1, 2);Add(3, 4);cout << "Add(1, 2) is :"<< ret <<endl;return 0;
}

在Vs编译运行图:结果是7,真的是正确吗?
在这里插入图片描述

问题分析:
如果函数返回时,返回的对象已经超出了函数作用域(即已经被销毁),那么不能返回该对象的引用,必须返回值。

在第一个示例中:

int& Add(int a, int b)
{int c = a + b;return c;
}

这里函数返回了局部变量c的引用,但c在函数返回后就已经被销毁了,所以这是一个未定义行为,输出结果是不确定的。

而在第二个示例中:

int& Add(int a, int b)
{int c = a + b;return c;
}
int main()
{int& ret = Add(1, 2);Add(3, 4);cout << "Add(1, 2) is :"<< ret <<endl;return 0;
}

这里同样是返回了局部变量c的引用,但是在main函数中又调用了一次Add函数,这时第一次调用返回的引用ret已经指向了一个不存在的对象,所以输出结果也是未定义的。

函数返回引用时必须确保返回的对象在调用者作用域内仍然存在,否则就会产生未定义行为。这是C++中函数返回引用需要特别注意的地方。
在这里插入图片描述

答案思考:
Visual Studio上运行这段代码,输出结果是:

Add(1, 2) is :7

这个结果确实是未定义行为,但在某些情况下可能会输出7。之所以会出现这种情况,是因为Visual Studio的编译器在处理这种未定义行为时可能会做一些特殊的优化或处理,导致在某些环境下能够得到一个看似合理的结果。但这种行为是不可靠的,因为它依赖于具体的编译器实现细节。在不同的编译器或环境下,输出可能会完全不同。

正确的做法:是要么返回值,要么返回一个在调用者作用域内仍然存在的对象的引用。这样可以确保代码的行为是可预测和可移植的。

🌉引用和指针的区别

  1. 语法概念:

    • 引用是变量的别名,没有独立的存储空间,而是和其引用的实体共用同一块内存空间。
    • 指针是一个独立的变量,存储了另一个变量的内存地址。
  2. 声明语法:

    • 引用使用&符号声明,如int& ref = x;
    • 指针使用*符号声明,如int* ptr = &x;
  3. 操作方式:

    • 引用直接访问和操作其引用的实体,如ref = 10;
    • 指针需要先解引用(*)才能访问其指向的实体,如*ptr = 10;
  4. Null值:

    • 引用不能为空(Null),必须在声明时初始化为一个有效的实体。
    • 指针可以为空(Null),指向空地址0x0

让我们看看例子来说明引用和指针的区别:
假设我们有一个整型变量x,值为10。
使用引用:

int x = 10;
int& ref = x; // 声明引用ref,它是x的别名
ref = 20; // 通过ref修改x的值
cout << "x = " << x << endl; // 输出 x = 20

refx的引用,它们共享同一块内存空间。通过ref修改值,实际上是在修改x的值。 输出x的值为20,因为x的值已经被修改了。

使用指针:

int x = 10;
int* ptr = &x; // 声明指针ptr,存储x的地址
*ptr = 20; // 通过ptr解引用并修改x的值
cout << "x = " << x << endl; // 输出 x = 20

ptr是一个指向x的指针,存储了x的内存地址。通过*ptr解引用并修改值,实际上是在修改x的值。输出x的值为20,因为x的值已经被修改了。

底层实现上实际是有空间的,因为引用是按照指针方式来实现的

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. 引用比指针使用起来相对更安全

🌠常引用

从上述代码中,我们可以得出以下关于常引用的结论:

  1. 常量引用:
    const int a = 10;//int& ra = a;   // 该语句编译时会出错,a为常量const int& ra = a;

对于常量对象a,我们可以使用常引用const int& ra = a;来引用它。这样做可以避免对常量进行修改,直接使用非常引用int& ra = a;会在编译时报错,因为不允许对常量进行非常引用。
2. 字面量常引用:

// int& b = 10; // 该语句编译时会出错,b为常量const int& b = 10;

我们可以使用常引用const int& b = 10;来引用字面量常量。这样做可以避免创建临时变量, 直接使用非常引用int& b = 10;会在编译时报错,因为字面量不能被非常引用。
3. 类型不匹配的常引用:

double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;

根据类型不同的变量,如double d = 12.34;,我们可以使用常引用const int& rd = d;来引用它,直接使用非常引用int& rd = d;会在编译时报错,因为类型不匹配。


🚩总结

请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/787381.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

苹果开发者账号注册步骤中的常见疑问解答与技巧分享

转载&#xff1a;注册苹果开发者账号的方法 在2020年以前&#xff0c;注册苹果开发者账号后&#xff0c;就可以生成证书。 但2020年后&#xff0c;因为注册苹果开发者账号需要使用Apple Developer app注册开发者账号&#xff0c;所以需要缴费才能创建ios证书了。 所以新政策出…

蓝桥杯-单片机基础12——对芯片LM555产生的频率脉冲计数思路

蓝桥杯单片机组备赛指南请查看 &#xff1a;本专栏第1篇文章 本文章针对蓝桥杯-单片机组比赛开发板所写&#xff0c;代码可直接在比赛开发板上使用。 型号&#xff1a;国信天长4T开发板&#xff08;绿板&#xff09;&#xff0c;芯片&#xff1a;IAP15F2K61S2 &#xff08;使…

如何在CentOS安装StackEdit Markdown编辑器并实现无公网IP远程访问使用

最近&#xff0c;我发现了一个超级强大的人工智能学习网站。它以通俗易懂的方式呈现复杂的概念&#xff0c;而且内容风趣幽默。我觉得它对大家可能会有所帮助&#xff0c;所以我在此分享。点击这里跳转到网站。 文章目录 前言1. ubuntu安装VNC2. 设置vnc开机启动3. windows 安…

反射的学习

反射的作用&#xff1a; 1.获取一个类里面的所有信息&#xff0c;获取到之后&#xff0c;在执行其他的业务逻辑 2.结合配置文件&#xff0c;动态的创建对象并调用方法

(4)(4.5) Underwater Sonar (Analog)

文章目录 前言 1 推荐硬件 2 连接和配置 3 参数说明 前言 本页详细介绍了低成本模拟水下声纳&#xff08;又称"探鱼器"&#xff09;和数字转换器的设置&#xff0c;数字转换器可将模拟读数转换成 NMEA 0183&#xff0c;供 ardupilot 读取。这种设置可以测量船下…

Kubernetes篇(三)— 资源管理

目录 前言资源管理介绍YAML语言介绍资源管理方式命令式对象管理命令式对象配置声明式对象配置 前言 本章节主要介绍yaml语法和kubernetes的资源管理方式 资源管理介绍 在kubernetes中&#xff0c;所有的内容都抽象为资源&#xff0c;用户需要通过操作资源来管理kubernetes。 …

【THM】SQL Injection(SQL注入)-初级渗透测试

简介 SQL(结构化查询语言)注入,通常称为 SQLi,是对 Web 应用程序数据库服务器的攻击,导致执行恶意查询。当 Web 应用程序使用未经正确验证的用户输入与数据库进行通信时,攻击者有可能窃取、删除或更改私人数据和客户数据,并攻击 Web 应用程序身份验证方法以获取私有数据…

java-map接口学习

java-map接口学习 Java Map接口HashMapLinkedHashMapTreeMap例子 Java Map接口 Map接口是基于键(key)和值(value)对的集合。每个键值对被称为一个条目(entry)。Map中的键是唯一的。 如果需要根据键进行搜索、更新或删Java Map接口除元素&#xff0c;那么Map是很有用的。 在Ja…

PCB三大走线,如何高效率检查?

在PCB设计中&#xff0c;走线的布局与检查是至关重要的环节。按照走线类型&#xff0c;可分为直角走线、差分走线及蛇形线&#xff0c;如何针对这三种走线方式进行高效率检查&#xff0c;去也报电路的稳定性和可靠性&#xff1f; 1、直角走线 容性负载&#xff1a;观察直角拐角…

从SQL质量管理体系来看SQL审核(4)- 如何设计一个优秀的SQL审核引擎

如何设计一个优秀的SQL审核引擎 从SQL质量管理体系来看SQL审核系列包括多篇文章&#xff0c;从SQL质量管理体系的角度来讨论如何设计一个优秀SQL审核引擎&#xff0c;欢迎订阅。 从SQL质量管理体系来看SQL审核&#xff08;1&#xff09;- SQL质量管理体系概览 从SQL质量管理…

YOLOv9改进策略 :主干优化 | 无需TokenMixer也能达成SOTA性能的极简ViT架构 | CVPR2023 RIFormer

💡💡💡本文改进内容: token mixer被验证能够大幅度提升性能,但典型的token mixer为自注意力机制,推理耗时长,计算代价大,而RIFormers是无需TokenMixer也能达成SOTA性能的极简ViT架构 ,在保证性能的同时足够轻量化。 💡💡💡RIFormerBlock引入到YOLOv9,多个数…

JavaEE 初阶篇-深入了解单例模式(经典单例模式:饿汉模式、懒汉模式)

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 单例模式的概述 2.0 单例模式 - 饿汉式单例 2.1 关于饿汉式单例的线程安全问题 3.0 单例模式 - 懒汉式单例 3.1 关于懒汉式单例的线程安全问题 3.1.1 加锁 synchr…

spring boot3登录开发-3(2短信验证登录/注册逻辑实现)

⛰️个人主页: 蒾酒 &#x1f525;系列专栏&#xff1a;《spring boot实战》 &#x1f30a;山高路远&#xff0c;行路漫漫&#xff0c;终有归途 目录 写在前面 上文衔接 内容简介 功能分析 短信验证登录实现 1.创建交互对象 用户短信登录/注册DTO 创建用户登录VO…

IOTX:未来市场爆发点的RWA协议?DePIN赛道被低估的龙头

从基本面来看&#xff0c;IoTeX的目标是创建一个连接的世界&#xff0c;在这个世界中&#xff0c;每个人都能控制自己的数据、设备和身份。通过区块链技术&#xff0c;IoTeX旨在解锁智能设备和数据的潜力&#xff0c;支持新一代的现实世界Dapp和数字资产的发展。IOTX始终致力于…

红黑树剖析

目录 一. 红黑树的概念 二. 红黑树的性质 三. 红黑树节点的定义 四. 红黑树的插入操作 4.1 uncle存在且颜色为红 4.2 uncle不存在或者uncle存在且为黑 五. 整体代码展示 一. 红黑树的概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每…

OpenCV 4.9基本绘图

返回&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;OpenCV使用通用内部函数对代码进行矢量化 下一篇&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; ​目标 在本教程中&#xff0c;您将学习如何&am…

广东小团队惊艳业绩揭秘:链动模式助力面膜销售破千万!

惊爆&#xff01;广东一个默默无闻的小团队竟然在短短一个月内&#xff0c;仅凭销售面膜就实现了超过千万的惊人业绩&#xff01;这背后究竟隐藏着怎样的秘密武器呢&#xff1f; 揭开链动模式的神秘面纱 链动模式&#xff0c;作为社交电商领域的一股新兴力量&#xff0c;正以其…

【强化学习的数学原理-赵世钰】课程笔记(一)基本概念

目录 一. 内容概述1. 通过案例介绍强化学习中的基本概念2. 在马尔可夫决策过程&#xff08;MDP&#xff09;的框架下将概念正式描述出来 二. 通过案例介绍强化学习中的基本概念1. 网格世界&#xff08;A grid world example&#xff09;2. 状态&#xff08;State&#xff09;3.…

Spring AOP + 自定义注解 实现公共字段的填充

Spring AOP 自定义注解 实现公共字段的填充 代码冗,不利于后期维护. 定义操作这些字段的方法类型 实现步骤&#xff1a; 自定义注解AutoFill,用于表示操作这些公共字段的方法自定义切面类AutoFillAspect,统一拦截&#xff0c;通过反射获取方法入参&#xff0c;并填充公共字段…

【THM】Burp Suite:Other Modules(其他模块)-初级渗透测试

介绍 除了广泛认可的Repeater和Intruder房间之外,Burp Suite 还包含几个鲜为人知的模块。这些将成为这个房间探索的重点。 重点将放在解码器、比较器、排序器和组织器工具上。它们促进了编码文本的操作,支持数据集的比较,允许分析捕获的令牌内的随机性,并帮助您存储和注释…