C++自制心得——开篇(引用)
前言:
本心得只适合给那些拥有C语言基础 (系统的自主学习过C语言(以找工作为目标,学校老师教的一律视作没学),了解一些底层机制,用C实现过一些基本数据结构 (顺序表、链表、队列与栈、初等二叉树、十大经典排序等) ) 的人观看。如果你不满足上述条件就不要在这里浪费时间了。
本人在写专栏上的技术力并不好,如果你觉得这篇文章在排版上有需要改进的地方可以在评论区留言并附上具体操作流程。
本人目前是大二在读生,目前刚开始学习C++相关知识,如果有大佬发现哪里介绍的不对或者有疏漏欢迎在评论区留言,我尽量在发现的第一时间纠正。
好,废话不多讲,我们进入正题。
引用:
什么是引用

这就是一个简单的引用代码。在C++里,引用本质上是指针的pro max plus版(据说在Java里引用是传值调用),所以它有如下特点:
1. 在定义时必须指定是哪个变量的引用(Java可以不给)
2. 创建完成后不可更改指向(Java可以改)
3. 引用与其指向的变量共用一块空间
4. 对引用的合法更改会同步影响它所指的对象
5. 一个变量可拥有无数个引用(可以套娃)
Java这作业抄的真爽
(tips: 关于第二点,如果能改,C++就和Java一样再也不需要指针了,我怀疑C++这么设计就是为了保住指针仅剩的就业空间)


(tips: 在调试的时候打开反汇编窗口就能看到代码的汇编层,算是一个很实用的小技巧)
我来简单讲一下这几条汇编指令的意思:
mov dword ptr [a], 0 把值0放入内存地址a维护的空间
lea eax, [a] 把内存地址a放入寄存器eax
mov dword ptr [b], eax 把寄存器eax的值放入内存地址b维护的空间
(我其实不用讲汇编的,把这两张图贴出来就够了)
引用的常见使用场景
显然,如果不需要更改指定对象,引用在代码可读性与美观度上比指针好得多,只有像类似链表,二叉树等这一类需要频繁更改指定对象的情况才需要用到指针。让我们上点实例:
传引用的Swap函数是不是比以前好用了不少?再上一个更复杂的例子。
test1.c
二叉树遍历(C).h
二叉树遍历(C).c
这段代码用于输出该二叉树的中序遍历,在_MidorderTraversal函数有一个参数int* curPos,表示此时返回用前序遍历数组的待填空位。显然,以这个函数的逻辑,我们希望这个参数独立存在,不受递归调用的影响。因此这个参数应为全局变量,或者是指针变量,也可以是静态变量,上面的代码用的是第二种。现在有了引用就可以换一种写法了。
test2.cpp
二叉树遍历(C++).h
二叉树遍历(C++).cpp
觉得代码没有发生翻天覆地的变化?那我们再来一个。
test3.c
s_clist.h
s_clist.cpp
这是一个经典的单链表尾插,有没有注意到尾插函数里的二级指针?超讨厌(〝▼皿▼),但不得不用,谁叫在头指针为空的情况下我们要改变头指针本身,那只能传头指针的地址,即二级指针。不过现在,用引用就行了。
test3.c(修改后)
s_clist.h(修改后)
s_clist.cpp(修改后)
就问你,爽不爽?讲到这里我突然想开个腔,请看下面的代码:
这个代码是不是有点眼熟?没错它就是在数据结构课上很多老师教的单链表写法(据说有些数据结构的书上也是这么写的),好多学生看不懂。当然,现在的我们肯定是能看懂的,不就是把struct SClistNode* 重定义为PlistNode,然后函数传PlistNode的引用。这和SClistNode*&没有区别,干嘛写的这么别扭?据某些小道消息称,出书的人觉得指针已经够难了,再给你一个二级指针不就集体Orz了。所以他们整了一个烂活,把代码写成上面的样子,觉得这样同学们就能看懂了,把👴看乐了。期待各路大仙在评论区的发挥。
引用的一些特性
1. 传值调用与传引用调用的效率问题

以这段代码为例,函数a每次传参都要拷贝四十万字节的数据,整个函数调用了十万次,也就是说我们整整拷贝了37.25个G的数据。与此同时,函数b每次传参不拷贝数据。显然,在形参大小较大和函数频繁调用的情况下传引用调用比传值调用拥有更好的性能优势。
2. 常引用问题

3. 引用做返回值(不知道函数栈帧创建和销毁的同学建议去了解一下)
这块的问题很有意思,我会重点讲。
对于传值返回,返回的是变量的值,而不是变量的地址,返回值会有一个临时空间接收,一般情况下,这个临时空间会是一个寄存器或者是一个在主调函数栈帧与被调函数栈帧之间的预先开好的空间(函数的形参也会被存在这个夹缝里,后面的题会用到这个小知识)
对于传引用返回,返回的是变量的地址,而不是变量的值,如果返回的变量已经随着函数栈帧的销毁被释放,那这就成了一个经典的野指针问题。
最有意思的部分来了,猜猜看这段代码的运行结果。








下一个问题,为什么结果是这样的?







对于第一第二行输出,我们用的是int型变量接收的int&返回值,执行赋值操作,尽管int&返回值所在的空间已被释放,但释放操作与赋值操作间隔时间过短,那个空间还没有被其他线程占用,因此空间内的值没有被更改,返回值就被保存在一个稳定空间内。在这种情况下,除了会爆非法越界警告,没有任何异常。
对于第三第四行输出,我们们用的是int&型变量接收的int&返回值,此时我们保存了返回值的地址,接下来执行printf操作,前文提到函数形参会被保存在主调函数栈帧与被调函数栈帧的夹缝里。在printf函数准备形参时,因为其调用空间没有触及返回值所在空间,此时形参值正常,紧接着printf的函数栈帧开辟,返回值所在空间被占用,返回值被销毁。从第二次调用printf函数开始,因返回值被销毁,形参值异常,打印在屏幕上的结果就是随机值。
这道题就留给各位了,答案扣在评论区就行。
这种代码虽然没什么实际意义,但是讲解这种匪夷所思的错误示范能让你对下面这个结论印象深刻。只有当返回值所在的空间不会随着函数栈帧的销毁被释放,才能使用引用返回。
那引用返回有什么让人眼前一亮的用途吗?让我们来个大的。
test3.cpp
s_clist.h
s_clist.cpp
我刚刚又写了一个查找和一个修改函数,在C语言里我们需要给查找和修改分别设计一个接口,但是现在有了引用返回,我们就可以这么玩了。
test3.cpp(修改后)
s_clist.h(修改后)
s_clist.cpp(修改后)
诸位,引用返回爽不爽,还用不用C写代码了?
现在总结一下引用的好处:
传引用传参
1. 提高效率
2. 输出型参数
传引用返回
1. 提高效率
2. 修改返回对象
引用讲完了,离正餐就不远了,目前就剩下一个运算符重载要花点时间,剩下的一笔带过就行。基础知识搞定了,就要开始玩类和对象了,难度会再次加码,同志们还要继续努力。