[016] [RT-Thread学习笔记] 链表源码分析

RT-Thread
学习笔记
单向链表
双向链表
list_for_each与list_for_each_safe

RT-Thread版本:4.0.5
MCU型号:STM32F103RCT6(ARM Cortex-M3 内核)

rt-thread中的链表源码在rtservice.h中,以宏或者内联函数的形式给出,其中,list为双向链表,slist为单向链表。rt-thread链表主要是为了连接管理内核对象,因此并无数据域,仅有指针域。

1 单向链表

1.1 单向链表节点结构体

Single List structure定义在rtdef.h中:

struct rt_slist_node
{
    struct rt_slist_node *next;
};
typedef struct rt_slist_node rt_slist_t; 

链表是一种递归定义的结构体,即自已定义自已,需要注意只能定义指向自己的指针,因为指针大小可以确定,如果直接定义,编译器不知道具体大小会报错。

1.2 初始化

初始化有两种方法:

  • 内联函数
rt_inline void rt_slist_init(rt_slist_t *l)
{
    l->next = RT_NULL;
}

显然,这是带头结点的单链表初始化方式,将头结点指针域置空。

#define RT_SLIST_OBJECT_INIT(object) { RT_NULL }

使用示例:

rt_slist_t sl_node = RT_SLIST_OBJECT_INIT(sl_node);
// 宏展开:
sl_node = {RT_NULL};
// 即:
sl_node = {.next = RT_NULL};

1.3 尾插

rt_inline void rt_slist_append(rt_slist_t *l, rt_slist_t *n)
{
    struct rt_slist_node *node;
	
    node = l;
    // 遍历链表至表尾节点
    while (node->next) node = node->next;
	// 表尾节点指针域指向新插入节点
    node->next = n;
    // 新节点指针域置空
    n->next = RT_NULL;
}

l:链表头指针

n:待插入的节点指针

注意:不能直接用头指针遍历链表,否则头指针最后将指向表尾,链表无法再使用。

1.4 头插

rt_inline void rt_slist_insert(rt_slist_t *l, rt_slist_t *n)
{
    // 新节点指针域指向首元节点(第一个节点)
    n->next = l->next;
    // 将头节点指针域指向新节点,新节点成为首元节点
    l->next = n;
}

1.5 求表长

rt_inline unsigned int rt_slist_len(const rt_slist_t *l)
{
    unsigned int len = 0;
    // l->next为第一个节点
    const rt_slist_t *list = l->next;
    while (list != RT_NULL)
    {
        list = list->next;
        len ++;
    }

    return len;
}

const修饰指向表头结点的指针,表明求表长时不能修改原链表。

1.6 删除指定节点

rt_inline rt_slist_t *rt_slist_remove(rt_slist_t *l, rt_slist_t *n)
{
    struct rt_slist_node *node = l;
    // 遍历链表找到要删除节点的位置
    while (node->next && node->next != n) node = node->next;
	// 如果没有遍历到最后一个节点, 说明找到删除节点的位置, 即当前节点的下一节点node->next
    if (node->next != (rt_slist_t *)0) 
        node->next = node->next->next;	// 将当前节点指针域指向删除节点的指针域, 即跳过删除节点

    return l;	// 返回修改后的链表
}

1.7 获取首元节点

rt_inline rt_slist_t *rt_slist_first(rt_slist_t *l)
{
    return l->next;
}

1.8 获取尾节点


rt_inline rt_slist_t *rt_slist_tail(rt_slist_t *l)
{
    while (l->next) l = l->next;

    return l;
}

1.9 获取当前节点下一个节点

rt_inline rt_slist_t *rt_slist_next(rt_slist_t *n)
{
    return n->next;
}

1.10 判断表空

rt_inline int rt_slist_isempty(rt_slist_t *l)
{
    return l->next == RT_NULL;	// 带头节点的单链表表空判断方法
}

1.11 单向链表相关宏

