一、目的
学习过C语言的同学应该都知道几种常用的数据结构,例如数组、单链表、双链表等。
每种数据结构都有其特点和应用场景,本篇就结合RT-Thread源码分析一下其双链表实现细节和特点。
那什么是双链表呢,这边简单解释一下帮助大家理解。
通过双链表中的链表头可以直接访问到A,因为A是其下一个节点;也可以直接访问到C,因为C是其上一个节点(也可以认为是链表的最后一个节点)。
链表上的每一个节点都能访问到其上一个节点或者下一个节点,例如B就可以直接定位到其前置节点A或者后置节点C。
这样的数据结构就是双链表,每个节点都有前置和后置节点指针。
二、介绍
刚刚学习数据结构的同学如果自己实现双链表一般会这样设计:
struct my_list {
struct my_list *prev;
struct my_list *next;
void *my_data;
};
其中prev/next指针指向前后节点,my_data字段存放特定的数据结构。
但是这种设计有一个最令人诟病的问题,以后如果要复用此双链表就必须重新再写一遍;所以这样的双链表设计只能说能用但不是最好用的,那有没有更好的设计呢?
如果你是一个资深开发人员,你肯定知道linux里面有个最优的设计,大家可以去下面的链接去深入学习。
list.h - include/linux/list.h - Linux source code (v6.1.5) - Bootlin
其实RT-Thread的作者估计也是一个深度linux开发者(很多代码设计亮点都是从linux借鉴过来的)。
下面我们正式介绍一下RT-Thread中的双链表结构。
首先我们先来看一下链表头的设计
/**
* Double List structure
*/
struct rt_list_node
{
struct rt_list_node *next; /**< point to next node. */
struct rt_list_node *prev; /**< point to prev node. */
};
typedef struct rt_list_node rt_list_t; /**< Type for lists. */
链表头其实很简单,里面就只有两个字段prev/next指针,分别指向链表中的第一个和最后一个节点。
链表头需要初始化,prev/next指针都指向自身。
/**
* @brief initialize a list
*
* @param l list to be initialized
*/
rt_inline void rt_list_init(rt_list_t *l)
{
l->next = l->prev = l;
}
如果prev或者next指向自身则代表链表为空
rt_inline int rt_list_isempty(const rt_list_t *l)
{
//return l->next == l;
return l->prev = l;
}
从上面的代码片段中细心的小伙伴可能会发现链表头和链表节点使用的是同样的数据结构,大家是不是很疑惑(链表节点中一般都要包含特定的业务数据信息)。
其实大家可以这么理解,链表头就是一个没有数据块的数据节点。
上图是一个拥有3个数据节点A/B/C的双链表,A是第一个节点,C是最后一个节点。
如果某个节点从链表中去除,那么这个节点就变成孤立节点
孤立节点的prev/next都指向自身。
如果我们将节点B从链表中取下,那么此时的链表变成
从链表中删除一个节点的代码如下
/**
* @brief remove node from list.
* @param n the node to remove from the list.
*/
rt_inline void rt_list_remove(rt_list_t *n)
{
n->next->prev = n->prev;
n->prev->next = n->next;
n->next = n->prev = n;
}
只需要将被删除节点的前一个节点的next指针指向被删除节点下一个节点;下一个节点的prev指针指向被删除节点的上一个节点。
在某个节点处可以在其前后插入一个新的节点
rt_inline void rt_list_insert_after(rt_list_t *l, rt_list_t *n)
{
l->next->prev = n;
n->next = l->next;
l->next = n;
n->prev = l;
}
/**
* @brief insert a node before a list
*
* @param n new node to be inserted
* @param l list to insert it
*/
rt_inline void rt_list_insert_before(rt_list_t *l, rt_list_t *n)
{
l->prev->next = n;
n->prev = l->prev;
l->prev = n;
n->next = l;
}
链表的遍历,也就是从链表头依次编译每个节点,直到某个节点的next指针指向的是链表头节点则结束
/**
* rt_list_for_each - iterate over a list
* @pos: the rt_list_t * to use as a loop cursor.
* @head: the head for your list.
*/
#define rt_list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)
链表的遍历的安全版本,在遍历过程中可以新增或者删除一个节点
/**
* rt_list_for_each_safe - iterate over a list safe against removal of list entry
* @pos: the rt_list_t * to use as a loop cursor.
* @n: another rt_list_t * to use as temporary storage
* @head: the head for your list.
*/
#define rt_list_for_each_safe(pos, n, head) \
for (pos = (head)->next, n = pos->next; pos != (head); \
pos = n, n = pos->next)
实现细节就是将当前被遍历的节点的下一个节点先保存起来
如何使用该链表结构呢?
假如我们有这样一个数据结构
struct foo {
int a;
int b;
};
那么我们可以重新定义我们的foo结构,将rt_list_t作为一个双链表钩子放置在我们定义的结构体中。
struct foo {
rt_list_t list;
int a;
int b;
};
重要知识点:通过结构体内的字段地址获取结构体首地址
/**
* @brief get the struct for this entry
* @param node the entry point
* @param type the type of structure
* @param member the name of list in structure
*/
#define rt_list_entry(node, type, member) \
rt_container_of(node, type, member)
关于container_of的讲解请看《container_of(ptr, type, member)详解》。
至此,我们基本讲解完RT-Thread的双链表结构。