C语言的通用链表

时间:2022-08-11 03:55:12

在操作系统编程中, 往往是使用C语言, 但C使用起来极为痛苦, 不像C++有方便的STL模板库使用。

linux内核中,有一套非常神奇的通用链表结构,能够方便的使用,管理各种类型的数据,我们今天就来研究一下,内核中的C数据结构。

本文参考:【深入分析 Linux 内核链表】

首先,我们的目标是构建一个循环双链表结构,为何是双链表,还要循环,当然是从易用性考虑了,双链表能够方便的得知自己的上一个元素,在内核中管理数据更为方便。

其一般结构大概是这样:
C语言的通用链表

实现机制

首先定义一个list_node结构,用来保存链表节点信息:

    /**
* @brief 链表节点结构
*/

typedef struct _list_node
{
struct _list_node *next;
struct _list_node *prev;
} list_node;

然后用一个存放数据的结构,比如我们要用int型的链表,如下定义:

    /**
* @brief 数据存放结构
*/

typedef struct _Int_List
{
list_node node;
int data;
} Int_List;

这和我们传统的链表实现思路好像不大一样,我们经典的C语言链表当然是在链表中保存数据:

    /**
* @brief 链表节点结构
*/

typedef ElementType int;
typedef struct _list_node
{
struct _list_node *next;
struct _list_node *prev;
ElementType data;
} list_node;

但这样的实现必然会造成链表结构被反复定义,难以实现泛型。另外一种思路可能是将ElementType实现成void*指针,然后在使用时进行强制类型转换,但这样必然会带来反复的类型转换,而且其使用相对不便。

这时要出问题了,如果数据在外层,链表在里层,那么如何从链表找到数据呢?

offsetof宏和container_of宏

为了实现从内层元素找外层元素的功能,我们需要用到这两个系统宏,一眼看上去很复杂,但实际上并不难理解。

/**
* @brief 确定当前成员变量,在结构体中偏移量的宏
*/

#ifndef offsetof
#define offsetof(type, member) ((size_t) &((type*)0)->member)
#endif
/**
* @brief 根据成员变量,找包含他的结构体指针
*/

#ifndef container_of
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
#endif

这两个宏较为复杂,首先是offsetof宏,这个宏用0号指针去寻找成员变量,我们试想,如何是普通的一个结构体,C编译器如何查找其一个成员呢?
例如:

    /**
* @brief 数据存放结构
*/

typedef struct _Int_List
{
list_node node;
int data;
} Int_List;

假设我们用Int_List A语句,实例化一个结构体,然后取到了A的地址0x00001000
那么node的起始地址是不是也是0x00001000
data的起始地址是不是0x00001000+data的偏移量?

那么如何我想求data的偏移量,不就是如下的代码么:

    (&A)->data - (&A);

如果A的地址为0时,那么也就不用减了,就是我们宏定义中的用法。

而container_of宏也是采用类似的思路,既然找到了当前list_node成员的偏移量,那么减去偏移量,便是外层包围着的结构体的起始地址。

那好,这样我们就能实现这个链表了,但其实也不用这样费事,还有简单的办法,那就是让我们的被包含的list_node成员结构,处于我们数据存储结构的第一个位置,那么其地址就是包围其结构的地址,直接类型转换即可。

这种思路实际上也被用于C语言的继承机制的实现。

Github项目

到此,我们已经讲解了通用链表的实现思路,大家可以尝试自己实现一个独特的通用链表,具体内容就不一一赘述,我将完整的工程已经发布到了Github上面,欢迎大家前来fork测试。
【Clist项目地址】

本人才疏学浅,如有疏漏,还望指正。