數據結構 | TencentOS-tiny中的雙向循環鏈表的實現及使用

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』。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章