此篇文章,陆续更新,完成一个不带头节点链表(用面向对象的思想)的编写。并且,通过这个练习,可以加深我们对实参和形参的关系,真正搞懂它。
内容:不带头节点链表完成屏幕点坐标管理。
屏幕点坐标:POINT,这里只考察行,列坐标值;屏幕在编程时有两种不同的状态:
图像状态和文本状态,我们这里只考虑文本状态。
文本状态下,行号从1到25,列号从1到80;屏幕左上角为“原点”,向右、向下分别是列和行的增量方向;
此次编程可实现基本功能:新点插入;指定点的删除;点的显示;按行升序排列等操作;
typedef struct POINT {
int row;
int col;
struct POINT *next;
}POINT;
如果按我们书本上学的基础进行思考,我想我们都会把结构的定义成这样,增加一个“链域”,并且以这样的结构体作为链表中的一个节点,就可以完成链表的处理;但是,这种做法有很大的弊病:对于不同的数据域,所编写的代码根本无法进行“复用”,如果进行这样的编程,我们总是在重复,这也不适合用在将来的工作上,也不符合软件工程的基本思想。“面向对象”思想一直强调“数据封装,代码复用”,所有,编写出有意义的代码才是最重要的,而不只是应付考试。那么,如何实现“无论数据域的具体形式”的链表,可以采用这个思想:
将数据域“模块化”,强调链域,这可以使得有关链表的绝大部分操作可以实现(大部分的链表操作都与数据域没有关系)。其实,这种思想我们既陌生又熟悉,那就是malloc()函数的思想,我们知道,malloc()函数的返回值是void *,即无类型指针,根据需要可以指定。例如:
int *p;
p = (int *) malloc(sizeof(int));
所以,我们可以把结构体定义成这样:
typedef struct NODE {
void *data;
struct NODE *next;
}NODE;
我们的目的不是简单的实现屏幕点坐标的管理,而是给出一个具有通用性质的“链表工具”,但凡是链表本身的操作,都不应该让屏幕点管理程序去处理,而是,调用这个我们编写好的工具函数即可!此外,在进行编写之前,进行一个知识的扩充(联合编译,Makefile)。
在C语言编程中,通常用一个.c和一个配套的.h文件对应的方式进行开发(这是我们在大一的学习中没有接触到的).
.h文件编写时有基本的技术要求,只能出现以下三种内容:
1、宏定义;
2、用户自定义类型;
3、函数声明;
不能在.h文件中出现全局变量;最好也不要出现函数定义。这是我对mecLink.h文件的编写(主要看格式)
#ifndef _MEC_LINK_H_
#define _MEC_LINK_H_
typedef struct LINK {
void *data;
struct LINK *next;
}LINK;
void appendNode(LINK **linkHead, void *data);
#endif
另外,在这里,也会涉及多.c文件联合编译的技术,如果不会,可以参考我的博客--Makefile编写的简单总结。
好,以下我们开始这个链表的分析。
typedef strucrt LINK {
int row;
int col;
struct LINK *next;
}LINK;
以这个结构体为链表节点具体的存储形式,若需要在链表末尾追加一个节点,基本过程是:
1、先申请一个节点:
Link *p = NULL;
p = (LINK *) malloc(sizeof(LINK));
上述操作的本质上可以理解为:定义一个变量。这个变量需要通过指针p进行操作;
p->row、p->col是具体要存储的数据;
p->next是对节点链接方式的控制;
2、找到链表的末尾:
从第一个节点开始遍历,直到末节点;
对于空链,一开始循环就应该结束;对于非空链,以末节点“标志特征”作为循环条件;
LINK *q;
for(q = 第一个节点的首地址; NULL != q && NULL != q->next; q = q->next)
;
3、将新节点连接到末尾:
if() {
更改链表头指针的值,使其为p的值;
} else {
q->next = p;
}
上述LINK结构体中,存在int row和int col;这种存储方式简单,操作也较简单,但是,也有一个很大的缺点:
1、所形成的链表、包括所有代码,都只能处理row和col;如果有另外的数据需要相同链表的处理方式,则需要完全重新编写代码。
2、这种方式其实是将两件事合并起来了:1)链表;2)屏幕点信息;
如果把这两件事彻底分开,两个结构体各司其职,就需要高度抽象的解决问题,是有一定难度的。
链表只管链表的操作,应用数据(row,col)只管应用数据的事,所有定义的结构体如下:
typedef struct LINK {
void *data;
struct LINK *next;
}LINK ;
typedef struct POINT {
int row;
int col;
}POINT;
接下来就是各个函数的分析了。
1、添加节点:
void appendNode(LINK **linkHead, void *data) {
if(NULL == linkHead || NULL != *linkHead) {
return;
}
LINK *node = NULL;
node = (LINK *) malloc(sizeof(LINK));
node->data = data;
node->next = NULL;
if(NULL == *linkHead) {
*linkHead = node;
return;
}
LINK *lastNode;
for(lastNode = *headPoint; lastNode && lastNode->next; lastNode = lastNode->next) {
;
}
lastNode->next = node;
}
首先,需要判断传入的linkHead参数是否为空和其指向的空间是否是非空的(非空,则表示其指向的空间是垃圾数据),如果为这两种情况,则直接什么都不做,return即可。接下来,就可以申请一个节点,添加节点肯定是要把新申请的节点添加到链表的末尾。如果传入的链表是一个空链,则把首先申请的这个节点作为它的表头,就退出,进行第二次的添加工作。除了这种特殊情况,其余的都需要把节点添加到链表末尾,那么,就需要找到链表的末尾,所以,定义一个链表尾指针,用for循环一直找,直到找到链表的尾(结束标志是lastNode为空),找到链表的尾部,就可以把新申请的节点连接到后面了,这样,添加节点的工作也就完成了。
2、插入节点:
关于插入节点,其实说的很泛泛,信息量很少。到底是插入data还是LINK的node,所以我们的插入函数的参数应该有三个:
1、链表头指针首地址;因为,有可能出现将节点插入到整个链表的第一个节点的前面,即,要更改头指针的指向关系。
2、要插入的用户数据;用户应该提供的是void *data;函数应该申请LINK的一个node,再完成插入操作;
3、指定位置:要先找到插入的位置,这个是插入的一个很大的难点,因为很难确定指定位置。
1)、下标方式;
2)用户提供“指定”数据,提供的数据有两种可能性:
1、提供的是链表的某个节点的数据域所指向的数据块的首地址;
2、用户不知道指定数据的数据块首地址,但是,知道其值,但是如果提供知道的这个值,也是没用的,因为,这个值的首地址肯定是和链表中要找的那个位置的地址是不同的!
因此,有一个解决方案:将“定位”问题从“插入”操作中分离出去,在专门定义一个“查找”的函数,这个函数能返回指定节点的“下标”。但是,只定位是没有用的,我们可以这样做:
1、实现一个定位函数,该函数能够找到指定点的“下标”,这个函数给用户看;
2、实现一个根据下标,找到指定节点的首地址的函数,这个函数是工具内部使用的,不给用户看。
所以,为了实现这一功能,应使用C语言一个特别的技术:指向函数的指针!
以下是上传到GitHub上的代码:
https://github.com/yangchaoy259189888/MEC_Link.git