用C语言写一个通用的链表、对列

前言

在工作中经常会使用到链表作为动态的缓存队列。麻烦的是,链表中的数据部分并不是一成不变的,有时候是一个基本类型(如int、float等);有时候是复杂的结构体。这导致每次都要重新修改数据的赋值操作甚至重新写一个链表,代码重用率很低。那么,为什么不写一个通用的链表来适应不同的数据类型呢?

定义一个通用的变量类型

我们知道,链表的节点包括数据域和指针域,如:

typedef int DATETYPE;
typedef struct node_list
{
	DATETYPE data;
    struct node_list *prev;//指向前一个节点
    struct node_list *next;//指向后一个节点
}NodeList;

指针域的操作在所有的链表中都是通过,但是数据域因为类型不同赋值的方法不同。所以我们需要把数据域的数据类型统一化
实际上,对一个变量赋值,就是对变量所在的地址写入相应的值。例如在下面的例子中,*p = 12a = 12效果是一样的。

int a;
int *p = &a;//获取a的地址
*p = 12;//给地址写入12
a = 12;

假设上面例子中int占用4个字节,当a变量定义后系统会分配4个字节的空间用来储存a的值,并且给出a的地址(&a)。给a赋值后,这个连续的地址上的值变成 0x00 0x00 0x00 0x0c(根据不同计算机字节序不同,赋值后顺序不同)。因此,只要知道变量的大小和首地址,我们就直接对地址操作,而不需要知道变量类型了。 所以,上面的结构体改为:

typedef struct node_list
{
	void *data;
    struct node_list *prev;//指向前一个节点
    struct node_list *next;//指向后一个节点
}NodeList;

这里定义void型指针来指向变量的地址。还有一个表示变量大小的变量,因为一个链表中的数据类型是相同的,所以变量大小不需要在每个节点中都定义,这里再定义整体的结构体用来表示整个链表:

typedef struct 
{
    NodeList *head;
    int data_len;//表示变量大小
}GenList;

head表示链表的头节点,指向整个链表的数据;data_len表示数据域变量的大小。

完成链表操作

1.初始化GenList

首先,我们根据项目需要,自定义一个数据类型,例如:

struct Type
{
    int id;
    char data[17];
    int num;
};

接下来是定义GenList变量list并初始化:将自定义的Type结构体大小(sizeof(struct Type))告诉list,并申请头节点,头节点用来接连链表的头和尾,链表空时,头和尾指针都指向自身。头结点不保存数据:

GenList list;
ret = gen_list_init(&list,sizeof(struct Type));
int gen_list_init(GenList *list, int dataLen)
{
    NodeList *head = (NodeList*)malloc(sizeof(NodeList));
    if(!head)
        return LIST_STA_MEM_ERR;//返回错误码
    list->head = head;
    list->head->prev = list->head->next = head;
    list->data_len = dataLen;
    return LIST_STA_OK;
}  

初始化后list变成:

在这里插入图片描述

2.向链表尾部添加一个节点

定义一个struct Type的变量并赋值,调用add_list_tail将该数据添加到链表中:

struct Type d = {12,"hello",43};
ret = add_list_tail(&list, &d);
int add_list_tail(GenList *list, void *data)
{
    NodeList *head,*p;
    head = list->head;//获得头节点
    p = head->prev;//p指向尾节点

    NodeList *new = (NodeList*)malloc(sizeof(NodeList));//申请一个新的节点
    if(new)
    {
        new->data = malloc(list->data_len);//为数据域申请空间,大小与自定义的类型相等
		if (!new->data)
		{
			free(new);
			return LIST_STA_MEM_ERR;
		}     
        memcpy(new->data, data, list->data_len);
        p->next = new;
        new->prev = p;
        new->next = head;
        head->prev = new;
		return LIST_STA_OK;
    }
	return LIST_STA_MEM_ERR;
}

链表节点的数据域我们定义的是一个void型指针,必须给它分配内存才能写入数据,分配的内存大小根据自定义的类型大小来决定,即:sizeof(struct Type),初始化时已经将sizeof(struct Type)赋值给了data_len,因此,申请了data_len个字节的内存。
调用add_list_tail函数时,参数data是指向变量d的首地址,从这个地址开始的连续data_len个地址上保存的就是变量d的值。所以,我们直接通过memcpy函数,把数据复制到我们申请的新节点数据域上。
根据上述代码,向链表尾部添加一个节点后,list变成:

2.从链表尾部读出节点数据

和其他的链表一样,我们可以很容易的读出一个节点。对于队列先进先出操作,一般是向尾部添加节点而从头部读出节点。例如下面从头部读出节点:

void* get_list_data_head(GenList list)
{
    return (list.head->next->data);
}

返回的数据是void*,需要转换成我们自定义的类型:

struct Type d = *(struct Type*)get_list_data_head(list);
printf("***id:%d,num:%d,data:%s\n",d.id,d.num,d.data);

3.删除链表尾部出节点

读出数据后,就可以删除该节点了,上面是从链表头读取的数据,所以删除头节点:

int del_list_tail(GenList *list)
{
    NodeList *head,*p;
    head = list->head;//获得头节点
    p = head->prev;//p指向尾节点
    if(p == head)
        return LIST_STA_LIST_NULL;
	if(p->data)
		free(p->data);//释放数据域申请的内存
    p->prev->next = p->next;
    p->next->prev = p->prev;
    p->prev = NULL;
    p->next = NULL;
    free(p);//释放节点申请的内存
    p = NULL;
    return LIST_STA_OK;
}

至此,一个链表的创建、添加、读取、删除已经完成了!
以下是完整的代码,包含库文件gen_list.c gen_list.h和示例代码test.c:gen_list

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

仲生plus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值