不带头节点链表--用面向对象的思想实现

此篇文章,陆续更新,完成一个不带头节点链表(用面向对象的思想)的编写。并且,通过这个练习,可以加深我们对实参和形参的关系,真正搞懂它。

内容:不带头节点链表完成屏幕点坐标管理。

屏幕点坐标: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

猜你喜欢

转载自blog.csdn.net/weixin_38214171/article/details/80218873