跟着野火从零开始手搓FreeRTOS(2.2)链表的初始化和插入

PS:本次更新加了示意图,并对部分解释做了补充

在艰难理解链表和节点之后,我们迎来了整个学习过程中的第一个难点——链表的代码实现。

本篇涉及结构体的应用,可以参考我的文章:结构体的定义和应用-CSDN博客

或者去订阅我的FreeRTOS补课专栏,在学习过程中遇到的如数据结构和C语言方面的知识会在里面更新。

        因为之前我在(1)的时候直接把工程模板的文件复制了一份(我复制的是时间片那个),所以算上今天会用到的list.h文件,一共是7个文件在include文件夹里。这里我把目前不需要的四个删掉了,所以是下图的几个文件:

21312606f22241ae9129fd8229f58af4.png

        当然,你也可以选择把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变量。

3dffccbbd4064918a2b98e6531549af1.png

节点初始化    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)由于链表首位相连,所以第一个节点又是最后一个,我认为其和根节点是类似的。姑且称之为迷你节点吧。

7110a412ccb44ddeb1ca2d016af0db1b.png

定义迷你节点结构体    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节点也必须指向自己。

        在完成节点和根节点的结构体定义和初始化之后,就可以进行插入和删除操作了。

b90d8c03cee844b4a0c1f863cb8276d1.png

将新节点插入到链表

本质上就是将节点插入到空链表的尾部(也可以说成是头部)。

/* 将节点插入到链表的尾部 */
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外的其他数据,不是没有。

45b7594b0cf04522902af3de0e9a0bbc.png

两个参数分别是被插入的链表和插入的节点。
整个函数流程如下:
首先,定义一个索引指向最后一个节点。这一步是为了后续对根节点进行操作。


让新节点的next指向根节点;

c9c7f3dfb70c4f07adfdc628ee6c84e0.png

让新节点的previous指向根节点的上一个节点;

5452e067049e43b3a8a670d10c7fd256.png

让根节点的上一个节点的next指向新节点;

6478bcccf5c34104a25500e756abbdd2.png

让根节点的previous指向新节点;

c9e850041a2d4f35b22cb5cfcc9adb61.png

让新节点的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······

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值