关于rt_container_of具体分析可参考:rt_list_entry函数

/**
 * @brief 根据单链表节点的地址,获取其所在type类型结构体的地址
 * @param node   入口节点地址
 * @param type   节点所在结构体的类型  
 * @param member 链表所在结构体中的链表变量的名称
 */
#define rt_slist_entry(node, type, member) \
    rt_container_of(node, type, member)

/**
 * 遍历单链表
 * @pos:    for循环中迭代变量(rt_slist_t *)
 * @head:   slist链表头结点
 */
#define rt_slist_for_each(pos, head) \
    for (pos = (head)->next; pos != RT_NULL; pos = pos->next)

/**
 * 遍历获取每个单链表节点所在type类型结构体的地址
 * typeof 运算符把类型信息作为字符串返回, typeof(*pos)即为当前节点指向的结构体类型的字符
 * @pos:    指向宿主结构的指针, 在for循环中是一个迭代变量(type*类型)
 * @head:   slist表头指针
 * @member: 链表所在结构体中的链表变量的名称
 */
#define rt_slist_for_each_entry(pos, head, member) \
    for (pos = rt_slist_entry((head)->next, typeof(*pos), member); \
         &pos->member != (RT_NULL); \
         pos = rt_slist_entry(pos->member.next, typeof(*pos), member))

/**
 * 获取表头元素所在结构体地址
 * @ptr:    slist表头指针
 * @type:   链表所在结构体的类型
 * @member: 链表所在结构体中的链表变量的名称
 * 注意:链表不能为空
 */
#define rt_slist_first_entry(ptr, type, member) \
    rt_slist_entry((ptr)->next, type, member)

/**
 * 获取表尾元素所在结构体地址
 * @ptr:    slist表头指针
 * @type:   链表所在结构体的类型
 * @member: 链表所在结构体中的链表变量的名称
 * 注意:链表不能为空
 */
#define rt_slist_tail_entry(ptr, type, member) \
    rt_slist_entry(rt_slist_tail(ptr), type, member)

2 双向链表

2.1 双向链表节点结构体

Single List structure定义在rtdef.h中:

struct rt_list_node
{
    struct rt_list_node *next;
    struct rt_list_node *prev;
};
typedef struct rt_list_node rt_list_t;

双向链表有两个指针域,next指向下一个节点,prev指向上一个节点。因为多了一个prev指针域,相对于单链表,其获取前驱结点比较方便(l->prev),时间复杂度为O(1);而单链表则需要遍历查找,时间复杂度为O(n)。

2.2 初始化

  • 内联函数
rt_inline void rt_list_init(rt_list_t *l)
{
    l->next = l->prev = l;
}

[016] [RT-Thread学习笔记] 链表源码分析_第1张图片

双向非循环链表初始化是将l->next = l->prev = NULL;,而这种是双向循环链表初始化方法,即将头结点的前驱指针与后继指针均指向自身。

#define RT_LIST_OBJECT_INIT(object) { &(object), &(object) }

用法示例:

rt_list_t l_node = RT_LIST_OBJECT_INIT(l_node);
// 宏展开:
l_node = {&l_node, &l_node};
// 即:
l_node = {.next = &l_node, .prev = &l_node};

2.3 指定节点后插入

/**
 * @param l list to insert it
 * @param n new node to be inserted
 */
rt_inline void rt_list_insert_after(rt_list_t *l, rt_list_t *n)
{
    // 与当前节点的后一个节点连接
    l->next->prev = n;	// 1.将当前节点的后一个节点的prev指针指向新节点
    n->next = l->next;	// 2.将新节点的next指针指向当前节点的后一个节点
	// 与当前节点连接
    l->next = n;		// 3.将当前节点的next指针指向新节点
    n->prev = l;		// 4.将新节点的prev指针指向当前节点
}

[016] [RT-Thread学习笔记] 链表源码分析_第2张图片

