/ 前言 /
在之前的文章分享 Linux 内核源码实现的循环双链表也是一种泛型的思想(不了解的请戳这里:Linux内核源码剖析(一)--不同寻常的双向链表),利用用户自定义的结构体包含 Linux 内核双链表节点,通过结构体元素偏移找到用户结构体起始位置,实现一种“泛型”链表。不过今天小 C 分享的是另外一种实现思路,请往下看~
/ 设计 /
主要依据两个点来设计实现的:指定元素内存,通过宏定义推迟元素定义。
1.指定元素内存
常规的链表定义如下,定义一个节点指向下一个节点,其他就是元素,这种结构体在内存上的布局如下图所示:
struct list
{
struct list *next;
int age;
int sex;
float height;
char name[32];
};
list结构体内存分布图
看了上图结构体元素在内存中的分布后,我们将结构体拆为如下两部分,但是申请内存时候还是一起申请,如下图所示。这样我们可以只定义一个 list 链表结构体,human 结构体由用户定义,想包含什么数据就包含什么数据。但是这样有一个问题:我们我们创建节点时候要把链表指针和元素的内存都申请了,元素是用户自定义的,而我们不知道元素有多大?请往下看。
struct list
{
struct list *next;
}
struct human
{
int age;
int sex;
float height;
char name[32];
};
将两个结构体的内存同时申请,创建的内存连接在一起。
2.通过宏定义推迟元素定义
前面我们通过将一个常规的结构体给拆开,实现用户自定义链表中的元素,降低了耦合,也迎来了一个问题:那就是用户自定义的结构体有多大,这关乎我们创建链表节点时候申请内存的大小。怎么解决?下面这样定义?
struct list* create_node()
{
struct list* node = (struct list*)malloc(sizeof(struct list) + sizeof(struct human));
return node;
}
这样定义创建节点,的确可以解决,但是如果用户不想让链表的元素名字叫 struct human 咋办?
答案:采用宏定义的方式推迟了对自定义链表元素名字的要求,如下所示。这个 type 是用户自定义链表节点元素的名字,例如 struct human,也可以定义其他的名字,完全看个人意愿。同理,其他的链表操作也采用宏定义的方式,这样还减少了函数调用的栈开销,一定程度上提升了性能。
#define slist_new_node(_type) (struct list*)malloc(sizeof(struct list)+sizeof(_type))
/ 实现 /
C 语言泛型单链表实现源码如下:
#ifndef __SLIST_H__
#define __SLIST_H__
#include <stdio.h>
#include <stdlib.h>
/**
* @brief 实现C语言泛型单链表
* @author young 【微信公众号: Linux编程用C】
* @mail estyoung71@gmail.com
*/
/**
* @brief 定义链表节点
*/
typedef struct slist_s
{
struct slist_s *next;
}slist_t;
/**
* @brief 创建一个链表节点
* @param _type 链表中元素的类型
*/
#define slist_new_node(_type) (slist_t*)malloc(sizeof(slist_t)+sizeof(_type))
/**
* @brief 传入链表节点,获取节点的元素
* @param _node 传入的链表节点指针
* @param _type 链表元素的类型
* @return 返回节点元素的指针
*/
#define slist_get_elem(_node, _type) ( (_type*)(((char*)_node)+sizeof(slist_t)) )
/**
* @brief 在指定节点位置前插入一个节点
* @param _pos 节点位置指针
* @param _node 要插入的节点指针
*/
#define slist_insert_front(_pos, _node) ({ _node->next = _pos; _pos = _node; })
/**
* @brief 在指定节点位置后插入一个节点
* @param _pos 节点位置指针
* @param _node 要插入的节点指针
*/
#define slist_insert_back(_pos, _node) ({ _node->next = _pos->next; _pos->next = _node; })
/**
* @brief 删除链表节点
* @param _prev 要删除的前一个节点
*/
#define slist_erase_node(_prev) ({ slist_t* tmp = _prev->next; _prev->next = tmp->next; tmp; })
/**
* @brief 删除节点后重新复位
* @param _prev 被删除节点前一个节点指针
* @param _node 要删除节点的指针
*/
#define slist_erase_reset(_prev, _node) ({ _node = _prev; })
/**
* @brief 遍历链表
* @param head 链表头节点
* @param node 当前节点指针
*/
#define slist_foreach(head, node) \
for(node=head; node; node=node->next)
/**
* @brief 遍历并删除节点
* @param head 链表头节点
* @param prev 保存节点的前一个节点
* @param node 当前节点
*/
#define slist_foreach_safe(head, prev, node) \
for(prev=head, node=prev->next; node; prev=node, node=node->next)
#endif /* end slist */
/ 验证 /
编写测试程序,测试单链表:struct info_t这个结构体是用户自定义的,测试创建、插入、遍历、删除等功能。
#include <string.h>
#include "slist.h"
typedef struct info_t
{
int age;
char name[32];
}info_t;
int main(void)
{
slist_t *node, *tmp, *prev, *head = slist_new_node(info_t);
slist_get_elem(head, info_t)->age = -1;
snprintf(slist_get_elem(head, info_t)->name, 32, "tom");
for(int i=0; i<10; ++i){
node = slist_new_node(info_t); //创建节点
slist_get_elem(node, info_t)->age = 10+i; //给元素赋值
snprintf(slist_get_elem(node, info_t)->name, 32, "tom%d", i);
slist_insert_back(head, node); //插入到头节点后面
}
printf("----------split-------------\n");
slist_foreach(head, node){ //遍历
printf("name=%s age=%d\n", slist_get_elem(node, info_t)->name, slist_get_elem(node, info_t)->age);
}
printf("----------split-------------\n");
slist_foreach_safe(head, prev, node){ //遍历删除
if(slist_get_elem(node, info_t)->age<15){
tmp = slist_erase_node(prev); //删除
free(tmp); //释放内存
tmp = NULL;
slist_erase_reset(prev, node); //连续删除需要给node赋值
}
}
printf("----------split-------------\n");
slist_foreach(head, node){ //删除后在遍历打印
printf("name=%s age=%d\n", slist_get_elem(node, info_t)->name, slist_get_elem(node, info_t)->age);
}
return 0;
}
测试效果:插入10个元素,遍历打印,删除5个,遍历打印。
欢迎大家一起交流讨论,如果本篇文章对你有用的,请点赞、再看、转发吧~~,蟹蟹你~。
我把代码传入GitHub:https://github.com/young-1-code/data_structure.git,在上面也有完整的slits代码和测试代码,取代码时候帮忙点个star吧~,蟹蟹你~。