FreeRTOS链表详解

一、先搞懂:链表到底是什么?

链表是一种动态存储数据的结构,核心是 “用指针把离散的节点串起来”,像一串糖葫芦:

  • 「节点」:每个糖葫芦(存储一个数据),包含两部分:
    1. 数据域:存储实际要保存的数据(比如数字、结构体、指针);
    2. 指针域:存储下一个(或上一个)节点的地址,相当于糖葫芦的竹签,把节点连起来。
  • 「链表」:所有节点通过指针域串联形成的整体,不需要连续的内存空间(和数组最大的区别)。

对比数组,你就能秒懂链表的核心优势:

特性数组(比如int arr[10]链表
内存分配连续内存块离散内存块(按需分配)
插入 / 删除需移动大量元素(效率低)只需改指针(效率高)
容量扩展固定大小(无法动态扩展)可动态添加节点(无上限)
访问方式下标直接访问(arr[3]必须从头遍历(顺序访问)

二、最基础的两种链表:单向链表 vs 双向链表

FreeRTOS 中用的是双向链表(方便正反遍历、插入删除),但先从单向链表入门,再过渡到双向链表更易理解。

1. 单向链表(入门必备)
  • 结构:每个节点只有一个指针,指向下一个节点,最后一个节点的指针为NULL(表示链表结束)。
  • 核心:只能从 “头节点” 往后遍历,不能反向查找。
(1)单向链表节点定义(C 语言代码)

c

运行

// 定义节点结构体:数据域 + 指针域 struct Node { int data; // 数据域:存储整数(可替换为任意类型,比如结构体) struct Node *next; // 指针域:指向同类型的下一个节点 }; // 为了书写方便,重定义类型名(类似FreeRTOS的typedef) typedef struct Node ListNode; 
(2)单向链表的核心操作(3 个最常用)
① 创建节点(申请内存 + 赋值)

c

运行

// 创建一个新节点,返回节点指针 ListNode* createNode(int data) { // 1. 动态分配内存(链表节点通常动态创建,按需分配) ListNode *newNode = (ListNode*)malloc(sizeof(ListNode)); // 2. 给节点赋值 newNode->data = data; // 数据域存值 newNode->next = NULL; // 新节点默认是最后一个,next指向NULL return newNode; }
② 插入节点(尾部插入,最简单)
  1. 找到链表的最后一个节点(最后一个节点的nextNULL);
  2. 把最后一个节点的next指针,指向新创建的节点;
  3. 新节点的next保持NULL(因为它现在是新的尾部)。
// 单链表节点定义 typedef struct Node { int data; // 数据域 struct Node *next; // 指针域:指向下一个节点 } ListNode; // 尾部插入节点:head是链表头指针的地址(二级指针) void insertNode(ListNode **head, int data) { // 步骤1:创建新节点 ListNode *newNode = createNode(data); // 步骤2:如果链表为空,新节点就是头节点 if (*head == NULL) { *head = newNode; return; } // 步骤3:遍历找到链表最后一个节点 ListNode *p = *head; while (p->next != NULL) { p = p->next; // 指针后移,直到找到最后一个节点 } // 步骤4:把新节点接在最后一个节点后面 p->next = newNode; } // 创建新节点(辅助函数) ListNode* createNode(int data) { ListNode *newNode = (ListNode*)malloc(sizeof(ListNode)); // 申请内存 newNode->data = data; // 给数据域赋值 newNode->next = NULL; // 新节点默认是尾部,next指向NULL return newNode; }
步骤 1:创建新节点(createNode函数)
  • 核心作用:申请一块内存,封装 “数据” 和 “指针”,形成一个独立的节点。
  • 关键代码解释:
    • (ListNode*)malloc(sizeof(ListNode)):向系统申请一块和ListNode结构体大小相同的内存,强制转换为ListNode*类型(因为malloc返回void*);
    • newNode->next = NULL:这是尾部插入的关键!新节点一开始是 “孤立” 的,它的next必须设为NULL,表示它暂时是 “最后一个节点”,后续接在链表尾部时才不会出错。

示意图(新节点创建后):plaintext

newNode: [data=传入的值] -> next = NULL 
步骤 2:判断链表是否为空(if (*head == NULL)
  • 核心逻辑:如果链表还没有任何节点(头指针*headNULL),新节点就直接成为 “头节点”。
  • 为什么用ListNode **head(二级指针)?
    • 因为要修改 “头指针本身” 的值(让它指向新节点)。如果用一级指针ListNode *head,函数内修改的只是head的副本,外部的头指针不会变,链表还是空的。
    • 简单说:要修改指针变量本身,就传它的地址(二级指针)。

示意图(链表为空时插入):plaintext

插入前:*head = NULL(链表空) 插入后:*head -> newNode: [data=值] -> next = NULL 
步骤 3:遍历找到链表尾部(while (p->next != NULL)
  • 核心目标:找到最后一个节点(nextNULL的节点)。
  • 逐行解释:
    • ListNode *p = *head:定义一个 “临时指针p”,从 “头节点” 开始遍历(p一开始指向头节点);
    • while (p->next != NULL):循环条件:只要pnext不是NULL,说明p不是最后一个节点,继续往后找;
    • p = p->nextp指针 “移动” 到下一个节点(相当于 “顺着糖葫芦的竹签往下走”)。
  • 示意图(遍历过程):假设链表已有节点:head -> 节点1 -> 节点2 -> NULL
    1. 初始:p = 节点1p->next = 节点2,不是NULL,进入循环);
    2. 第一次循环:p = p->next → p = 节点2p->next = NULL,循环结束);
    3. 最终:p指向最后一个节点(节点 2)。
步骤 4:连接新节点(p->next = newNode
  • 核心操作:把最后一个节点的next指针,指向新创建的节点,让新节点成为新的尾部。
  • 示意图(连接后):连接前:节点2 -> NULLnewNode -> NULL连接后:节点2 -> newNode -> NULL
  • 为什么新节点的next不用改?
    • 因为createNode中已经把newNode->next设为NULL,连接后它自然是最后一个节点,无需额外操作。
(3)单向链表使用示例
int main() { ListNode *head = NULL; // 链表头指针,初始为空 // 插入3个节点 insertNode(&head, 10); insertNode(&head, 20); insertNode(&head, 30); // 遍历链表,输出:10 20 30 traverseList(head); return 0; }
  1. 初始状态:ListNode *head = NULL(链表空);
  2. 插入第一个节点(data=10):
    • newNode[10] -> NULL
    • *head == NULL → *head = newNode
    • 链表:head -> [10] -> NULL
  3. 插入第二个节点(data=20):
    • newNode[20] -> NULL
    • *head != NULL → 定义p = headp指向 [10]);
    • p->next = NULL(循环不执行);
    • p->next = newNode → 链表:head -> [10] -> [20] -> NULL
  4. 插入第三个节点(data=30):
    • newNode[30] -> NULL
    • p = head(指向 [10]);
    • p->next = [20] != NULL → p = p->next(指向 [20]);
    • p->next = NULL(循环结束);
    • p->next = newNode → 链表:head -> [10] -> [20] -> [30] -> NULL

最容易踩的 2 个坑(重点提醒)

  1. 忘记给新节点的nextNULL:如果newNode->next是随机值(malloc 申请的内存未初始化),遍历的时候会陷入死循环;
  2. 用一级指针ListNode *head当参数:插入第一个节点时,head的副本被修改,外部head还是NULL,链表始终为空。

(2)双向链表的核心优势
  • 可以从表头往后遍历,也可以从表尾往前遍历;
  • 删除任意节点时,只需通过prevnext找到前后节点,直接修改指针即可(无需遍历找前驱)。

比如删除节点p

c

运行

// p是要删除的节点 p->prev->next = p->next; // 前节点的next指向后节点 p->next->prev = p->prev; // 后节点的prev指向前节点 free(p); // 释放节点内存
1. FreeRTOS 链表的特殊设计
  • 环形结构:没有 “NULL 结尾”,最后一个节点的next指向表头,表头的prev指向最后一个节点(方便循环遍历);
  • 根节点(xListEnd):不存储实际数据,仅作为链表的 “锚点”(方便插入删除操作,不用判断表头为空);
  • 节点带 “辅助值”(xItemValue):用于排序(比如延时列表按延时时间排序,就绪列表按优先级排序)。
2. 和通用双向链表的对应关系
通用双向链表FreeRTOS 链表(List)作用
节点数据域pvOwner指向节点的拥有者(比如 TCB)
节点前驱指针pxPrevious指向前一个列表项
节点后继指针pxNext指向后一个列表项
链表头xListEnd链表根节点(锚点)

比如 FreeRTOS 的就绪列表pxReadyTasksLists,就是一个数组,每个元素是一个双向环形链表,对应一个优先级的任务队列 —— 这就是多任务调度的基础!

四、链表的核心要点总结(必记)

  1. 节点是链表的基本单位:必须包含 “数据域” 和 “指针域”;
  2. 链表的核心是 “指针串联”:通过指针域把离散的内存块连起来,无需连续内存;
  3. 动态分配内存:链表节点通常用malloc创建(FreeRTOS 中也支持静态分配),按需扩展;
  4. 操作核心是 “指针移动”:遍历、插入、删除本质都是修改指针指向;
  5. FreeRTOS 的链表是 “增强版双向环形链表”:为任务管理、排序优化设计,本质还是链表的核心逻辑。

五、动手实践:写一个简单的双向链表(加深理解)

c

运行

#include <stdio.h> #include <stdlib.h> // 双向链表节点定义 typedef struct DoubleNode { int data; struct DoubleNode *prev; struct DoubleNode *next; } DoubleListNode; // 创建节点 DoubleListNode* createDoubleNode(int data) { DoubleListNode *newNode = (DoubleListNode*)malloc(sizeof(DoubleListNode)); newNode->data = data; newNode->prev = NULL; newNode->next = NULL; return newNode; } // 尾部插入节点 void insertDoubleNode(DoubleListNode **head, int data) { DoubleListNode *newNode = createDoubleNode(data); if (*head == NULL) { *head = newNode; return; } DoubleListNode *p = *head; while (p->next != NULL) { p = p->next; } p->next = newNode; newNode->prev = p; // 双向链表:新节点的prev指向最后一个节点 } // 遍历链表(正序) void traverseDoubleList(DoubleListNode *head) { DoubleListNode *p = head; while (p != NULL) { printf("%d ", p->data); p = p->next; } printf("\n"); } // 主函数测试 int main() { DoubleListNode *head = NULL; insertDoubleNode(&head, 100); insertDoubleNode(&head, 200); insertDoubleNode(&head, 300); traverseDoubleList(head); // 输出:100 200 300 return 0; }

Read more

三种适用于Web版IM(即时通讯)聊天信息的加密算法实现方案

三种适用于Web版IM(即时通讯)聊天信息的加密算法实现方案

文章目录 * **第一部分:引言与核心密码学概念** * **1.1 为什么IM需要端到端加密(E2EE)?** * **1.2 核心密码学概念与工具** * **第二部分:方案一:静态非对称加密(基础方案)** * **2.1 方案概述与流程** * **2.2 前端Vue实现(使用node-forge)** * **1. 安装依赖** * **2. 核心工具类 `crypto.js`** * **3. Vue组件中使用** * **2.3 后端Java实现(Spring Boot)** * **1. 实体类** * **2. Controller层** * **3. WebSocket配置** * **2.4 密钥管理、注册与登录集成** * **1. 用户注册/登录时生成密钥** * **2. 密钥设置页面** * **2.

By Ne0inhk
Flutter for OpenHarmony: Flutter 三方库 cached_query 为鸿蒙应用打造高性能声明式数据缓存系统(前端缓存终极方案)

Flutter for OpenHarmony: Flutter 三方库 cached_query 为鸿蒙应用打造高性能声明式数据缓存系统(前端缓存终极方案)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在进行 OpenHarmony 应用开发时,网络请求的响应速度直接决定了用户体验(体验 UX)。如果用户每次切换页面都必须等待加载动画,应用会显得非常低级。我们不仅需要处理异步数据请求,更需要一套精密的机制来解决以下痛点: 1. 自动缓存:第二次访问时应瞬间展示历史数据。 2. 过期失效(Stale-while-revalidate):在展示旧数据的同时,后台静默拉取新数据。 3. 无限滚动:简单地处理分页与数据追加内容逻辑。 cached_query 是一个类似于 Web 端 React Query 的 Dart 状态管理库。它专注于数据获取与同步,让你的鸿蒙应用具备顶级的数据缓存表现。 一、核心缓存驱动机制 cached_query 在内存与数据源之间建立了一层“智能感知”缓存。 数据过期/缺失 返回新数据 发射流

By Ne0inhk
【前端】Vue3+elementui+ts,TypeScript Promise<string>转string错误解析,习惯性请出DeepSeek来解答

【前端】Vue3+elementui+ts,TypeScript Promise<string>转string错误解析,习惯性请出DeepSeek来解答

🌹欢迎来到《小5讲堂》🌹 🌹这是《前端》系列文章,每篇文章将以博主理解的角度展开讲解。🌹 🌹温馨提示:博主能力有限,理解水平有限,若有不对之处望指正!🌹 目录 * 前言 * 报错信息 * DeepSeek解答 * 问题原因 * 解决方案 * 最佳实践 * 异步和同步 * 1. 同步(Synchronous)操作 * 示例:同步数据更新 * 2. 异步(Asynchronous)操作 * 示例 1:`setTimeout` * 示例 2:`async/await` * 3. Vue 3 的异步更新机制 * 如何等待 DOM 更新? * 4. 生命周期钩子中的异步 * 5. 总结 * 最佳实践 * 文章推荐 前言 好久没有写前端,

By Ne0inhk

满分高危来袭!CVE-2026-21962击穿Oracle WebLogic代理插件,无认证远程控服全解析

2026年1月20日,Oracle发布2026年度首个关键补丁更新(CPU Jan 2026),一次性修复了全产品线158个CVE漏洞、发布337个安全补丁,其中27个关键级漏洞占比8%,涉及13个核心CVE编号。而Oracle WebLogic Server代理插件中曝出的CVE-2026-21962漏洞,凭借CVSS 3.1满分10.0的评级、无认证远程利用、低攻击复杂度的特性,成为本次更新中最具威胁的漏洞,也让全球大量部署WebLogic中间件的企业陷入安全危机。该漏洞并非简单的权限绕过,而是可直接实现远程命令执行(RCE),攻击者仅需构造恶意HTTP请求,即可绕过所有安全校验直接控制目标服务器,窃取、篡改核心业务数据,甚至实现内网横向移动,其危害覆盖金融、政务、能源、电商等所有使用WebLogic代理插件的关键行业。本文将从漏洞背景、技术原理、利用现状、防护方案及行业安全启示等维度,进行专业、全面的深度解读,并结合WebLogic历史漏洞规律给出前瞻性防护建议,为企业筑牢安全防线。 一、漏洞核心背景:Oracle 2026首波更新,WebLogic成高危重灾区 Oracl

By Ne0inhk