前言
此程序是数据结构课程设计(运气好抽了个最简单的emmm^^),但是从这个相对简单的课设也学到了不少东西,下面做下记录
1、C++中指针的指针和引用
(1)首先再次深入了解指针和指针的指针
实际上,指针保存的就是我们存放数据的存储空间地址,指针的指针就是保存的存放指针的存储空间地址,通过指针保存的地址,我们通过*p
能够对该指针指向的地址的空间的内容进行操作
由此,我们能够得到一个结论1、实际上指针的指针也是归于指针变量的范畴,他不是多层概念,指针是我们希望通过地址对一个数据进行操作而引入的,指针和指针指向的数据就是一个两层概念,指针的指针只不过是我们希望通过地址对指针变量进行修改而创建的 ,以此类推我们希望通过地址对指针的指针进行修改,就需要创建相应类型的指针的指针的指针。。。。往后也一样。
(2)引用
我们都知道引用就是变量的别名,操作一个变量的引用也就相当于操作变量本身
但是引用是在编译的过程中被处理的,实际上就是在编译层面对程序员进行的一个比较友好的语法,而在实现上是由编译器完成了地址的传递,实质上还是指针
讲人话,第二个结论:使用一个引用变量操作,就相当于使用一个指向原数据的指针来对原数据进行操作,只不过过程被隐藏了,同时创造引用的人封装这个过程的同时还增加了一些,引用不能为空,引用不能更改等等的规定,防止使用中不必要的错误”
例如:
1 | #include<iostream> |
也就是说,使用引用变量a=b,就是*a=b,但是创建指针,使用时带星花,都省略了,表面理解就是“起了个别名”
规定引用不能为空,也是为了概念上契合“起个别名”,没有赋值,去给谁起别名操作呢?,引用不能改变,“别名”强制绑定一个对象,不能更改。
这里再添加一个误区说明,在C++中NULL的设定不是我们理解上的“空”而是:
1 |
所以一般来说NULL就是地址为0的概念,不是“空”,而大多机器中地址0中的内容一般是不准许用户更改的。一般用于系统或者硬件调用。所以NULL可以赋予给引用变量,编译通过但是一般不要对其进行操作
(3)函数返回引用值
问题:函数内部创建的变量都是局部变量,即私有的,作用域就在函数之内,为什么却可以把值传出去呢呢?根本就在于实现机制中,类似于形式参数,函数结束返回时,将局部变量值拷贝给一个临时变量,然后将这个临时变量返回给调用函数,这样即使局部变量在返回时已经释放内存,也不影响返回的变量值
既然类似于形式参数,那么我们返回值能不能类似的使用引用呢?
答案是肯定可以的
一个函数的返回值定义为引用形式,就相当于真的“返回了它本身”没有副本的概念。
然而实际上,它的实现依旧是指针。实际上封装的的操作就是用指针接受返回的&a,再用这个指针去操作其对应值。所以对应的,我们想要使用这个引用值,必须要使用引用变量,否则和返回非引用效果相同。同时,我们也明确到,这个引用返回必须不能是局部变量,否则函数结束,释放空间,我们引用返回封装中本质上返回的地址所存储的值也就不明确了。虽然编译可以通过只是给予警告,有时该局部空间也没有被重新分配保持原值,但是这样是没有保障的,风险极大,故我们一般不能使用。
当然如果非要返回一个局部变量的引用,可以new 类型(初值) 申请一个堆内存的临时变量,这样只要不delete释放,那么在程序退出之前内存就不会被释放,直到不再使用时便可delete掉.
通常情况下,我们希望做到通过一个函数,真的对外部原数据进行操作,而不是局部数据,这样使用引用参数或者指针参数是足够的,但是,若我们想对函数得到的返回值结果再次进入一个函数进行操作,且仍然实际上是操作这个返回值(前提已经是这个返回值变量不是局部变量了),那么我们就必须使用返回引用同时使用引用变量接受或者返回地址同时使用指针接受,然后再进入另一个参数为引用或者指针的函数进行操作才可以
例如本次的二叉排序树的查询和插入操作:
我们希望使用查询的结果作为插入的输入,进行对以root为根的树的操作,我们查找不到某数据后必定为结果NULL,这也是我们希望插入的位置,但是NULL是我们希望插入节点的父节点的指针域,我们希望链接上父节点,即将父节点的NULL修改后指向我们新插入的节点,就必须使用返回指针的指针或者返回指针引用(前面讲过,这是两层概念,而非多层,我们希望对指针内容原数据修改,就使用指针的指针),这样再进入插入函数操作我们才能真正意义上的做到父节点链接新节点,否则我们只是创建副本接受了NULL的这个值,做不到真正的使用父结点的指针域
使用指针:
1 | template <class T> |
使用引用:
1 | template <class T> |
(4)总结:
1、引用其实本质就是地址
2、当函数返回值类型为引用时,一般就用引用类型去接收,或者就使用了引用的作用,如果用非引用类型接受,就等于将函数返回的引用的数据值,复制给了该接收对象,和函数返回非引用类型是一样的效果。
3、当函数返回值类型为引用时,如果不考虑接收问题,则有一个特性,则是可以直接操作该函数返回的引用,如放在=左面 +=等.
4、错象:当在函数内部定义了局部变量(本质就是为一段内存取了一个名字,并占用),出了这个函数,这个局部变量不可再使用,也就是这个局部变量并不指向 任何一个内存了,但是这个局部变量原来所指的内存如果没有被系统重新分配,里面的值仍然没有变,如果有一个引用指向该局部变量,在局部变量被释放内存以 后,如果没有被系统重新将这段内存分配,那么其值仍可用。
5、不可以将常引用当作函数返回值返回.
6、用引用作函数参数和返回值类型的好处。直接是地址操作,不需要将值一一复制给形参,
7、返回值不需要有临时变量的存在,也不需要调用任何构造函数。节省了开销
8、一般当函数形参需要复杂类型的数据时,最好用引用,可以节省系统开销,
9、能用常引用的地方尽量用常引用。
10、如果非要返回一个局部变量的引用,可以new 类型(初值) 申请一个堆内存的临时变量,这样只要不delete释放,那么在程序退出之前内存就不会被释放,直到不再使用时便可delete掉.
推荐详细阅读:
2、封装一棵树
我们希望“定义一棵树”,真的就是定义一棵树,它的操作就是默认对本树操作,所以我们将很多递归程序或者具有使用根节点的程序分成两部分,主实现函数放入private域,不予公开,而添加调用函数,内容就是使用主实现函数,但是我们认为固定设定参数,例如树根指针就是本树的root
类似的操作我们在本次二叉树封装中进行了很多:
主实现函数:
1 | private: |
调用函数:
1 | public: |
这样从外部看,我们定义一个二叉排序树,对其进行创建、增删改查都是默认对这一个树进行操作,不再需要树根指针等参数了。
3、图形化输出一棵树
其实很简单:就是利用了右子树—>父结点—>左子树遍历,根据结点所处的层数和指针域状况中间夹杂着一些符合的判断添加
1 | template<class T> |
这样我们就能横向打印一个树了(结点数少的时候慢清晰的,能够直观根据树形判断自己的树功能是否正确,代码也不复杂,挺实用的)
4、本树的模板类的进一步封装体会
功能较多,为了使用更方便,于是在main函数中设计了一个菜单功能,但是,在这个设计中,不同类型的树,对于停止标记符号也不同,因为停止符号也设计成了模板T类符号,没有进行统一化,这样相关函数中输入才好设计,否则若设计了固定停止符号为‘#’,在一个int型的树中,我们万一有数据就是‘#’代表的ASCII码,就会停止,所以这样设计不是很好。而固定输入数据个数,万一个数打错了又要从新来过。最好还是设计一个停止标记,每种树设计一种对应类型的停止标记。而这样设计带来了一个问题,那就是菜单中是固定的调用,我们如何确认是哪种树呢?只是为了不同类型树使用不同标记而再次复制粘贴修改一两句代码显然是不合理的。
于是,我们将这个菜单功能也进行模板化,参数就是类型树和对应的停止标志,这样就合理了!
最终,我们的主函数只有寥寥几句
1 | int main() |
不同的树使用不同的模板类型的function即可,封装的很漂亮
5、switch case和{}
总结一句话:只要case中带变量不带括号,编译器都会报错,使用switch case的时候建议每个匹配选项后的执行区域推荐使用{}括起来,或者干脆不要将变量带入case中。
6、其他小坑
找合法序列时候使用单个读取而不是真的读取一个序列,导致有时一个序列中途就判断是错误的,那么后续的字符会被输入流读入菜单选项中,尽管使用了非正确功能序号不予相应的循环,但是若万一系列中多余的数据就符合菜单功能序号,就会出现非法调用功能的严重BUG,所以最终还是使用了动态数组存储然后删除
7、最终代码
1 |
|