PS:本次更新加了示意图,并对部分解释做了补充
在艰难理解链表和节点之后,我们迎来了整个学习过程中的第一个难点——链表的代码实现。
本篇涉及结构体的应用,可以参考我的文章:结构体的定义和应用-CSDN博客
或者去订阅我的FreeRTOS补课专栏,在学习过程中遇到的如数据结构和C语言方面的知识会在里面更新。
因为之前我在(1)的时候直接把工程模板的文件复制了一份(我复制的是时间片那个),所以算上今天会用到的list.h文件,一共是7个文件在include文件夹里。这里我把目前不需要的四个删掉了,所以是下图的几个文件:
当然,你也可以选择把list.h保留,这样剩下的3个文件我们直接移植就可以了。接下来,我会把需要的代码都贴出来。
首先,我们需要先定义节点的数据体结构。 在FreeRTOS中,最常用的是双链表。因此这里定义的是双链表,但单链表其实也是一样的定义流程。定义链表需要分别定义结构体和初始化,结构体在list.h中定义,初始化在list.c中完成。
定义节点数据结构 list.h
#include "FreeRTOS.h"
/* 节点结构体定义 */
struct xLIST_ITEM
{
TickType_t xItemValue; /* 辅助值,用于帮助节点做顺序排列 */ (1)
struct xLIST_ITEM * pxNext; /* 指向链表下一个节点 */ (2)
struct xLIST_ITEM * pxPrevious; /* 指向链表前一个节点 */ (3)
void * pvOwner; /* 指向拥有该节点的内核对象,通常是TCB */ (4)
void * pvContainer; /* 指向该节点所在的链表 */ (5)
};
typedef struct xLIST_ITEM ListItem_t; /* 节点数据类型重定义 */ (6)
(1)xItemValue是一个辅助值,用于帮助节点做顺序排列。该辅助值的数据类型为TickType_t。因为我们已经移植好了头文件,所以TickType_t并不会报错。在 FreeRTOS 中,凡是涉及到数据类型的地方,FreeRTOS 都会将其用 typedef 重新取一个类型名。这些经过重定义的数据类型放在 portmacro.h中。
采用这种方式的好处是为了方便系统在不同类型的单片机上移植,避免了单片机因为位数不同而使系统出现问题。
(2)和(3)是节点指针,用于指向上一个和下一个节点;
(4)涉及到任务的相关知识,这里不做分析
(5)这个指针指向这个节点所在的链表,也就是指向其所在链表的根节点。通过这个指针,系统可以找到其所在的链表。
(6)结构体数据类型重定义。typedef在这里是重定义的作用,struct xLIST_ITEM ListItem_t是将结构体声明为ListItem_t变量。
节点初始化 list.c
所谓节点初始化,就是一个节点最初的样子。节点刚创建的时候,没有插入链表,所以使comtainer指针为空。
/* 节点初始化 */
void vListInitialiseItem( ListItem_t * const pxItem )
{
/* 初始化该节点所在的链表为空,表示节点还没有插入任何链表 */
pxItem->pvContainer = NULL;
}
定义根节点数据结构 list.h
/* 链表(根节点)结构体定义 */
typedef struct xLIST
{
UBaseType_t uxNumberOfItems; /* 链表节点计数器 */ (1)
ListItem_t * pxIndex; /* 链表节点索引指针 */(2)
MiniListItem_t xListEnd; /* 链表最后一个节点 */(3)
} List_t;
(1)链表节点计数器,用于表示该链表下有多少个节点,根节点除外。
(2)链表节点索引指针,用于遍历节点。遍历,简单来讲就是沿着链表访问所有数据,之后插入和删除可能用得到。
(3)由于链表首位相连,所以第一个节点又是最后一个,我认为其和根节点是类似的。姑且称之为迷你节点吧。
定义迷你节点结构体 list.h
/* mini节点结构体定义,作为双向链表的结尾
因为双向链表是首尾相连的,头即是尾,尾即是头 */
struct xMINI_LIST_ITEM
{
TickType_t xItemValue; /* 辅助值,用于帮助节点做升序排列 */
struct xLIST_ITEM * pxNext; /* 指向链表下一个节点 */
struct xLIST_ITEM * pxPrevious; /* 指向链表前一个节点 */
};
typedef struct xMINI_LIST_ITEM MiniListItem_t; /* 最小节点数据类型重定义 */
根节点初始化 list.c
/* 链表根节点初始化 */
void vListInitialise( List_t * const pxList )
{
/* 将链表索引指针指向根节点 */ (1)
pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd );
/* 将链表最后一个节点的辅助排序的值设置为最大,确保该节点就是链表的最后节点 */(2)
pxList->xListEnd.xItemValue = portMAX_DELAY;
/* 将最后一个节点的pxNext和pxPrevious指针均指向节点自身,表示链表为空 */(3)
pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd );
pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd );
/* 初始化链表节点计数器的值为0,表示链表为空 */ (4)
pxList->uxNumberOfItems = ( UBaseType_t ) 0U;
}
(1)因为根节点,或者说第一个或最后一个节点不计入节点计数器,所以将索引指针指向根节点才能正常使用节点计数器。
(2)为了确保这个节点是最后一个节点,将辅助值拉满。
(3)作为第一个节点,其previous指针只能指向自己;作为最后一个节点,next节点也必须指向自己。
在完成节点和根节点的结构体定义和初始化之后,就可以进行插入和删除操作了。
将新节点插入到链表
本质上就是将节点插入到空链表的尾部(也可以说成是头部)。
/* 将节点插入到链表的尾部 */
void vListInsertEnd( List_t * const pxList, ListItem_t * const pxNewListItem )
{
ListItem_t * const pxIndex = pxList->pxIndex;
pxNewListItem->pxNext = pxIndex;
pxNewListItem->pxPrevious = pxIndex->pxPrevious;
pxIndex->pxPrevious->pxNext = pxNewListItem;
pxIndex->pxPrevious = pxNewListItem;
/* 记住该节点所在的链表 */
pxNewListItem->pvContainer = ( void * ) pxList;
/* 链表节点计数器++ */
( pxList->uxNumberOfItems )++;
}
简单来说就是实现下图的变换过程:
PS:为了简单明了,我省略了除了next和pervious外的其他数据,不是没有。
两个参数分别是被插入的链表和插入的节点。
整个函数流程如下:
首先,定义一个索引指向最后一个节点。这一步是为了后续对根节点进行操作。
让新节点的next指向根节点;
让新节点的previous指向根节点的上一个节点;
让根节点的上一个节点的next指向新节点;
让根节点的previous指向新节点;
让新节点的container记住这个链表;
最后使根节点的节点计数器加一。
将节点按照升序排列插入到链表
将节点按照升序排列插入到链表,如果有两个节点的值相同,则新节点在旧节点的后面插入。
/* 将节点按照升序排列插入到链表 */
void vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
{
ListItem_t *pxIterator;
/* 获取节点的排序辅助值 */
const TickType_t xValueOfInsertion = pxNewListItem->xItemValue;
/* 寻找节点要插入的位置 */
if( xValueOfInsertion == portMAX_DELAY )
{
pxIterator = pxList->xListEnd.pxPrevious;
}
else
{
for( pxIterator = ( ListItem_t * ) &( pxList->xListEnd );
pxIterator->pxNext->xItemValue <= xValueOfInsertion;
pxIterator = pxIterator->pxNext )
{
/* 没有事情可做,不断迭代只为了找到节点要插入的位置 */
}
}
pxNewListItem->pxNext = pxIterator->pxNext;
pxNewListItem->pxNext->pxPrevious = pxNewListItem;
pxNewListItem->pxPrevious = pxIterator;
pxIterator->pxNext = pxNewListItem;
/* 记住该节点所在的链表 */
pxNewListItem->pvContainer = ( void * ) pxList;
/* 链表节点计数器++ */
( pxList->uxNumberOfItems )++;
}
整个函数流程如下:
首先定义一个指针,用于存放要插入的节点。
获得辅助值,然后寻找要插入的位置。
如果辅助值等于最大,那么插入到最后一个节点后面;
否则的话进行for循环寻找插入的位置;
找到插入位置后,让新节点的next指向原节点下一个节点;
让新节点的下一个节点的previous指向新节点;
让新节点的previous指向原节点;
原节点的next指向新节点;
这部分就是分别和两边的节点建立联系,理解了上面的尾部插入后难理解。
让新节点的container记住这个链表;
最后使根节点的节点计数器加一。
接下来解释以下for循环的原理:
我们常用的一个for是这个样子的:for(i=0;i+1<=j;i++)
第一个分号的内容是令pxIterator指向根节点,换句话说就是令其为0;
第二句的意思是pxIterator的下一个节点的辅助值是否大于或等于xValueOfInsertion;
第三句就是pxIterator等于其对应的下一个节点。
当i=j的时候,跳出循环,如跳出时链表有和pxIterator辅助值一样的话,插入其后面;没有的话就直接插入那个位置。
将节点从链表删除
假设将一个有三个节点的链表中的中间节点删除。
/* 将节点从链表中删除 */
UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
{
/* 获取节点所在的链表 */
List_t * const pxList = ( List_t * ) pxItemToRemove->pvContainer;
pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious;
pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;
/* Make sure the index is left pointing to a valid item. */
if( pxList->pxIndex == pxItemToRemove )
{
pxList->pxIndex = pxItemToRemove->pxPrevious;
}
/* 初始化该节点所在的链表为空,表示节点还没有插入任何链表 */
pxItemToRemove->pvContainer = NULL;
/* 链表节点计数器-- */
( pxList->uxNumberOfItems )--;
/* 返回链表中剩余节点的个数 */
return pxList->uxNumberOfItems;
}
整个函数流程如下:
首先获取要删除节点所在的链表;
使要删除节点的下一个节点的previous指向要删除节点的上一个节点;
使要删除节点的上一个节点的next指向要删除节点的下一个节点;
调整将该链表的节点索引指针;
初始化要删除节点所在的链表为空,表示节点还没有插入任何链表;
然后将节点计数器减一并返回。
至此,链表的基本用法告一段落。至于野火的实验,没什么东西,看心情弄吧。
PS:理解弄清楚整个链表部分几乎花了我几乎三天时间,我还特意去复习了结构体和指针的部分。之后有时间我会画图放在这篇文章里面,方便理解,双休日先放个假。
好累QAQ······