RTOS-F429-HAL-列表的插入和删除(2026/7/29)
目录一FreeRTOS 列表 — 双向环形链表1、为什么不用数组2.列表的结构体原型3、三个成员逐个拆4、画出来5、哨兵存在的意义6列表下标的意义二FreeRTOS 列表项1结构体源码2、五个成员逐个拆3、每一列排队的人身上带了什么4、跟我们之前学的 TCB 连起来三迷你列表项1结构体定义四列表和列表项的关系1、空链表初始状态2、插入列表项 1xItemValue 103、再插入列表项 2xItemValue 204、哨兵的 xItemValue 为什么是 0xFFFFFFFF五FreeRTOS 列表 — 完整梳理1、为什么用链表而不用数组2、三种结构体3、List_t列表— 3 个成员4、ListItem_t列表项— 5 个成员5、MiniListItem_t哨兵— 精简版列表项6、插入操作——四步3 和 4 不能换7、三者关系一张图1pxReadyTasksLists[3].xListEnd.next2列表项 排序六API① vListInitialise — 列表初始化② vListInitialiseItem — 列表项初始化③ vListInsert — 升序插入④ vListInsertEnd — 尾插时间片轮转用⑤ uxListRemove — 摘除使用示例七FreeRTOS 挂起和恢复实验一、实验目的二、API 对比三、注意点一FreeRTOS 列表 — 双向环形链表1、为什么不用数组任务数量不固定——你随时可能调xTaskCreate加一个任务、vTaskDelete删一个任务。数组长度要预先声明最大任务数写死就浪费写少了溢出。链表动态挂接——每创建一个任务就多一个节点删了就摘下来内存用量跟任务数量完全匹配。这正是 RTOS 需要的数据结构。2.列表的结构体原型typedef struct xLIST { listFIRST_LIST_INTEGRITY_CHECK_VALUE /* 校验值 */ volatile UBaseType_t uxNumberOfItems; /* 列表中的列表项数量 */ ListItem_t * configLIST_VOLATILE pxIndex /* 用于遍历列表项的指针 */ MiniListItem_t xListEnd /* ③ 哨兵节点迷你*/ listSECOND_LIST_INTEGRITY_CHECK_VALUE /* 校验值 */ } List_t;两个宏校验值默认不开忽略。3、三个成员逐个拆①uxNumberOfItems— 有几个排队的人记录列表里有几个列表项不含xListEnd它不算排队的。xTaskCreate时 1vTaskDelete时 -1。调度器判断这个优先级有没有任务就看这个值是不是 0。②pxIndex— 一根指针用来遍历调度器看同优先级任务时不能一直挑第一个跑会饿死后面的。pxIndex指向下一个该跑的每次时间片轮转就往后挪一个节点。这就是时间片轮转的实现。③xListEnd— 永远站在末尾的哨兵它是一个迷你列表项——不是真正的任务节点。它的作用标记链表终点也是起点因为环形插入新节点时插在它前面尾插法永远不摘掉、不计入uxNumberOfItems4、画出来List_t就绪列表[2] ┌─────────────────────┐ │ uxNumberOfItems 3 │ │ pxIndex ─────────┐ │ │ xListEnd ← 哨兵 │ │ └──────────────────┼──┘ │ ┌──────────────┘ │ 哨兵 列表项1(TaskA) 列表项2(TaskB) 列表项3(TaskC) ▼ ▲ ▲ │ ▲ │ ▲ │ ├───┤ prev ←─────┤ │ ←────── prev┤ │ ←────── prev┤ │ │哨兵│ │ └──→ next ───┤ └──→ next ───┤ └──→ next ──┐ ├───┤ ├───┤ ├───┤ ├───┤ │ └───┘ └───┘ └───┘ └───┘ │ ↑ │ └──────────────────────────────────────────────────────────────┘ 环形最后一项的 next 回到哨兵 哨兵的 prev 指向最后一项5、哨兵存在的意义没有哨兵的单链表插入头尾要判 NULL环形链表不带哨兵要判断是不是回到了起点。有哨兵之后哨兵 是最后一项哨兵不是排队的人它是一个固定的路标——插在环形链表的第一个人的前面、最后一个人的后面让环变成一个永不 NULL 的闭环。next 指向队首prev 指向队尾 因此 xListEnd.prev 指向队尾 xListEnd.next 指向队首 没有哨兵的单链表插入头尾要判 NULL环形链表不带哨兵要判断是不是回到了起点。有哨兵之后 // 插入新列表项——永远是插在哨兵前面尾插 newItem-prev xListEnd.prev; // 新节点的前驱 现在的尾 newItem-next xListEnd; // 新节点的后继 哨兵 xListEnd.prev-next newItem; // 原来的尾 → 新节点 xListEnd.prev newItem; // 哨兵的前驱 → 新节点 // 不用判空不用判是不是第一个永远一样 哨兵是链表的润滑剂——有它在所有插入删除操作都不需要特殊判断。哨兵是链表的润滑剂——有它在所有插入删除操作都不需要特殊判断告诉新朋友之前的队尾是谁newItem-prev xListEnd.prev告诉新朋友你后面就是哨兵了newItem-next xListEnd告诉原来的队尾你后面不是哨兵了是新来的xListEnd.prev-next newItem告诉哨兵新来的变成队尾了xListEnd.prev newItem先 3 后 4步骤 3 要读xListEnd.prev原来的队尾地址步骤 4 会把它改掉。一旦先改掉步骤 3 拿不到原来的队尾链就断了。6列表下标的意义pxReadyTasksLists[2]是一个数组下标——所有优先级为 2 的任务都串在这条链表里排队。uxNumberOfItems存的是这条链表上有几个真实任务。pxReadyTasksLists[0] → 优先级 0 任务空闲任务的链表 pxReadyTasksLists[1] → 优先级 1 任务的链表 pxReadyTasksLists[2] → 优先级 2 任务的链表 ← task1 在这 pxReadyTasksLists[3] → 优先级 3 任务的链表 ← task2 在这 pxReadyTasksLists[4] → 优先级 4 任务的链表 ← task3 在这 ... pxReadyTasksLists[31]→ 优先级 31 任务的链表数组长度 configMAX_PRIORITIES 32。每个槽一个链表调度器从高到低扫——先看 [31] 有没有人有就挑队首跑。二FreeRTOS 列表项1结构体源码struct xLIST_ITEM { listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /* 用于检测列表项的数据完整性 */ configLIST_VOLATILE TickType_t xItemValue /* 列表项的值 */ struct xLIST_ITEM * configLIST_VOLATILE pxNext /* 下一个列表项 */ struct xLIST_ITEM * configLIST_VOLATILE pxPrevious /* 上一个列表项 */ void * pvOwner /* 列表项的拥有者 */ struct xLIST * configLIST_VOLATILE pxContainer /* 列表项所在列表 */ listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE /* 用于检测列表项的数据完整性 */ }; typedef struct xLIST_ITEM ListItem_t; struct xLIST_ITEM { TickType_t xItemValue; /* ① 排序用的值 */ struct xLIST_ITEM *pxNext; /* ② 指向下一个列表项 */ struct xLIST_ITEM *pxPrevious; /* ③ 指向上一个列表项 */ void *pvOwner; /* ④ 谁拥有这个列表项 */ struct xLIST *pxContainer; /* ⑤ 这个列表项挂在哪条链表 */ }; typedef struct xLIST_ITEM ListItem_t;两个校验宏默认关忽略我们只看①~⑤ 几项2、五个成员逐个拆①xItemValue— 排序键// FreeRTOS 内部定义tasks.c static List_t pxReadyTasksLists[configMAX_PRIORITIES]; // 数组32 条链表 static List_t xDelayedTaskList1; // 一条链表 static List_t xDelayedTaskList2; // 一条链表辅助 static List_t xSuspendedTaskList; // 一条链表 // 类型全是 List_t跟 int a, b, c 声明三个 int 一样32 个就绪链表 2 个延时链表 1 个挂起链表全是一个结构体类型。区别只是xItemValue排序方式不同——就绪链表按优先级倒序、延时链表按唤醒时间升序、挂起链表不排序。按这个值升序排列。不同的链表用不同的值排序链表xItemValue 存的是效果pxReadyTasksLists[x]任务优先级高优先级在队尾逆序队首先跑xDelayedTaskList唤醒时间xTickCount delay谁先到期谁排前面xSuspendedTaskList不用排序无意义挂起了排不排无所谓延时列表最直观现在 xTickCount 100 taskA 调 vTaskDelay(50)xItemValue 100 50 150 taskB 调 vTaskDelay(30)xItemValue 100 30 130 taskC 调 vTaskDelay(80)xItemValue 100 80 180 插入 xDelayedTaskList 后按 xItemValue 升序排队 队首(130) → 队尾(180) taskB(130) → taskA(150) → taskC(180) → 哨兵 最先醒 ↑ ↑ 最后醒SysTick 每 1ms 只看队首队首.xItemValue ≤ xTickCount 了没到了就摘下来插回就绪列表。不用扫整条链表——只有队首到时间了后面的肯定还没到。②③pxNext/pxPrevious— 双向环的指针就是你刚才学的那四条线。next指向后一个节点prev指向前一个。环形——最后一个节点的next指向哨兵哨兵的next指向对头。④pvOwner— 谁拥有这个列表项指向包含这个列表项的对象。对于状态列表项xStateListItempvOwner指向这个任务的TCB。pxReadyTasksLists[2] → 某列表项 → pvOwner → TCB(task1) ├─ pcTaskName task1 ├─ uxPriority 2 └─ pxTopOfStack ...调度器通过列表项找到 TCB再通过 TCB 找到任务栈——全部靠pvOwner这条链。⑤pxContainer— 这个列表项挂在哪个链表指向包含这条列表项的列表List_t。xStateListItem.pxContainer pxReadyTasksLists[2];摘除时用来检查——如果pxContainer NULL说明这个节点已经被摘掉了再次摘除会炸FreeRTOS 用这个做防御。3、每一列排队的人身上带了什么列表项就像每个学生胸前夹着的号码牌 ├─ xItemValue → 号码本身按号排队 ├─ pxNext/prev → 告诉前后的人你在哪 ├─ pvOwner → 指向学生本人TCB| 调度器通过号码牌找到你 └─ pxContainer → 哪个班级的队列 | 防止插错队4、跟我们之前学的 TCB 连起来// TCB 里嵌了列表项——任务到处排队都靠它 typedef struct tskTaskControlBlock { ListItem_t xStateListItem; // 状态排队就绪/阻塞/挂起 ListItem_t xEventListItem; // 事件排队等队列/等信号量 ... } TCB_t; // 调度器扫就绪列表时 ListItem_t *node pxReadyTasksLists[3].xListEnd.next; // 取队首 TCB_t *task (TCB_t *)node-pvOwner; // 通过 pvOwner 找到任务 // 然后从 TCB 读栈指针 → PendSV → 跑这个任务三迷你列表项1结构体定义struct xMINI_LIST_ITEM { listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /* 用于检测数据完整性 */ configLIST_VOLATILE TickType_t xItemValue; /* ①列表项的值哨兵固定填 0xFFFFFFFF */ struct xLIST_ITEM * configLIST_VOLATILE pxNext; /* ②上一个列表项注图中注释可能为笔误实际应为“下一个”需结合双向链表逻辑修正 */ struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /*③ 下一个列表项注图中注释可能为笔误实际应为“上一个”需结合双向链表逻辑修正 */ }; typedef struct xMINI_LIST_ITEM MiniListItem_t;1个校验宏默认关忽略我们看①~③它是 List_t 里的xListEnd——那个永远站岗的哨兵。省掉 pvOwner 和 pxContainer 是因为一个哨兵只需要知道前后是谁不需要知道自己是哪个任务、自己在哪个列表——它就不是任务。省掉一点内存。四列表和列表项的关系1、空链表初始状态List_t 刚创建时里面一个人都没有 uxNumberOfItems 0 pxIndex ──────────────┐ xListEnd │ xItemValue 0xFFFFFFFF ← 比所有真实任务的值都大 pxNext ────┐ │ pxPrevious ─│───┐ │ │ │ │ ▼ ▼ ▼ 哨兵自己指自己自闭环 ┌───────────┐ │ pxNext ───→ 自己 │ pxPrevious ─→ 自己 └───────────┘2、插入列表项 1xItemValue 10插入前哨兵自闭环 插入后 哨兵 列表项1(10) pxNext ────→ 列表项1 pxNext ────→ 哨兵 pxPrev ←──── 列表项1 pxPrev ←──── 哨兵 uxNumberOfItems 1 pxIndex → 列表项1 等价于 哨兵 ←→ 列表项1 ←→ 哨兵3、再插入列表项 2xItemValue 20升序排列2010插在列表项1后面哨兵 列表项1(10) 列表项2(20) pxNext ────────→ 列表项1 pxNext ────→ 列表项2 pxNext ────→ 哨兵 pxPrev ←─────── 列表项2 pxPrev ←── 哨兵 pxPrev ←── 列表项1 遍历顺序哨兵 → 列表项1(10) → 列表项2(20) → 哨兵回到起点 uxNumberOfItems 2 pxIndex → 列表项1 可视化 ┌→ 哨兵 ←─────────────┐ │ ↕ │ │ 列表项1(10) │ │ ↕ │ └── 列表项2(20) ──────┘4、哨兵的 xItemValue 为什么是 0xFFFFFFFF升序排列的列表插入算法是从头往后扫while (新节点的值 当前节点的值) 继续往后走; // 遇到比它大的节点 → 插在这个节点前面哨兵的值是最大值0xFFFFFFFF4294967295任何真实任务的值都比它小。所以循环到了哨兵一定停新节点永远插入在哨兵前面——这正是尾插法。哨兵用一个极大值把自己变成链表的逻辑终点。五FreeRTOS 列表 — 完整梳理1、为什么用链表而不用数组任务数量随时变——创建就加、删除就减。链表动态插拔内存按需分配。数组要预声明长度写死浪费写小溢出。RTOS 核心数据结构必然是链表。2、三种结构体结构体作用实例List_t链表本身带头结点pxReadyTasksLists[32]、xDelayedTaskList、xSuspendedTaskListListItem_t完整列表项嵌在 TCB 里TCB.xStateListItem、TCB.xEventListItemMiniListItem_t精简列表项就是哨兵List_t.xListEnd3、List_t列表— 3 个成员typedef struct xLIST { UBaseType_t uxNumberOfItems; /* ① 有几个真实任务在排队 */ ListItem_t *pxIndex; /* ② 遍历指针指向下一个该跑的 */ MiniListItem_t xListEnd; /* ③ 哨兵永远站末尾 */ } List_t;成员作用uxNumberOfItems不计哨兵。创建 1删除 -1调度器凭此判空pxIndex时间片轮转用——指向下一个该跑的跑完就往后挪xListEnd哨兵永远在环里。next指向队首prev指向队尾4、ListItem_t列表项— 5 个成员struct xLIST_ITEM { TickType_t xItemValue; /* ① 排序键——按此值升序排队 */ struct xLIST_ITEM *pxNext; /* ② 指向下一个 */ struct xLIST_ITEM *pxPrevious; /* ③ 指向上一个 */ void *pvOwner; /* ④ 谁拥有它 → 指向 TCB */ struct xLIST *pxContainer; /* ⑤ 它挂在哪个列表里 */ };pvOwner是调度器的命脉——从列表项反查 TCB再反查任务栈PendSV 切过去。整条调用链全靠这个指针。xItemValue在不同列表里含义不同列表xItemValue排序方式pxReadyTasksLists[x]任务优先级高在前队首先跑xDelayedTaskList唤醒时间xTickCountdelay小在前谁先到期谁排前面xSuspendedTaskList不排序随便放pxContainer是防御——摘除时检查是不是从这条列表摘防二次摘除。5、MiniListItem_t哨兵— 精简版列表项struct xMINI_LIST_ITEM { TickType_t xItemValue; /* 固定 0xFFFFFFFF最大值 */ struct xLIST_ITEM *pxNext; /* 指向队首 */ struct xLIST_ITEM *pxPrevious; /* 指向队尾 */ };没有pvOwner和pxContainer——它不是任务不需要知道主子是谁。xItemValue 0xFFFFFFFF是升序链表的逻辑终点——插入算法从头扫遇到比新节点大的值就停到哨兵必然停因为是最大值。新节点永远插在哨兵前面——天然尾插法。6、插入操作——四步3 和 4 不能换// 尾插永远插在哨兵前面 newItem-prev xListEnd.prev; // ① 新朋友记住之前的队尾 newItem-next xListEnd; // ② 新朋友你后面是哨兵 xListEnd.prev-next newItem; // ③ 告诉原来的队尾你后面换人了 xListEnd.prev newItem; // ④ 哨兵以后队尾是他了步骤 3 要读xListEnd.prev原队尾地址步骤 4 会把它改掉。先 3 后 4不然链断。7、三者关系一张图pxReadyTasksLists[2]List_t ┌──────────────────────┐ │ uxNumberOfItems 2 │ │ pxIndex ──────────┐ │ │ xListEnd哨兵 │ │ │ next ─────────┐ │ │ │ prev ───────┐ │ │ │ └───────────────┼─┼─┼──┘ │ │ │ ┌───────────┘ │ └──────────────┐ ▼ ▼ ▼ 哨兵 列表项1 列表项2 xItemValue10 xItemValue20 pxNext → 项2 pxNext → 哨兵 pxPrev → 哨兵 pxPrev → 项1 pvOwner → TCB_A pvOwner → TCB_B pxContainer → pxContainer → ReadyLists[2] ReadyLists[2]调度器挑任务pxReadyTasksLists[3].xListEnd.next→ 取列表项 →pvOwner→ TCB → 栈指针 → PendSV → 跑。整条路径从列表开始以 TCB 结束。1pxReadyTasksLists[3].xListEnd.nextpxReadyTasksLists[3].xListEnd.next 优先级 3 的链表上排第一个的任务。不是从 31 往下扫。调度器用位图 CLZ 指令O(1) 直达最高优先级// 每个优先级有一个 bit 标记是否有人就绪 // 优先级 3 有人 → bit 3 1 // 优先级 5 有人 → bit 5 1 // 优先级 0~2 没人 → bit 0/1/2 0 // uxTopReadyPriority 位图0b...00101000 // ↑ bit5 是最高的 1 // __clz(位图) → 算出最高那位的编号 5 // 直接取 pxReadyTasksLists[5].xListEnd.next → 队首任务 ✅这就是之前configUSE_PORT_OPTIMISED_TASK_SELECTION 1做的事——用硬件 CLZ 指令一条指令找到最高优先级不循环。[3]是我举的例子实际代码会自动找到当前最高的那个数字。2列表项 排序列表自动帮你排好。插入时按xItemValue升序——谁的值最小谁站前面。// 三个任务优先级都是 3同时进 xDelayedTaskList taskA 延时 50msxItemValue 100 50 150 taskB 延时 30msxItemValue 100 30 130 taskC 延时 80msxItemValue 100 80 180 // 插入函数 vListInsert 逐个比较升序排列 队首: taskB(130) → taskA(150) → taskC(180) :队尾 · 哨兵 最先醒 ↑ ↑ 最后醒SysTick 只盯队首——队首没到时间后面的肯定也没到。B 醒了摘走A 变成新的队首继续判断。就绪列表的同优先级任务特殊处理——新来的插在队尾实现先就绪的先跑FIFO时间片轮转时才往后挪。六API① vListInitialise — 列表初始化void vListInitialise(List_t *pxList);让一个刚定义的List_t变成空环uxNumberOfItems 0pxIndex指向哨兵哨兵自己指自己。调用一次即可后续直接往里插节点。② vListInitialiseItem — 列表项初始化void vListInitialiseItem(ListItem_t *pxItem);把列表项的prev/next/pvOwner/pxContainer/xItemValue全部归零。插入链表前必须调。③ vListInsert — 升序插入void vListInsert(List_t *pxList, ListItem_t *pxNewItem);从头往后扫找到第一个xItemValue大于新节点的位置插在它前面。结果xItemValue 小的在前大的在后。延时队列靠这个排队——谁先到期谁排前面。④ vListInsertEnd — 尾插时间片轮转用void vListInsertEnd(List_t *pxList, ListItem_t *pxNewItem);不管 xItemValue直接插在pxIndex指向的节点后面。同优先级任务就绪时用——新来的排到同优先级队的末尾保证先就绪的先跑FIFO。⑤ uxListRemove — 摘除uint32_t uxListRemove(ListItem_t *pxItem);把列表项从它所属的链表上摘下来前后节点自动重连。返回摘除后链表还剩几个节点。不释放内存——只摘不删。任务从就绪变阻塞时调这个。使用示例List_t TestList; /* 测试列表含哨兵 xListEnd*/ // 创建了一个列表包括哨兵存在 .bss 段 ListItem_t ListItem1; /* 列表项 1xItemValue40 */ // 创建了一个列表项节点存在 .bss 段 ListItem_t ListItem2; /* 列表项 2xItemValue60 */ ListItem_t ListItem3; /* 列表项 3xItemValue50 */ vListInitialise(TestList); vListInitialiseItem(ListItem1); vListInsert(TestList, ListItem1); /* 升序插入 */ vListInsertEnd(TestList, ListItem2); /* 尾插插在 pxIndex 后面 */ uxListRemove(ListItem2); /* 摘除不会释放内存 */ 创建变量介绍 List_t是一个结构体 typedef struct xLIST { listFIRST_LIST_INTEGRITY_CHECK_VALUE /* Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */ configLIST_VOLATILE UBaseType_t uxNumberOfItems; ListItem_t * configLIST_VOLATILE pxIndex; /* Used to walk through the list. Points to the last item returned by a call to listGET_OWNER_OF_NEXT_ENTRY (). */ MiniListItem_t xListEnd; /* List item that contains the maximum possible item value meaning it is always at the end of the list and is therefore used as a marker. */ listSECOND_LIST_INTEGRITY_CHECK_VALUE /* Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */ } List_t; ListItem_t 也是结构体 struct xLIST_ITEM //先定义结构体类型 { listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /* Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */ configLIST_VOLATILE TickType_t xItemValue; /* The value being listed. In most cases this is used to sort the list in descending order. */ struct xLIST_ITEM * configLIST_VOLATILE pxNext; /* Pointer to the next ListItem_t in the list. */ struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /* Pointer to the previous ListItem_t in the list. */ void * pvOwner; /* Pointer to the object (normally a TCB) that contains the list item. There is therefore a two way link between the object containing the list item and the list item itself. */ void * configLIST_VOLATILE pvContainer; /* Pointer to the list in which this list item is placed (if any). */ listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE /* Set to a known value if configUSE_LIST_DATA_INTEGRITY_CHECK_BYTES is set to 1. */ }; typedef struct xLIST_ITEM ListItem_t; // 给这个类型起个别名 ListItem_t七FreeRTOS 挂起和恢复实验一、实验目的掌握 FreeRTOS 内核列表List_t和列表项ListItem_t的操作函数通过串口打印每一步的指针地址验证双向环形链表的插入、删除、尾插操作与理论分析一致。二、API 对比vListInsertvListInsertEnduxListRemove功能按xItemValue升序插入插在pxIndex指向节点前面从链表中摘除列表项参数(List_t*, ListItem_t*)(List_t*, ListItem_t*)(ListItem_t*)排序✅ 自动排序❌ 不排序纯位置插入—返回值无无剩余节点数对 pxIndex 影响不影响不影响仅以它为插入位锚点若 pxIndex 指向被删项则前移时间复杂度O(n)O(1)O(1)典型用途任务插入就绪列表按优先级排序任务加入延时列表尾部任务从当前状态列表移除辅助 APIAPI功能vListInitialise()初始化列表哨兵自指计数器归零vListInitialiseItem()初始化列表项pvContainer NULL三、注意点pxIndex 永远指向“当前遍历认为的最后一个节点也就是哨兵”修改指向以后比如我们实验代码中的 TestList.pxIndex ListItem1;TestList.pxIndex ListItem1; printf(/***** 第八步修改指针指向后尾插项2 *****/\r\n); vListInsertEnd(TestList, ListItem2); printf(pxIndex 0x%p (应为列表项1地址)\r\n, TestList.pxIndex); printf(哨兵.next 0x%p (应为列表项2地址)\r\n, TestList.xListEnd.pxNext); printf(Item1.next 0x%p (应为列表项3地址)\r\n, ListItem1.pxNext); printf(Item2.next 0x%p (应为列表项1地址)\r\n, ListItem2.pxNext); printf(Item3.next 0x%p (应为哨兵)\r\n, ListItem3.pxNext); printf(哨兵.prev 0x%p (应为列表项3地址)\r\n, TestList.xListEnd.pxPrevious); printf(/********* 结束 *********/\r\n\r\n); printf( 实验结束 \r\n);让他指向了列表项1 导致格局变成了xListEnd ⇄ ListItem2(60) ⇄ ListItem1(40) ⇄ ListItem3(50) ⇄ xListEnd 以ListItem1为最后节点在此之前插入ListItem2