图中Node1为当前节点,Node2当前节点的后一个节点

l为当前节点,n为插入在l后的新节点。双向链表插入相对单链表,需要修改两个方向上的指针,即当前节点lnext指针域(l->next),当前节点后一个节点的prev指针域(l->next->prev)。

2.4 指定节点前插入

/**
 * @param l list to insert it
 * @param n new node to be inserted
 */
rt_inline void rt_list_insert_before(rt_list_t *l, rt_list_t *n)
{
    // 与当前节点的前一个节点连接
    l->prev->next = n;	// 1.将当前节点的前一个节点的next指针指向新节点
    n->prev = l->prev;	// 2.将新节点的prev指针指向当前节点的前一个节点
	// 与当前节点连接
    l->prev = n;		// 3.将当前节点的prev指针指向新节点
    n->next = l;		// 4.将新节点的next指针指向当前节点
}

[016] [RT-Thread学习笔记] 链表源码分析_第3张图片

图中Node1为当前节点,Node3当前节点的前一个节点(Node1prev指向它,这是一个双向循环链表,表尾next要指向表头,表头prev要指向表尾)

同时需要修改两个方向上的指针:当前节点lprev指针域(l->prev),当前节点前一个节点的next指针域(l->prev->next)。

2.5 求表长

rt_inline unsigned int rt_list_len(const rt_list_t *l)
{
    unsigned int len = 0;
    const rt_list_t *p = l;
    // 循环链表可以从任意节点开始遍历求表长, 当当前节点next指针自身时,结束遍历
    while (p->next != l)
    {
        p = p->next;
        len ++;
    }

    return len;
}

2.6 删除指定节点

rt_inline void rt_list_remove(rt_list_t *n)
{
    n->next->prev = n->prev;	// 1.被删节点的后一个节点的prev指向被删结点的前一个结点
    n->prev->next = n->next;	// 2.被删节点的前一个节点next指向被删结点的后一个结点

    n->next = n->prev = n;		// 3.被删结点置空
}

[016] [RT-Thread学习笔记] 链表源码分析_第4张图片
同时需要修改两个方向上的指针:被删节点的后一个节点的prev指针域(n->next->prev),被删节点的前一个节点的next指针域(n->prev->next)。

2.7 判断表空

rt_inline int rt_list_isempty(const rt_list_t *l)
{
    return l->next == l;
}

2.8 双向链表相关宏

/**
 * @brief 根据双向链表节点的地址,获取其所在type类型结构体的地址
 * @param node   入口节点地址
 * @param type   节点所在结构体的类型  
 * @param member 节点在该结构体中的成员名称
 */
#define rt_list_entry(node, type, member) \
    rt_container_of(node, type, member)

/**
 * 遍历双向循环链表
 * @pos:	for循环中迭代变量(rt_list_t *类型)
 * @head:	表头
 */
#define rt_list_for_each(pos, head) \
    for (pos = (head)->next; pos != (head); pos = pos->next)

/**
 * 安全地遍历一个链表,防止遍历过程中删除链表元素引发的异常
 * @pos:    for循环中迭代变量(rt_list_t *类型)
 * @n:      临时存储节点pos变量(rt_list_t *类型)
 * @head:   表头
 */
#define rt_list_for_each_safe(pos, n, head) \
    for (pos = (head)->next, n = pos->next; pos != (head); \
        pos = n, n = pos->next)

/**
 * 遍历获取每个双向链表节点所在type类型结构体的地址
 * typeof 运算符把类型信息作为字符串返回, typeof(*pos)即为当前节点指向的结构体类型的字符
 * @pos:    指向宿主结构的指针, 在for循环中是一个迭代变量
 * @head:   表头指针
 * @member: 链表所在结构体中的链表变量的名称
 */
#define rt_list_for_each_entry(pos, head, member) \
    for (pos = rt_list_entry((head)->next, typeof(*pos), member); \
         &pos->member != (head); \
         pos = rt_list_entry(pos->member.next, typeof(*pos), member))
