在嵌入式设备的串口数据接收、Linux 内核的任务调度、单片机的环形缓冲区中,你很难看到 Python 的身影 —— 这些对性能、内存占用极致敏感的场景,正是 C 语言的主场。而循环链表与双向链表,作为 C 语言中 “轻量级数据结构” 的代表,解决了两大核心问题:
- 循环链表:用环形结构实现 “无溢出” 的数据缓存(如串口接收环形缓冲区,避免数组队列 “假溢出”)
- 双向链表:通过前后指针快速定位节点(如 Linux 内核的list_head结构,实现任务的快速插入 / 删除)
与 Python 的 “类封装” 不同,C 语言需用结构体 + 指针手动构建链表,虽多了内存管理的繁琐,却能让你彻底理解链表的底层逻辑。
二、C 语言循环链表:用结构体与指针构建 “环形数据链”
循环链表的核心是 “尾节点指针指向头节点”,C 语言中需先定义节点结构体,再通过指针操作实现增删改查,且必须手动管理内存(malloc分配、free释放),避免内存泄漏。
1. 节点结构体定义(核心基础)
用typedef简化结构体类型名,节点包含 “数据域” 和 “指针域”(指向自身类型的next指针):
#include <stdio.h>
#include <stdlib.h> // 包含malloc、free函数
// 定义循环链表节点结构体
typedef struct CircularNode {
int data; // 数据域(存储整型数据,可按需修改)
struct CircularNode *next; // 指针域(指向同类型节点)
} CircNode, *CircList; // CircNode=结构体名,CircList=结构体指针类型
2. 核心操作实现(附完整代码 + 解析)
(1)初始化链表:创建空链表(头节点为 NULL)
// 初始化循环链表,返回头节点指针
CircList circ_list_init() {
CircList head = NULL; // 头节点初始化为空
return head;
}
(2)判断链表是否为空
int circ_list_is_empty(CircList head) {
return head == NULL; // 头节点为NULL则为空
}
(3)遍历链表:注意终止条件(next == head)
C 语言遍历需用指针逐节点移动,终止条件不再是next == NULL,而是 “回到头节点”,同时需处理空链表避免野指针:
void circ_list_traverse(CircList head) {
if (circ_list_is_empty(head)) {
printf("循环链表为空\n");
return;
}
CircNode *current = head; // 从头部开始遍历
do {
printf("%d → ", current->data); // 打印当前节点数据
current = current->next; // 指针向后移动
} while (current != head); // 回到头节点则终止
printf("(回到头节点)\n");
}
(4)尾部插入节点:需找到尾节点并调整指针
插入分 “空链表” 和 “非空链表” 两种情况,空链表时新节点next指向自身(形成环):
// 尾部插入节点(数据为data),返回新的头节点
CircList circ_list_append(CircList head, int data) {
// 1. 分配新节点内存,判断是否分配成功(避免内存不足)
CircNode *new_node = (CircNode *)malloc(sizeof(CircNode));
if (new_node == NULL) {
printf("内存分配失败\n");
return head;
}
new_node->data = data; // 赋值数据
new_node->next = new_node; // 初始时自循环(避免野指针)
// 2. 处理空链表:新节点即为头节点
if (circ_list_is_empty(head)) {
head = new_node;
return head;
}
// 3. 非空链表:找到尾节点(next指向头节点的节点)
CircNode *tail = head;
while (tail->next != head) {
tail = tail->next;
}
// 调整指针:尾节点next指向新节点,新节点next指向头节点
tail->next = new_node;
new_node->next = head;
return head;
}
(5)删除指定值节点:需处理 3 种边界情况
删除时需注意:① 链表为空;② 删除头节点;③ 删除中间 / 尾节点;④ 链表只剩一个节点:
// 删除值为data的节点,返回新的头节点
CircList circ_list_delete(CircList head, int data) {
// 情况1:链表为空
if (circ_list_is_empty(head)) {
printf("链表为空,无法删除\n");
return head;
}
CircNode *current = head; // 当前节点
CircNode *prev = NULL; // 前驱节点
// 情况2:链表只有一个节点
if (head->next == head) {
if (head->data == data) {
free(head); // 释放节点内存
head = NULL;
}
return head;
}
// 情况3:遍历找待删除节点
while (current->next != head) {
prev = current;
current = current->next;
if (current->data == data) {
// 子情况3.1:删除头节点(current是头节点的下一个,需特殊处理)
if (prev == NULL) {
// 找到尾节点,让尾节点next指向新头节点
CircNode *tail = head;
while (tail->next != head) {
tail = tail->next;
}
tail->next = current;
free(head);
head = current;
} else {
// 子情况3.2:删除中间节点
prev->next = current->next;
free(current); // 必须释放内存,避免泄漏
}
return head;
}
}
// 情况4:待删除节点是尾节点(循环结束后current是尾节点)
if (current->data == data) {
prev->next = head;
free(current);
}
return head;
}
(6)释放链表内存:避免内存泄漏
C 语言无自动垃圾回收,需遍历所有节点逐一释放:
void circ_list_free(CircList *head) {
if (*head == NULL) return;
CircNode *current = (*head)->next;
CircNode *temp;
// 从第二个节点开始释放(避免头节点被提前释放)
while (current != *head) {
temp = current;
current = current->next;
free(temp);
}
// 最后释放头节点
free(*head);
*head = NULL; // 头节点置空,避免野指针
}
(7)测试代码:验证所有操作
int main() {
CircList clist = circ_list_init();
// 插入节点
clist = circ_list_append(clist, 10);
clist = circ_list_append(clist, 20);
clist = circ_list_append(clist, 30);
printf("插入后遍历:");
circ_list_traverse(clist); // 输出:10 → 20 → 30 → (回到头节点)
// 删除节点
clist = circ_list_delete(clist, 20);
printf("删除20后遍历:");
circ_list_traverse(clist); // 输出:10 → 30 → (回到头节点)
// 释放内存
circ_list_free(&clist);
printf("释放后遍历:");
circ_list_traverse(clist); // 输出:循环链表为空
return 0;
}
3. C 语言循环链表的典型场景
- 嵌入式串口接收:用环形缓冲区存储串口数据,避免数据溢出(如 STM32 的 USART 接收中断)
- 实时任务调度:多个任务按环形排列,调度器循环取任务执行(如 RTOS 中的任务队列)
- 共享资源池:如内存块池,用循环链表管理空闲内存块,快速分配 / 回收
三、C 语言双向链表:前后指针的 “双向导航” 实现
双向链表的节点需额外存储prev指针(指向前驱节点),操作时需同时维护prev和next的一致性,核心痛点是 “指针关联的正确性” 和 “内存释放的顺序”。
1. 节点结构体定义(多一个 prev 指针)
// 定义双向链表节点结构体
typedef struct DoublyNode {
int data; // 数据域
struct DoublyNode *prev; // 前驱指针(指向前一个节点)
struct DoublyNode *next; // 后继指针(指向后一个节点)
} DoubNode, *DoubList;
2. 核心操作实现(重点:双向指针维护)
(1)初始化与判空
DoubList doub_list_init() {
return NULL;
}
int doub_list_is_empty(DoubList head) {
return head == NULL;
}
(2)双向遍历:向前 + 向后
向后遍历从头部到尾部,向前遍历需先找到尾节点:
// 向后遍历(头→尾)
void doub_list_traverse_forward(DoubList head) {
if (doub_list_is_empty(head)) {
printf("双向链表为空\n");
return;
}
DoubNode *current = head;
while (current != NULL) {
printf("%d → ", current->data);
current = current->next;
}
printf("NULL\n");
}
// 向前遍历(尾→头)
void doub_list_traverse_backward(DoubList head) {
if (doub_list_is_empty(head)) {
printf("双向链表为空\n");
return;
}
// 先找到尾节点
DoubNode *tail = head;
while (tail->next != NULL) {
tail = tail->next;
}
// 从尾节点向前遍历
while (tail != NULL) {
printf("%d ← ", tail->data);
tail = tail->prev;
}
printf("NULL\n");
}
(3)头部插入节点:同时调整 prev 和 next
DoubList doub_list_insert_head(DoubList head, int data) {
// 分配新节点
DoubNode *new_node = (DoubNode *)malloc(sizeof(DoubNode));
if (new_node == NULL) {
printf("内存分配失败\n");
return head;
}
new_node->data = data;
new_node->prev = NULL; // 头部节点前驱为NULL
new_node->next = NULL;
// 空链表:新节点即为头节点
if (doub_list_is_empty(head)) {
head = new_node;
return head;
}
// 非空链表:调整指针
new_node->next = head; // 新节点next指向原头
head->prev = new_node; // 原头prev指向新节点
head = new_node; // 更新头节点
return head;
}
(4)尾部插入节点:需找到尾节点并关联双向指针
DoubList doub_list_insert_tail(DoubList head, int data) {
DoubNode *new_node = (DoubNode *)malloc(sizeof(DoubNode));
if (new_node == NULL) {
printf("内存分配失败\n");
return head;
}
new_node->data = data;
new_node->prev = NULL;
new_node->next = NULL;
if (doub_list_is_empty(head)) {
head = new_node;
return head;
}
// 找到尾节点
DoubNode *tail = head;
while (tail->next != NULL) {
tail = tail->next;
}
// 双向关联:尾节点next指向新节点,新节点prev指向尾节点
tail->next = new_node;
new_node->prev = tail;
return head;
}
(5)删除指定节点:同步维护 prev 和 next
删除时需处理 “头节点”“中间节点”“尾节点” 三种情况,且必须释放内存:
DoubList doub_list_delete(DoubList head, int data) {
if (doub_list_is_empty(head)) {
printf("链表为空,无法删除\n");
return head;
}
DoubNode *current = head;
// 遍历找待删除节点
while (current != NULL && current->data != data) {
current = current->next;
}
// 未找到节点
if (current == NULL) {
printf("未找到值为%d的节点\n", data);
return head;
}
// 情况1:删除头节点
if (current->prev == NULL) {
head = current->next; // 新头节点是当前节点的next
if (head != NULL) { // 若链表不止一个节点
head->prev = NULL;
}
free(current);
return head;
}
// 情况2:删除尾节点
if (current->next == NULL) {
current->prev->next = NULL;
free(current);
return head;
}
// 情况3:删除中间节点
current->prev->next = current->next; // 前驱的next指向后继
current->next->prev = current->prev; // 后继的prev指向前驱
free(current);
return head;
}
(6)释放内存:从头部逐节点释放
void doub_list_free(DoubList *head) {
if (*head == NULL) return;
DoubNode *current = *head;
DoubNode *temp;
while (current != NULL) {
temp = current;
current = current->next;
free(temp); // 释放当前节点
}
*head = NULL; // 头节点置空,避免野指针
}
(7)测试代码
int main() {
DoubList dlist = doub_list_init();
// 头部插入
dlist = doub_list_insert_head(dlist, 20);
dlist = doub_list_insert_head(dlist, 10);
// 尾部插入
dlist = doub_list_insert_tail(dlist, 30);
printf("向后遍历:");
doub_list_traverse_forward(dlist); // 10 → 20 → 30 → NULL
printf("向前遍历:");
doub_list_traverse_backward(dlist); // 30 ← 20 ← 10 ← NULL
// 删除节点
dlist = doub_list_delete(dlist, 20);
printf("删除20后向后遍历:");
doub_list_traverse_forward(dlist); // 10 → 30 → NULL
// 释放内存
doub_list_free(&dlist);
printf("释放后向后遍历:");
doub_list_traverse_forward(dlist); // 双向链表为空
return 0;
}
3. C 语言双向链表的典型场景
- Linux 内核:list_head结构体(双向循环链表)用于管理进程、文件描述符等
- 数据库索引:B + 树的叶子节点用双向链表连接,支持顺序查询
- 文本编辑器:光标移动(向前 / 向后定位字符)、撤销操作的历史记录
四、C 语言循环链表 vs 双向链表:关键差异与选择
|
对比维度 |
循环链表(单向) |
双向链表(非循环) |
|
指针操作 |
仅维护next,逻辑简单 |
需同步维护prev和next,易出错 |
|
内存开销 |
每个节点 1 个指针,开销小 |
每个节点 2 个指针,开销大 |
|
遍历效率 |
仅单向遍历,找前驱需 O (n) |
双向遍历,找前驱 O (1) |
|
内存管理难度 |
释放时需遍历到尾节点(避免断环) |
释放时直接从头部逐节点处理 |
|
典型场景 |
嵌入式环形缓冲区、RTOS 任务队列 |
Linux 内核对象管理、数据库索引 |
选择建议:
- 嵌入式 / 单片机开发(内存紧张、需循环缓存):选单向循环链表
- 系统编程 / 数据库(需双向查找、频繁删除):选双向链表
- 需兼顾循环与双向操作(如内核任务管理):选双向循环链表(扩展节点为prev+next,尾节点next指向头,头节点prev指向尾)
五、C 语言链表开发的避坑指南
- 野指针问题:节点初始化时next/prev需置为NULL或自指向,避免访问非法内存
- 内存泄漏:任何malloc分配的节点,必须在删除 / 释放链表时free
- 边界条件:空链表、单节点链表、删除头 / 尾节点的情况必须单独处理
- 指针传递:修改头节点的操作(如插入头节点、删除头节点)需用 “指针的指针”(CircList *),否则头节点值无法同步更新
六、总结:C 语言链表的核心价值
C 语言实现链表,本质是 “用指针操作内存、用结构体组织数据” 的过程 —— 没有 Python 的封装,却能让你直击数据结构的底层逻辑。无论是嵌入式的环形缓冲区,还是 Linux 内核的list_head,循环链表与双向链表的核心都是 “指针的正确关联” 与 “内存的高效管理”。
掌握这些实现,不仅能解决实际开发中的性能、内存问题,更能帮你理解操作系统、嵌入式系统的底层设计思想 —— 这正是 C 语言的魅力所在。
1670

被折叠的 条评论
为什么被折叠?



