1. 什麼是雙向循環鏈表
雙向鏈表也是鏈表的一種,區別在於每個節點除了後繼指針外,還有一個前驅指針,雙向鏈表的節點長下面這樣:
由這種節點構成的雙向鏈表有兩種分類:按照是否有頭結點可以分爲兩種,按照是否循環可以分爲兩種。
本文討論的是不帶頭節點的雙向循環鏈表,如下圖:
2. 雙向循環鏈表的實現
TencentOS-tiny中的雙向鏈表實現在tos_list.h
中。
2.1. 節點實現
節點數據結構的實現如下:
typedef struct k_list_node_st {
struct k_list_node_st *next;
struct k_list_node_st *prev;
} k_list_t;
2.2. 雙向鏈表初始化
鏈表初始化的實現如下:
void tos_list_init(k_list_t *list)
{
list->next = list;
list->prev = list;
}
其中傳入的list參數是指向雙向鏈表的頭指針,初始化之後,如圖:
2.3. 插入節點
向雙向鏈表中插入一個節點非常簡單:
void _list_add(k_list_t *node, k_list_t *prev, k_list_t *next)
{
next->prev = node;
node->next = next;
node->prev = prev;
prev->next = node;
}
其中node是待插入的節點,prev是插入節點位置的前一個節點,next是插入節點位置的後一個節點,插入過程如下。
插入前的雙向循環鏈表如下:
插入後的雙向循環鏈表如下:
圖中的四個插入過程分別對應代碼中的四行代碼。
除了這個基本的API之外,tencentOS-tiny還提供了兩個插入的API,分別是頭部插入和尾部插入:
void tos_list_add(k_list_t *node, k_list_t *list)
{
_list_add(node, list, list->next);
}
void tos_list_add_tail(k_list_t *node, k_list_t *list)
{
_list_add(node, list->prev, list);
}
因爲是雙向循環鏈表,所以尾部插入是在第一個節點和最後一個節點之間插入。
2.4. 刪除節點
同樣,刪除節點的操作也比較簡單,把鉤子斷開即可:
void _list_del(k_list_t *prev, k_list_t *next)
{
next->prev = prev;
prev->next = next;
}
void _list_del_node(k_list_t *node)
{
_list_del(node->prev, node->next);
}
刪除過程如圖所示,同樣,編號對應源碼中的兩行代碼:
2.6. 判斷鏈表是否爲空
判斷鏈表第一個節點是否指向自己即可:
int tos_list_empty(const k_list_t *list)
{
return list->next == list;
}
3. 雙向鏈表使用示例
3.1. 實驗內容
本實驗會創建一個帶有10個靜態結點的雙向鏈表,每個新的自定義節點中有一個數據域,存放一個uint8_t類型的值,有一個雙向鏈表節點,用於構成雙向鏈表。
3.2. 實驗代碼
首先包含內核頭文件:
/**
* @brief TencentOS-tiny雙向鏈表測試
* @author Mculover666
* @date 2020/6/2
*/
#include <tos_k.h>
創建一個自己的新節點:
typedef struct node
{
uint8_t data;
k_list_t list;
}node_t;
新建一個任務用來測試,編寫如下的任務入口函數:
#define LIST_LEN 10
void double_list_test(void *args)
{
int i;
/* 用於掛載自定義節點中的雙向節點 */
k_list_t list;
/* 創建10個鏈表節點 */
node_t node_pool[LIST_LEN];
/* 遍歷,初始化自定義節點的數據域和雙向節點 */
tos_list_init(&list);
for(i = 0;i < LIST_LEN;i++)
{
tos_list_init(&node_pool[i].list);
node_pool[i].data = i;
}
/* 構建一條具有LIST_LEN個節點的雙向鏈表 */
for(i = 0; i < LIST_LEN;i++)
{
tos_list_add_tail(&node_pool[i].list, &list);
}
/* 遍歷打印所有節點 */
k_list_t *cur;
node_t *n;
//for(cur = list.next;cur != &list;cur = cur->next)
TOS_LIST_FOR_EACH(cur, &list)
{
n = TOS_LIST_ENTRY(cur, node_t, list);
printf("n = %d\n", n->data);
}
return;
}
構建完成之後鏈表如圖(只畫了3個有數據的節點):
遍歷整條鏈表的時候,使用了tencentOS-tiny中提供的宏定義 TOS_LIST_FOR_EACH,它的定義如下:
#define TOS_LIST_FOR_EACH(curr, list) \
for (curr = (list)->next; curr != (list); curr = curr->next)
注意,此宏定義是從傳入地址的下一個節點開始遍歷!
還有最後一個使用問題,我們都是對整條鏈表進行操作(比如可以輕鬆的遍歷整條鏈表),操作的時候得到的地址都是node_t類型節點中k_list_t類型成員的地址,那麼如何訪問到data成員呢?
TencentOS-tiny中依然提供了兩個宏定義來解決這一問題,在tos_klib.h
中。
① 計算某一個成員在結構體基地址中的偏移地址:
#define TOS_OFFSET_OF_FIELD(type, field) \
((uint32_t)&(((type *)0)->field))
② 已知某一個成員的地址,計算結構體的基地址:
#define TOS_CONTAINER_OF_FIELD(ptr, type, field) \
((type *)((uint8_t *)(ptr) - TOS_OFFSET_OF_FIELD(type, field)))
這兩個宏定義的實現屬實有點騷,其中的巧妙之處可以再寫一篇文章講解了哈哈,此處我們先了解其使用即可(此處要感謝戴大神的解答)。
有了這兩個宏定義,就有了實驗中所使用的宏定義,用來獲取結構體(node_t類型節點)的基地址:
#define TOS_LIST_ENTRY(node, type, field) \
TOS_CONTAINER_OF_FIELD(node, type, field)
獲取到結構體的基地址,還愁訪問不到其中的任何一個成員嗎?
最後的實驗結果,你應該能猜到了,上圖:
接收更多精彩文章及資源推送,歡迎訂閱我的微信公衆號:『mculover666』。