穷究链表(十一)

前面我们已经实现了链表的C语言版本,并看了现实中的实现,现在来实现C++版本。C++的话,就有类,有构造函数,析构函数。但是这里实现链表不需要使用到继承,没有继承也就没有多态,只是封装。

列出参考书籍和网址:

参考《C++标准程序库》,由侯捷、孟岩翻译,在166页开始讲解list的部分。

侯捷的《STL源码分析》

SGI STL网站

http://www.sgi.com/tech/stl/download.html

STL资源介绍

http://www.stlchina.org/twiki/bin/view.pl/Main/STLLearnResouce

BOOST源码

http://www.boost.org/

对于boost的说明

http://www.stlchina.org/twiki/bin/view.pl/Main/BoostEnterBoost

 

下面就来创建一个list.cpp文件,创建一个链表节点类

Code

 

可以看到,listnode类有两个成员变量,一个构造函数,由于其本身并没有创建内存,所以不需要自己定义一个析构函数。

 

同时,该构造函数使用了默认参数,这里要注意的就有下面几点:

1.      如果有默认参数,则该默认参数之后所有的参数都必须定义有默认参数

如果将上面的listnode(int d=0, listnode* next=NULL) 定义为

listnode(int d=0, listnode* next) ,则会编译出问题。

 

2.      关于NULL的定义问题

可以看这两篇讨论

http://topic.csdn.net/t/20060328/10/4644706.html

http://topic.csdn.net/t/20030401/21/1606139.html

还有一篇blog

http://bigwhite.blogbus.com/logs/4738221.html

 

由此可见,在C++C中,NULL的含义是不同的,同时void也是不同含义的。

C++中,如果NULL本身没有被系统占用作为保留字的话,则按照TCPL的建议,使用const int NULL=0;

来对NULL进行定义。

 

对于0,还可以具体看它的其他含义,除了数值意义的0,它还有很多其他的含义。

另外,对于浮点数的与0比较,相信大家都看过,但是具体写过这样的比较没有?在VS2005中试验一下看看呢?

 

其实浮点数是比较奇特的,对于floatdouble,曾经解过浮点数越界的bug,但是何时越界,如何做到精确,却又是一个问题。尽管将bug解了,但是对于浮点数,还是没有遇到说讲解得特别清楚的,应该是有,但是没有看到。这里算是一个遗留问题。大家有兴趣可以讨论讨论看,我也学习学习。

 

对于浮点数与0进行比较,在VS2005中试验是进入不了if,不知道是否是我做的方法有问题,大家可以自己试一下。毕竟使用浮点数的机会并不是那么多。当然,真正要用到的时候,就是要用很多遍了。

 

PS:在加入

#include <iostream>

using namespace std;

后,NULL就已经被定义,不需要另外定义了。

真正的定义部分是在stdio.h中

1/* Define NULL pointer value */
2#ifndef NULL
3#ifdef __cplusplus
4#define NULL    0
5#else
6#define NULL    ((void *)0)
7#endif
8#endif
9

 

3. 这里还可以使用成员初始化列表来完成,而且推荐是使用成员初始化列表来做。

Code

 

这里是测试两个next在成员初始化列表中进行赋值,经过测试可以。(当然,为了方便,在测试时将两个成员变量修改为了pubic

4.      this指针为C++中指向该对象的指针,也是C++成员函数中默认自带的一个隐含参数。

在实现中,如果成员变量的名字和参数的名字相同,那调用的时候,就不清楚具体是调用哪个,所以加上this修饰符来代表成员变量。

 

因为两个next还是会误解(在成员初始化列表中),所以修改为下面的写法

 1class listnode
 2{
 3public:
 4    int data;
 5    listnode *next;
 6
 7public:
 8    listnode(int d=0, listnode* n=NULL):data(d),next(n) 
 9    {
10    }

11}
;
12

 

【关于inline函数】

inlineC++中加入的新特性,这里的构造函数就是用的inline形式,inline和宏的区别是:inline是在编译期而不是预编译期进行替换的,inline的好处是能够减少掉函数的调用,而比宏好用的地方是在调试的时候可以看到变量的内容。

具体是否对inline函数进行真正的inline操作,要看编译器的决定,主要的决定因素是是否耗时,如果耗时比较少,则可以进行inline;否则不进行inline

所以,一些计算复杂,或者进行递归操作的函数,一般不要进行inline

 

inline可以有两种,一种就是像上面一样,将函数写在类的声明里面,还有一种是在函数前面加上inline修饰符,然后不管在类中实现还是在类的外部实现,都可以。

 

减少函数调用的开销,函数调用时一般会有怎样的开销呢?

首先会保存各寄存器的值,IP,ESP

然后将各参数进行压栈

建立新的寄存器值

在其中,可能会将某些寄存器的中间值等临时放入内存进行保存等工作。

 

在完成函数调用后,还需要

如果有返回值,一般保存在某个寄存器中(EAX),需要进行一系列的处理工作。

恢复函数调用开始时保存的寄存器的值。

 

不过inline和宏一样,都会造成代码膨胀。

http://game.lihuasoft.net/showart.php?id=4954

 

定义好linknode之后,定义linkedlist

Code

 

由于linkedlist需要使用到linknode中的private变量,所以要将其声明为linknode的友元。

 

此时,就有一个位置的问题了。

linkedlist中使用了listnode,所以将listnode的类声明放在类linkedlist前面,这个一开始做的时候就没有问题。

但是,现在linknode中要声明linkedlist为其友元。所以在linknode之前,就必须知道有一个类linkedlist(终于,拗口的东西又来了,放心,只要你用C++,总是会碰到一些拗口的东西的,PS: 不知道JAVA这些语言怎么样)。

 

最近看JAVAJAVA中一般有匿名类,内部类(inner class)等,JAVA尽管对C++进行了简化,使得开发效率得到了提高,但是现在也是挺庞大的,而且又将模板加入了,不过对设计模式的大量使用,以及大量的库和开源框架,JAVA还是不错的工程语言。

 

这样,我们就需要有一个前置声明。(的确,这样不是很美,但是这就是这样的solution)。我们还可以使用嵌套类来实现得好一些,但是嵌套类在使用模板的时候,VS会有一些问题,这个,慢慢来看吧,做好长期抗战的准备。(经过试验,惊喜的发现现在的VS对于模板的支持是越来越好了,不过以前使用的时候出现过一个什么问题造成编译器内部错误来着的,但是现在记不起来了,一般的使用似乎现在支持不错了,很好)

 

此时,完整的实现如下

Code

 

可以注意到,其中linkedlist的所有函数,我们都没有实现,但是,编译和链接是没有问题的。这就说明,只要不调用这些函数,那链接就没有问题,函数定义了但是不实现是允许的。

 

插播:

不知道大家记不记得,如何阻止一个类产生自己的对象。

答案就是:定义一个private的构造函数,这样说不准确,应该是将所有的构造函数定义为private的,同时并不实现它,这样,在其他函数中调用,不管是编译还是链接,都会出现问题。而这样,是允许的。并不是你想象中的,定义了一个函数就必须实现它,你可以为了某些目的,而不去实现,这个程序也会照样成功运行。只要你不调用该函数。

 

实现的函数:

实现了默认构造函数

拷贝构造函数

析构函数

以及赋值函数

 

拷贝构造函数与赋值函数的不同:

赋值函数要判断一下是否是要赋值给本身。

 

这里实现赋值函数的时候,遇到一些疑问,怎么判断两个链表是同一条链表?这里的实现其实是不完整的,有问题的。不过先写在这里,没有错误的,怎么会有正确的呢,希望大家能够在上面修改,得到正确的版本。

 

Code

 

其中比较难实现的,Destroy函数,赋值函数,addNodedeleteNode中要注意size的变化。

其中mergelist有些问题,在merge之后,需要调用析构的时候,被merge上的那个链表要被析构两遍,这样会有问题。

 

得到的实现代码如下:

 

Code

 

一共有285行,而.c文件只有195行,(当然其中还有一些注释行,不过肯定要比C文件要长)由此可见,cpp文件比c文件要多书写一些东西。不过cpp在处理大型的系统时,比c要能够降低一些复杂性和冗余,多一些重用。

 

下面的问题就是,验证正确性。

不管代码如何优美,代码是用来实现功能的。如果一个代码写得很好,但是考虑得时候忘掉一些逻辑,产生了一些bug,那么这个代码就是不合格的。而如果一个代码写得马马虎虎,但是其功能实现得很好,那么它就是合格的。(当然,如果需要维护的话,那就需要可读性好的代码,以及注释清晰的代码,不过,who cares,先将这些工作完成,再来考虑这些有的没的吧,其实维护一般都不是自己来做,不是吗)

 

所以首先我们要保证代码是合格的,是针对任何输入数据,都可以正确处理的,然后我们才来考虑怎么实现比较好,比较漂亮,甚至怎么优化的问题。

 

这应该属于单元测试范畴的东西。

JUNIT出来的时候,我感觉很羡慕呀。JAVA有这么好的单元测试工具,而之后.NET也有了NUNIT,尽管CLRC++可以使用NUNIT,但是纯的C++代码,做单元测试还是很麻烦。不是应用程序级,而还是代码级。尽管因为JAVA它们有元数据的优势,可以通过反射来得到很多东西,不过C++的单元测试的复杂度一下子落后这么多,还是不爽之。

 

对于C++的单元测试,实在是能写的不多。找了一些库,不过也是自己平时稍微用一下,项目中肯定是不能用,而且经过自己试验,还是不太好用,或者是我自己不太会用。

我了解的有apache组织编写的cxxtest,还有cppunit,以及BOOST有一个test framework,还有一个visual unit,但是似乎需要收费,没有使用过。

然后Rationalrobot不太清楚好不好。另外还有一些检查memory leak的工具。

在搜索相关信息的时候,发现还有一个googletest的东西,不清楚具体的内容。

 

当然,上面列举的都是项目级的单元测试框架,如果你仅仅是做一些简单的测试,那就只需要在main函数中加入你需要测试的函数,然后填入准备好的数据,再来查看对应的输出是否符合自己的要求即可。

 

或者,你可以准备好输入文件和输出文件,然后像一些Online Judge系统一样来操作也一样可以,不过这样就复杂一些了。

 

测试并不是一样简单的事情,也许一部分的手工测试是这样,但是测试的范畴更大。它可能会涉及到系统,编译器,函数本身,UI,文件,状态变化,数据库,网络等各个范畴,如何将它们有机的结合,来得到正确/错误的结果(其实测试本身的目的就是证伪,只有证伪了才说明它是好的测试用例)。而如何更快的得到测试结果,则是另外一个目的。同时,还有就是更好的进行回归测试,这些应该是比较长久的课题。

 

http://www.yeeyan.com/articles/view/13124/50377

 

http://dreamhead.blogbus.com/logs/8315523.html

 

http://www.cppblog.com/shuiyuan2004/archive/2007/08/29/31105.html

 

从我的理解上来看,单元测试应该是属于白盒测试。同时,对于单元测试由程序员来做,这个我觉得是必须由程序员来做,但是并不是只是一句由程序员来做就完结的。从测试的角度来看,做单元测试的思路和程序开发的思路是不一样的,会开发并不一定就会做单元测试。做程序是一项细致的工作,如果每个都能用一句话来概括掉的话,那大家就肯定可以把这个工作糊掉,因为你的领导肯定什么都不懂。

 

所以呢,在这里,我就随便挑了一些数据测试了一下,至少这些数据没有出问题。

又所以呢,如果我的程序里面有问题,那就是开发的时候没想到,也是测试的时候没测好。

 

在实现的时候,发现deleteNode的实现还是有问题。需要进行修改。

当处理超出范围的pos时有问题。

 

修改之后的:

Code

 

本来这篇是可以分成好几篇来写的,不过为了连贯性,就写在一起了,篇幅可能长了些,如果有耐心的,看到这里也一样,没有耐心的,反正也看不完这里,下一篇就是用模板来实现,包括友元类和嵌套类。

posted on 2009-10-21 22:53  cnyao  阅读(373)  评论(0编辑  收藏  举报