/**
 * 安全遍历获取每个双向链表节点所在type类型结构体的地址
 * typeof 运算符把类型信息作为字符串返回, typeof(*pos)即为当前节点指向的结构体类型的字符
 * @pos:    指向宿主结构的指针, 在for循环中是一个迭代变量(type*类型)
 * @n:      临时存储pos变量(type*类型)
 * @head:   表头指针
 * @member: 链表所在结构体中的链表变量的名称
 */
#define rt_list_for_each_entry_safe(pos, n, head, member) \
    for (pos = rt_list_entry((head)->next, typeof(*pos), member), \
         n = rt_list_entry(pos->member.next, typeof(*pos), member); \
         &pos->member != (head); \
         pos = n, n = rt_list_entry(n->member.next, typeof(*n), member))

/**
 * 获取表头元素所在结构体地址
 * @ptr:    表头指针
 * @type:   链表所在结构体的类型
 * @member: 链表所在结构体中的链表变量的名称
 * 注意:链表不能为空
 */
#define rt_list_first_entry(ptr, type, member) \
    rt_list_entry((ptr)->next, type, member)

3 list_for_eachlist_for_each_safe

  • list_for_each遍历链表循环程序中,如果将当前pos指向的节点内存释放,那么该节点的前后指针将变为野指针。此外,rt_list_init(pos)rt_list_remove(pos)将让pos指向的节点的前后指针指向自身,导致死循环。
  • list_for_each_safe被称为安全遍历,它的做法是:先将pos后继指针pos->next缓存到n,下次遍历时再将n赋给pos,同时让n = pos->next,这样避免了直接使用pos = pos->next引发的异常。

注意:这种安全遍历也仅对错误操作当前遍历的pos指针的节点有效,如果误操作其后面还未遍历到的节点,依然会出错。

测试代码:

rt_list_t l = RT_LIST_OBJECT_INIT(l);
rt_list_t l_node1 = RT_LIST_OBJECT_INIT(l_node1);
rt_list_t* l_node2 = new rt_list_t;
rt_list_t l_node3 = RT_LIST_OBJECT_INIT(l_node3);
int main(int* arac, char** argv)
{
	rt_list_init(l_node2);
	rt_list_insert_after(&l, &l_node1);
	rt_list_insert_after(&l_node1, l_node2);
	rt_list_insert_after(l_node2, &l_node3);

	cout << "l_node1:" << &l_node1 << endl;
	cout << "l_node2:"  << l_node2 << endl;
	cout << "l_node3:"  << &l_node3 << endl;
	cout << "-------------------" << endl;

	rt_list_t* node_ptr = &l_node1;
	rt_list_t* l_ptr = &l;
	rt_list_t* n_ptr;
	rt_list_for_each_safe(node_ptr, n_ptr, l_ptr)
//  rt_list_for_each(node_ptr, l_ptr)
    {
     cout << node_ptr << endl;
     // 非安全遍历会死循环 
	 // rt_list_remove(node_ptr);
	 // rt_list_init(node_ptr);
	 // 野指针 程序崩溃 
     if (node_ptr == l_node2 && node_ptr != NULL) {
         delete node_ptr;
         node_ptr = NULL;
     }
    }
	system("pause");

	return 0;
}
  • 死循环情况:
    rt_list_for_each
    [016] [RT-Thread学习笔记] 链表源码分析_第5张图片
    rt_list_for_each_safe
    [016] [RT-Thread学习笔记] 链表源码分析_第6张图片
  • 野指针情况:
    rt_list_for_each
    [016] [RT-Thread学习笔记] 链表源码分析_第7张图片
    rt_list_for_each_safe
    [016] [RT-Thread学习笔记] 链表源码分析_第8张图片

END

你可能感兴趣的:(RT-Thread,链表,学习,数据结构,rtos)