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

内存暴涨700%背后的惊天真相:AI正在吞噬一切!能源·隐私·绿色三大维度深度拆解

内存暴涨700%背后的惊天真相:AI正在吞噬一切!能源·隐私·绿色三大维度深度拆解

🔥作者简介: 一个平凡而乐于分享的小比特,中南民族大学通信工程专业研究生,研究方向无线联邦学习 🎬擅长领域:驱动开发,嵌入式软件开发,BSP开发 ❄️作者主页:一个平凡而乐于分享的小比特的个人主页 ✨收录专栏:未来思考,本专栏结合当前国家战略和实时政治,对未来行业发展的思考 欢迎大家点赞 👍 收藏 ⭐ 加关注哦!💖💖 🔥内存暴涨700%背后的惊天真相:AI正在吞噬一切!能源·隐私·绿色三大维度深度拆解 |前言| 最近装机的小伙伴们欲哭无泪:DDR5内存价格一路狂飙,部分DRAM现货价格在过去一年暴涨近700% 。大家习惯性吐槽“厂商放火”、“产能不足”,但很少有人看到,这场涨价风暴的真正推手,是那只名为“AI”的巨兽。 当你还在为多花几百块钱买内存心疼时,国家正在西部荒漠建起一座座数据中心,科技巨头正在为“吃电怪兽”抢购每一颗芯片。2026年,大型科技公司的AI相关投资预计将达到6500亿美元,较去年增长约80% 。 今天,我们从能源供应、隐私安全、绿色AI 三个维度,结合东数西算、算电协同、

By Ne0inhk
AI实践(8)Skills技能

AI实践(8)Skills技能

AI实践(10)Skills技能 Author: Once Day Date: 2026年3月18日 一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦… 漫漫长路,有人对你微笑过嘛… 全系列文章可参考专栏: AI实践成长_Once-Day的博客-ZEEKLOG博客 参考文章:Prompt Engineering Guide提示词技巧 – Claude 中文 - Claude AI 开发技术社区Documentation - Claude API DocsOpenAI for developersSkills(技能) – Claude 中文 - Claude AI 开发技术社区模式库:把工程经验沉淀为 Skills – Claude 中文 - Claude AI 开发技术社区持续学习:把会话复盘沉淀成 Skills – Claude

By Ne0inhk
医疗AI场景下算法编程的深度解析(2026新生培训讲稿)(八)

医疗AI场景下算法编程的深度解析(2026新生培训讲稿)(八)

第15章 模型融合与集成策略 在机器学习竞赛和实际应用中,模型融合(Model Ensemble)是提升预测性能的利器。通过组合多个不同的基模型,集成策略能够综合各个模型的优势,抵消单个模型的偏差和方差,从而获得比任何单一模型更稳定、更准确的预测结果。在医疗AI领域,模型融合同样具有重要价值——面对复杂多模态的医疗数据,单一模型往往难以全面捕捉所有信息,而融合多个异质模型可以提升诊断的鲁棒性和准确性。本章将从集成学习的基本思想出发,系统介绍常见的模型融合方法,包括投票法、平均法、Stacking、Blending等,并通过实战案例展示如何构建融合模型来提升疾病预测性能。 15.1 集成学习的基本思想 集成学习(Ensemble Learning)的核心思想是“三个臭皮匠,顶个诸葛亮”——通过结合多个学习器来完成学习任务,通常可以获得比单一学习器更优越的泛化性能。根据个体学习器的生成方式,集成学习主要分为两大类: * Bagging:并行训练多个独立的基学习器,然后通过平均或投票进行结合。典型代表是随机森林。Bagging主要降低方差。 * Boosting:串行训练基学习

By Ne0inhk
人工智能:自然语言处理在教育领域的应用与实战

人工智能:自然语言处理在教育领域的应用与实战

人工智能:自然语言处理在教育领域的应用与实战 学习目标 💡 理解自然语言处理(NLP)在教育领域的应用场景和重要性 💡 掌握教育领域NLP应用的核心技术(如智能问答、作业批改、个性化学习) 💡 学会使用前沿模型(如BERT、GPT-3)进行教育文本分析 💡 理解教育领域的特殊挑战(如多学科知识、学生认知差异、数据隐私) 💡 通过实战项目,开发一个智能问答系统应用 重点内容 * 教育领域NLP应用的主要场景 * 核心技术(智能问答、作业批改、个性化学习) * 前沿模型(BERT、GPT-3)在教育领域的使用 * 教育领域的特殊挑战 * 实战项目:智能问答系统应用开发 一、教育领域NLP应用的主要场景 1.1 智能问答 1.1.1 智能问答的基本概念 智能问答是通过自然语言与用户进行交互,回答用户问题的程序。在教育领域,智能问答的主要应用场景包括: * 课程问答:回答课程相关的问题(如“什么是机器学习”

By Ne0inhk