| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> C++知识库 -> C++要笑着学:引用 -> 正文阅读 |
|
[C++知识库]C++要笑着学:引用 |
🔥 🔥 🔥 🔥 🔥??火速猛戳订阅?👉??《C++要笑着学》? ?👈?趣味教学博客?🔥 🔥 🔥 🔥 🔥 写在前面:?啊朋友们好啊,我是柠檬叶子C。本章将对C++的基础,引用部分的知识进行讲解。 有些地方为了能够加深理解,我们会举几个比较有意思的栗子,在讲解的同时会适当的整活。 我觉得这篇博客是我目前写的最好的一篇,希望大家能看到底!如果觉得文章不错, 可以 "一键三连"?支持一下博主!你们的关注就是我更新的最大动力!Thanks ? (・ω・)ノ (PS:文章的排版也进一步升级了,方便大家能在手机端上看得舒服些) Ⅰ.?引用的概念0x00 引入话题不知道大伙知不知道?"抓捕周树人跟我鲁迅有什么关系"? 这个梗 ~? 这段是2018年电视剧《楼外楼》中鲁迅先生的一段台词。剧中一个没文化的军官带着一批人要来抓捕周树人,鲁迅让他们拿出搜捕令,他们拿了出来,鲁迅看过之后就说:抓捕周树人和我鲁迅有什么关系?于是这群人都以为是抓错人了,就走了。 这一段其实是在嘲讽他们没有文化,连作家的笔名都不知道。 "抓捕周树人跟我鲁迅有什么关系" ,当然有关系了!哈哈哈哈哈哈。 后来这个军官才知道鲁迅是周树人的笔名,他们要抓的人正是鲁迅。 🔺 这个 "笔名" 其实就是引用,我们继续往下学习。 0x01 什么是引用😂 第一次接触?"引用" 的概念时,直接看词去理解,真的会让人一脸懵逼…… 但是如果用 "取别名" 或 "取绰号" 来理解,就没有那么难以理解了。 🤔 但是如果用 "取别名" 或 "取绰号" 来理解,就没有那么难以理解了。 📚 概念:引用就是给一个已经存在的变量取一个别名。 📜 语法:数据类型&? 引用名?=? 引用实体; ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?👆 这里的&可不是取地址啊!它是放在数据类型后面的&,一定要区分开来! 💬 代码演示:
🔑 解读: 引用在语法层,我们要理解这里没有开新空间,就是对原来的取了一个新名称而已。 📌 注意事项: ① 引用并不是新定义一个变量,只是给一个变量取别名。 ② 编译器不会为引用的变量开辟内存空间,它和它引用的变量会共用同一块内存空间。 0x02 引用的特性?引用在定义时必须初始化! 初始化时必须要指定清楚,你到底是要给谁取别名。 含糊不清是不行的,你都不知道要给谁取别名,你取他干甚呢? ? 💬 又到了大伙最爱的踩坑环节:
?一个变量可以有多个引用。 一个人当然可以有多个绰号,所以一个变量也可以有多个别名。 💬 代码演示:(川普? 川建国? 懂王)
?引用一旦引用了一个实体,就不能引用其他实体了。?
(这里取名为 ra,因为引用的英文是 reference,所以我简写为 r,或者 ref 来代表引用) ? 问号处是什么意思呢?这里是让 ra 变成 b 的别名,还是把 b 的值赋值给 ra 呢? 💡 这里是赋值,我们打开监视窗口看一下: 🔺 引用是不会变的,我们定义它的时候它是谁的别名,就是谁的别名了。 以后就不会改了,它是从一而终的!!! 💬 引用和指针是截然不同的,指针是可以改变指向的:
🔑 解析:指针在这里就像极了渣男! 📚 这里再提一句,引用的底层其实就是指针。 你可以这么理解,引用他不想像以前那样做渣男了,于是回炉重造! 《重生之我不是渣男》,开始一生只爱一个人了! 印象是不是很深刻?好好理解!做一个对感情专一的人!!! Ⅱ. 引用的应用0x00?引入? 平常这么写其实没什么意义:
?? 它真正有用的地方在于它能够做参数和做返回值。 0x01?引用做参数?我们在C语言教学中讲过 Swap 两数交换的三种方式。? 我们当时用的最多的就是利用临时变量去进行交换。 如果把它写成函数形式就是这样:
?这里我们调用 Swap 函数需要传地址,因为形参是实参的一份临时拷贝,改变形参并不会对实参产生实质性的影响。 💬 但是,我们学了引用之后我们就可以这么玩: ??
? 是怎么做到交换的? 🔑 我们知道,形参是定义在栈帧里面的。 实际调用这个函数的时候,才会给 ra 和 rb 开空间。调用这个函数的时候,把实参传给形参。 那什么时候开始定义的?实参传给形参的时候开始定义的。 ra 是 a 的别名,rb 是 b 的别名,所以 ra 和 rb 的交换,就是 a 和 b 的交换。 因此,我们利用这一特点,就可以轻松实现两数的交换。 🔺 我们来梳理一下,顺带复习一下之前讲的函数重载。? 💬 现在我们一共学了三种传参方式:传值、传地址、传引用。
?这里 Swap(a,b) 为什么会报错呢? 这三个 Swap 是可以构成函数重载的, 只要不影响它的函数名修饰规则,就不会构影响! 换言之,修饰出来的函数名不一样,就支持重载!
但是?Swap(a,b) 调用时存在歧义。调用不明确! 它不知道调用哪一个,是传值还是传引用,所以会报错。 当时再讲数据结构单链表的时候用的是二级指针,当时没有采用头结点的方式。 那么要传指针的地址,自然要用二级指针的方式接收。 现在我们学了引用,我们就可以试着用引用的方法来解决了(这里我们把 .c 改为 .cpp) ?任何类型都是可以取别名的,指针也不例外:
?我们来看如何用引用的方法来实现! 💬 SList.h:
💬 SList.cpp:
🔑 解读:?这里的 SLNode*& rpHead 就是 pHead 的一个别名。 💬 Test.cpp:
🔑 解读:这里我们采用引用的方法,调用 SListPushBack 时传递的就是 pList 的引用。 ?这里补充一下,为了能够更简单地表示,有些书上还有这种写法。 定义链表结构的时候 typedef 多定义一个 pSLNode*
0x02 传值返回?讲引用返回前,我们需要做一点点铺垫。 💬 传值返回:
🔑 解读: 这里 return 的时候会生成一个临时变量(c 为 3) 将 3 复制给这个临时变量,然后返回给 ret 如果我们直接把 c 交给 ret,就会出现一些问题。 如果直接取 c 给 ret,取到的是 3 还是 随机值,就要取决于栈帧是否销毁空间! ?这个时候严格来说,其实都是非法访问了。 因为这块空间已经还给操作系统了,这就取决于编译器了。 有的编译器会清,有的编译器不会清,这就太玄学了! 所以,在这中间会生成一个临时变量,来递交给 ret 。 而不是直接用 c 作为返回值,造成非法访问。 所以这里不会直接用 c 作为返回值,而是生成一个临时变量。 ?那么问题来了,这个临时变量是存在哪里的呢? ① 如果 c 比较小(4或8),一般是寄存器来干存储临时变量的活。 ② 如果 c 比较大,临时变量就会放在调用 Add 函数的栈帧中。 🔺 总结:所有的传值返回都会生成一个拷贝 (这是编译器的机制,就像传值传参会生成一份拷贝一样) 我们用 VS 来反汇编操作一下,加深理解: ?🔑 解读:我们可以清楚的看到,确实是通过寄存器将 a + b 的结果交给 ret 的。 0x03 引用做返回值我们已经知道,这里会生成一个临时变量了。 我们现在回到正题,我们来试试引用的返回。 💬 体会下面的代码:
🔑 引用返回的意思就是,不会生成临时变量,直接返回 c 的别名。 ? 这段代码存在的问题: ① 存在非法访问,因为 Add(1, 2) 的返回值是 c 的引用,所以 Add 栈帧销毁后,会去访问 c 位置空间。 ② 如果 Add 函数栈帧销毁,清理空间,那么取 c 值的时候取到的就是随机值,给 ret 就是随机值,当前这个取决于编译器实现了。VS?下销毁栈帧,是不清空间数据的。 ?既然不清空间数据,那还担心什么呢? 💭 我们来看看下面这种情况:
🔑 解读:我们并没有动 ret,但是 ret 的结果变成了 30,因为栈帧被改了。 当再次调用 Add 时,这块栈帧的所有权就不是你的了。 我函数销毁了,栈帧就空出来了,新的函数覆盖了之前那个已经销毁的栈帧, ?所以 ret 的结果变成 30 了。 ?似乎还是不太好理解,为了加深印象,我举个形象(奇葩)的例子: 其实,操作系统对内存空间的管理就像是房东一样。 我们使用的内存就好比是找房东租房子一样。 建立栈帧,函数调用完成后,把房子还给房东了。 但是你偷偷地把行李箱留在了房间里, 如果恰好没有人来住这个房间,你去取这个行李箱时完全没有问题的。 但是如果有人住了,新的租客没动你的行李箱,也不会有问题。 就怕这个新租客把你放在这的行李箱给丢了,甚至直接把你的行李箱占为己有了。 把你里面的衣服(数据)都给扔了,还把自己臭袜子放进去了。 这时你再回去取你的行李箱,取到的也只有臭袜子了,也不是你的衣服了。 🤣 呼呼啦啦说一大堆,结论就是:不要轻易使用引用返回! ??那引用返回有什么存在的意义呢?等我们后面讲完类和对象后再细说。 🔺 总结: 日常当中是不建议用引用返回的,如果函数返回时,出了函数的作用域, 如果返回对象还未还给操作系统,则可以使用引用返回,如果已经还给操作系统了, 就不要用引用返回了,老老实实传值返回就行了。 ?通俗点说就是 —— 看返回对象还在不在栈帧内,在的话就可以使用引用返回。 💬 举个例子:静态变量,全局变量,出了作用域不会销毁
📌 注意事项:临时变量具有常性
具有常性,临时变量是右值(不可被修改),可以读但不能修改。 Ⅲ. 关于引用的探讨0x00?比较传值和传引用的效率? 那传值返回和传引用返回的区别是什么呢? 💡 传引用返回速度更快。 📚 以值作为参数或者返回值类型,在传参和返回期间,?函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时拷贝。 因此值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。 传值和传引用的效率比较:
值和引用作为返回值类型的性能对比: 💬 记录起始时间和结束时间,从而计算出两个函数完成之后的时间。
?传值返回会创建临时变量,每次会拷贝?40000 byte。 而传引用返回没有拷贝,所以速度会快很多很多,因为是全局变量所以栈帧不销毁。 所以这种场景我们就可以使用传引用返回,从而提高程序的运行效率。 🔺 总结:传值和船只真在作为传参以及返回值类型上效率相差十分悬殊。 引用的作用主要体现在传参和传返回值: ① 引用传参和传返回值,有些场景下面,可以提高性能(大对象?+?深拷贝对象)。 ② 引用传参和传返回值,输出型参数和输出型返回值。
0x01 引用和指针的区别在语法概念上:引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。 ?但是在底层的实现上:实际上是有空间的,因为引用是按照指针方式来实现的。
🔍 我们来看看引用和指针的汇编代码对比: 0x02 指针和引用的不同点
📚 我这里总结9个点,欢迎在评论区补充! ① 引用概念上定义一个变量的别名,而指针是存储一个变量的地址。 ② 引用在定义时必须初始化,指针最好初始化,但是不初始化也不会报错。
③ 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型的实体。 ④ 有空指针,但是没有空引用。 ⑤ 在 sizeof 中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节数(64位平台下占8个字节)
⑥ 引用++即引用的实体增加1,指针++即指针向后偏移一个类型的大小。
⑦ 有多级指针,但是没有多级引用。 ⑧ 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
⑨ 引用比指针使用起来相对更加安全。 🔺 总结:指针使用起来更复杂一些,更容易出错一些。(指针和引用的区别,面试经常考察) 使用指针有考虑空指针,野指针等等问题,指针太灵活了,所以相对而言没有引用安全! Ⅳ.? 常引用0x00 常引用的概念?如果既要利用引用来提高程序的效率,又想要保护传递给函数的数据不能在函数中被改变,就应使用常引用。 📜 语法:const 数据类型&? 引用名?=? 引用实体; 一共有三种情况:分别是权限的放大、保持权限不变、权限的缩小。 0x01 权限的放大💬 下面是一个引用的例子:
💬 如果对引用实体使用?const 修饰,直接引用会导致报错:
🔑 分析:导致这种问题的原因是,我本身标明了 const,这块空间上的值不能被修改。 我自己都不能修改,你 ra 变成我 a 的引用,意味着你 ra 可以修改我的 a, 这就是属于权限的放大问题,a 是可读的,你 ra 要变成可读可写的,当然不行。 这要是能让你随随便便修改,那我岂不是 const 了个寂寞? ?这合理吗?这不合理! ? 那么如何解决这样的问题,我们继续往下看。 0x02 保持权限的一致?既然引用实体用了 const 进行修饰,我直接引用的话属于权限的放大, 我们可以给引用前面也加上 const,让他们的权限保持不变。 💬 给引用前面加上 const:
🔑 解读:const int& ra = a 的意思就是,我变成你的别名,但是我不能修改你。 这样 a 是可读不可写的,ra 也是可读不可写的,这样就保持了权限的不变。 如果我们想使用引用,但是不希望它被修改,我们就可以使用常引用来解决。 0x03 权限的缩小?如果引用实体并没有被 const 修饰,是可读可写的,但是我希望它的引用不能修改它,我们可以用常引用来解决。 ? 💬 a 是可读可写的,但是我限制 ra 是可读单不可写:
🔑 解读:这当然是可以的,这就是权限的缩小。 举个例子,就好比你办身份证,你的本名是可以印在身份证上的, 但是你的绰号可以印在身份证上吗? 所以就需要加以限制,你的绰号可以被人喊,但是不能写在身份证上。 ?? ?所以,权限的缩小,你可以理解为是一种自我的约束。 0x04 常引用的应用💬 举个例子: ?假设 x 是一个大对象,或者是后面学的深拷贝的对象 那么尽量用引用传参,以减少拷贝。 如果 Func 函数中不需要改变 x,那么这里就尽量使用 const 引用传参。
加 const 后,让权限保持一致:
🔑 解读:如此一来,a 是可读不可写的,传进 Func 函数中也是可读不可写的, 就保持了权限的一致了。b 是可读可写的,刚才形参还没使用 const 修饰之前, x 是可读可写的,但是加上 const 后,属于权限的缩小,x 就是可读但不可写的了。
0x05?带常性的变量的引用💬 先看代码:
?这里的 d 是可以给 i 的,这个在C语言里面叫做隐式类型转换。 它会把 d 的整型部分给 i,浮点数部分直接丢掉。 ? 但是我在这里加一个引用呢?
直接用 i 去引用 d 是会报错的,思考下是为什么? 这里可能有的朋友要说,d 是浮点型,i 是整型啊,会不会是因为类型不同导致的? 但是奇葩的是,如果我在它前面加上一个 const 修饰,却又不报错了,这又是为什么?
?哎它* *滴!const,const 为什么行!!! 🔑 解析:因为?临时变量具有常性,不能被修改。 隐式类型转换不是直接发生的,而是现在中间产生一个临时变量。 是先把 d 给了临时变量,然后再把东西交给 i 的: 如果这里用了引用,生成的是临时变量的别名, 又因为临时变量是一个右值,是不可以被修改的,所以导致了报错。 🔺 结论:如果引用的是一个带有常性的变量,就要用带 const 的引用。 0x06 常引用做参数?使用引用传参,如果函数中不改变参数的值,建议使用 const& 💬 举个例子: 一般栈的打印,是不需要改变参数的值的,这里就可以加上 const?
const 数据类型&? 可以接收各种类型的对象。 使用 const 的引用好处有很多,如果传入的是 const 对象,就是权限保持不变; 普通的对象,就是权限的缩小;中间产生临时变量,也可以解决。 因为 const 引用的通吃的,它的价值就在这个地方,如果不加 const 就只能传普通对象。 ?谢谢你读到这里! 参考资料: Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. . 百度百科[EB/OL]. []. https://baike.baidu.com/. 比特科技. C++[EB/OL]. 2021[2021.8.31]. . 程序员面试宝典[M]. 5. .
本章完。 |
|
C++知识库 最新文章 |
【C++】友元、嵌套类、异常、RTTI、类型转换 |
通讯录的思路与实现(C语言) |
C++PrimerPlus 第七章 函数-C++的编程模块( |
Problem C: 算法9-9~9-12:平衡二叉树的基本 |
MSVC C++ UTF-8编程 |
C++进阶 多态原理 |
简单string类c++实现 |
我的年度总结 |
【C语言】以深厚地基筑伟岸高楼-基础篇(六 |
c语言常见错误合集 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 7:01:30- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |