穷究链表(三)
今天,终于可以开始进行链表的定义了。
这里所讲的是关于C语言下的情况,所以首先我们所有的程序都要使用.C而不是.CPP作为后缀。在以cpp作为后缀的文件中进行编译,很多语法是不一样的。下面会在必要的地方提到一些区别。
另外还要注意的就是,一般在工程中,是分为头文件和源文件的,也就是.c文件和.h文件(因为一开始是使用C来实现,所以严格一些,还是说.c文件,不过不清楚的就是VS2005是否完全符合C99,还是符合哪个,所有都是在VS2005下面来做),这里一开始实现就尽量放在源文件中进行实现,也就是linkedlist.c中实现。在工程项目中,还是需要将头文件和源文件分清楚。头文件相当于接口的性质,同时在移植性的项目中,由于头文件的依赖性等问题,还会引出很多的问题,也会引出很多无奈的解决方案的产生(当然,有很多也是很天才的想法)。
也因为头文件带来了这么多的问题,所以在后面的JAVA和.NET中,就没有出现头文件的概念了。而C和C++相同的编译机制,使得其还是需要头文件来进行链接工作(对于这点,我很清楚的部分是头文件在链接中是需要的,否则只有定义没有声明的变量和函数就会找不到,obj文件之间通过头文件来互相索引,但是具体链接器的原理,我并没有研究过,所以更深入的部分就不太清楚了)。所以,我猜想,这也是C中还是继续使用头文件的原因。
下面的实现都放在源文件中
一开始当然是链表节点的定义了。
这里就开始考虑一些问题。
从表示方式来思考,我们使用结构体来做链表节点的表示。其中数据为int类型,我使用的是32位机器,IA-32架构,WinXP系统。其中操作系统的影响不大,如果是使用unicode相关的,则操作系统会有一些影响,98和NT架构内部对于ANSI程序和UNICODE程序的处理是不太一样的。同时,还包含有一个指向链表节点的指针,一般会命名为next,表明是指向下一个链表节点。
那这里的问题就是C语言中结构体定义的问题,同时再加上一个typedef的使用问题。我们如何用C语言来将我们需要表示的链表节点表示出来。
粗一看,很简单,不就是定义一个结构体吗,但是真正实现的时候,你会发现还是很有思考的余地的。就像吃东西一样,还是很有嚼头的。让我们慢慢往下看。
下面我们定义了一个链表节点结构体(片段1)
2{
3 int data;
4 struct listnode *next;
5};
6
这是最简单的链表节点的定义了。
你可能看到过这样的写法:(片段2--此片段在C中会出现错误)
2{
3 int data;
4 listnode *next;
5};
6
这样的写法在cpp中是可以编译通过的。但是在c中,你要使用结构体的自定义类型时,需要在前面加上struct关键词才可以。这就是c和cpp之间微妙的区别。所以,如果你在将cpp代码放入c中,编译不通过时,可以看一下,是否是你的struct关键词没有写上。尽管不了解这个,你也可以写出运行正确的代码来,多了解一些并不是坏处,不是吗?
对语言细节的掌握,可以让你更深刻的了解为什么要这么去编写这些代码。
另外,对于片段1的编写,可以注意的地方有:
1. struct的最后要使用分号来进行结尾,否则编译器无法解析,会报出让人无语的错误。同样,class的最后也是一样。
2. 你可以定义一个匿名的结构体,也就是在struct之后不用加上任何名字,但是在定义链表节点的时候不能这样做,为什么?因为在之后你需要使用到这个结构体定义,你需要定义一个指向这个结构体的指针next。
3. 这两个成员的访问权限,结构体的默认访问权限为public。
对于第二点,其实细想起来还是有些诡异的。因为你这个结构体还没有完全定义好,就又在使用了,而且是在其本身内部进行的使用。其实这里面使用的只是一个指针,而用的是使用前向声明,让编译器知道有这样的一个结构而已,具体这个结构的内容在此时并不需要知道。如果这里我们不用指针,而是用结构体本身,那就会出现错误。
在对于最简单的结构体了解清楚上面提到的问题后,我们接着往下看。
我们还可以这样定义:(片段3)
2{
3 int data;
4 struct listnode *next;
5}node;
6
可以看到,在最尾巴的地方,在分号之前增加了一个node。
说明:
C中有类型和变量之分。listnode就是定义出来的类型,而node就是listnode类型的变量。呵呵,比较绕口是吧,但是就是这样的。简单的说,listnode为类型,而且是用户(也就是我)自定义出来的新类型,node是一个变量。这里就定义了一个名为node的全局变量。
下面开始来编写实验代码,这里要注意的是如果你使用微软的Visual Studio系列,那试验时需要将文件的后缀名修改为.c,而不是.cpp,如果你使用.cpp后缀名是得不到上面的结果的。
完整的试验代码(代码1):
很久不写C代码,即使写,也是在cpp中写的伪c,感觉不太适应了。比如在创建链表节点的时候,想用new,编译不通过才想起应该用malloc,看着malloc不太爽而使用了局部变量(还是new好看些)。而在给链表节点赋值的时候,又想起使用构造函数,但是尽管是struct,也是在C++中才可以使用构造函数的。
另外还有一点,就是对next赋值的时候,使用的是0,而不是NULL。在标准中,NULL是不存在的(后来经过查证,NULL是存在的,只要include <iostream>即可,真正定义NULL的地方为stdio.h,此为VS2005的实现,其他编译器实现不清楚)。
指针,最标准的NULL为(void *)0;
注意到这里有一个函数f,因为C中只有全局函数,所以在实现库的时候,也是使用多个全局函数和结构体的结合来完成需要的功能的。这样其实对于数据本身的保护和管理就不是很好,而C++以及之后的抽象数据类型提供了封装性的特性,也是为了更好的实现数据和操作的整合。
稍微仔细看代码的同学就可以发现,此时,在上面代码中使用的node有两个,一个为全局变量,一个为main函数中的局部变量,其作用域范围是不同的。
局部变量其内存分配是在栈上面的,而全局变量是分配在全局区。这里就涉及到PE文件的格式,具体关于格式的问题,没有特别的深究过,所以这里也就是春秋笔法,简略而过。记得大概有.TEXT, .BSS, .DATA等段。以及代码段,有兴趣的同学可以继续研究下去,可以去pediy看看里面的内容。
关于变量,除了全局变量和局部变量外,还有一个修饰符的问题。
C中变量有auto, register, static, extern,另外的修饰符还有const。
auto变量是局部变量默认不加任何修饰符,默认就是auto变量,auto变量的作用域范围就是从声明的地方开始,到特定作用域的范围结束。(这里C和C++大概有些不同,C一般都是在函数开头的地方进行声明,然后使用;而C++是在需要使用的时候进行声明,比如在某个循环中,或者某个判断中,然后出了循环或者判断,这个变量的作用域也就结束了)。
register变量是存在寄存器中的。
static变量可以用来修饰函数和变量,其作用域范围是整个程序的生命周期。而对于文件来说,其作用范围是该文件,其他文件中是看不到该变量的。
extern用于函数和变量,是用来声明它们的。
最后来看一个代码,看看大家是否能够发现其中的问题,是在实验const的时候出现的编译错误,但是和const本身无关,害我找了好久。(代码2)
编译器报错为:
------ Build started: Project: linkedlist, Configuration: Debug Win32 ------
Compiling...
list.c
error C2143: syntax error : missing ';' before 'const'
linkedlist - 1 error(s), 0 warning(s)
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========
其实原因为,C中需要在函数开头的地方进行声明,然后使用。const int a是声明,所以要放在赋值语句的前面。这也是C和C++使用上的区别。在前面提到auto变量的时候也说过了。这个如果写惯了C++或者其他JAVA等语言的代码后,就常常会忘记。而编译时报出来的错也很难让你了解具体的问题。
下面来看最常用的形式(片段4)
2{
3 int data;
4 struct listnode *next;
5}node;
6
这里使用typedef,将结构体来定义为一个类型。此时尽管名字还是node,但是它已经不是一个变量,而是一个类型了。
我们可以这样使用:(代码3)
2
3typedef struct listnode
4{
5 int data;
6 struct listnode *next;
7}node;
8
9int main()
10{
11 node n;
12 n.data=5;
13 n.next=0;
14
15 return 0;
16}
17
此时n为局部变量。
而这里我们注意到,在main函数中定义变量n的时候,我们没有使用struct node,而是直接使用node来定义(事实上使用struct node反而会编译错误),这样十分简洁,同时与我们使用系统自带类型的使用方式就一样了。这就是我们为什么会在比较工整的C程序中看到typedef的原因了。一般我们看到最多的就是这种形式的结构体定义。
类型定义中node看起来会引起一些误解,所以将其名字修改一下,最后定型为:(片段5)
2{
3 int data;
4 struct listnodeT *next;
5}listnode;
6
短短的5行代码,花了这么多内容来进行介绍,由此可知,要写一个完整的链表程序是多么的困难。而要用C++来完成这个程序又是难上加难。而使用对模板支持极差的VC6来完成C++的模板程序更是Mission Impossible。(不知道有多少同学用VC6实现过,反正我是很久之后才能搞定的)。
所以,不管是教,还是学习数据结构的时候,请务必确定你的IDE环境工具,以及对该工具本身特性的掌握,同时对数据结构本身实现语言的掌握,如用C来实现就是对C的掌握,用JAVA就是对JAVA的掌握,算法可以使用伪码,但是数据结构大概还是需要用代码来实现一番。
这里的内容还有许多可以扩展的地方,不过一是篇幅的关系,二是重点还是围绕链表本身,所以感兴趣的同学大家可以一起研究研究,这里就不细写了。
好了,闲话少说,链表节点结束,下一篇开始进入链表的实现。