RT-Thread中的链表组织结构
RT-Thread中的链表是带表头节点的双向循环链表结构,它的表头节点与之前的博客《双向循环链表》中介绍的表头节点不同,之前博客介绍的表头节点与后继节点结构是一致的,这是因为指针类型问题,前面介绍过的链表都是前驱节点指向后继节点的首地址,即指向节点结构体的指针。RT-Thread链表节点中的指针并不是指向节点首地址(这种说法并不严谨,尽管实际上它确实不是指向节点首地址),而是指向节点中的list结构体元素,这种链表结构让链表更加灵活。
/**
* 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. */
RT-Thread中的链表指针定义为rt_list_t
,而不是节点类型,这就可以使链表的操作(例如:插入、删除)不用依赖整个节点,不用管节点结构体中成员的具体情况,甚至可以将不同类型的节点插入链表。这就是为什么表头节点与其他节点不同,在操作链表时却没有带来额外的麻烦。
节点与节点之间可以不一致,那么不管链表中节点用于存放何种数据,它的size
有多大,表头节点都可以只存放用于实现算法所需的辅助数据,在节点比较大时可以节省内存空间,还可以一致实现不同链表的表头节点。
RT-Thread中不同Object链表的表头节点都存放于rt_object_container
数组当中,各个链表在初始化时都初始化为带表头节点的空表,不同Object
的节点不同,同一Object
的节点也不尽相同。但表头都采用了统一结构:
/**
* The information of the kernel object
*/
struct rt_object_information
{
enum rt_object_class_type type; /**< object class type */
rt_list_t object_list; /**< object list */
rt_size_t object_size; /**< object size */
};
RT-Thread中的链表操作
之前介绍的双向循环链表的插入、删除等操作都依赖于节点的结构体类型,要准确找到next
、prev
等指针在节点中的位置,需要将指向节点的指针定义为该节点类型指针。在诸多不同链表,甚至同一链表中节点结构不尽相同的情况下,这种方式变得不可行。在节点结构体成员的顺序性不做要求的情况下,可以将prev
、next
两个指针放在节点起始位置,在遍历整个链表时,进行强转,从而实现不同链表、不同节点之间的遍历。但是RT-Thread中,每个节点的起始地址都用于存放"parent"
以实现类似面向对象语言中的一些特性。且每个"class"
的"parent"
长度不一,导致之后存放指针的位置不定,强转这真是个馊主意😐
链表节点的插入
在前文中提到,RT-Thread链表节点中都包含rt_list_t
结构,且prev
、next
指针都指向各自前驱节点和后继节点中的rt_list_t
成员,那链表插入就不麻烦了,只要将《双向循环链表》的操作方法稍微改动以下,将节点起始地址改为rt_list_t
节点起始地址。
&emsp;RT-Thread中专门实现了插入相关的内联函数:
/**
* @brief insert a node after a list
*
* @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;
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;
}
链表节点的删除
节点的删除也是同样的道理,在删除相关节点的时候,不再是将后继节点(前驱节点)的起始地址赋给前驱节点(后继节点)的指针,而是将后继节点(前驱节点)中rt_list_t
成员的地址赋给前驱节点(后继节点)的指针。
/**
* @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;
}
链表节点元素访问
既然rt_list_t
成员是存放在节点中部或是尾部,且不同类型的节点rt_list_t
成员位置还不一样,那在遍历整个链表时,获得的是后继节点(前驱节点)的rt_list_t
成员的地址,那如何根据rt_list_t
成员的地址访问节点中其他元素。
尽管不同类型节点中rt_list_t
成员位置不定,但是在确定类型节点中,rt_list_t
成员的偏移是固定的,在获取rt_list_t
成员地址的情况下,计算出rt_list_t
成员在该节点中的偏移,即(rt_list_t成员地址)-(rt_list_t成员偏移)=节点起始地址
。关键在于如何计算不同类型节点中rt_list_t成员偏移
。RT-Thread中给出的相应算法如下:
/**
* rt_container_of - return the member address of ptr, if the type of ptr is the
* struct type.
*/
#define rt_container_of(ptr, type, member) \
((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))
该宏替换之后,ptr
是该节点中rt_list_t
成员首地址,member
是rt_list_t
结构体成员,type
则是该节点的结构体类型。
在0
地址处强制转化为type
结构体类型,取得的相关member
成员地址即为member
成员在type
类型结构体中的偏移。
type类型结构体 | 成员地址 |
---|---|
int element1 | 0x00000000 |
int element2 | 0x00000004 |
int element3 | 0x00000008 |
int element4 | 040000000c |
int element5 | 0x00000010 |
… | … |
member | member成员地址 |
… | … |
将计算获得的节点首地址强制转化为相应的结构体类型便可访问相应的数据。
来源:CSDN
作者:Y-JESSE
链接:https://blog.csdn.net/y1757655788/article/details/104059572