双向链表作业实现与GDB调试实战
一、双向链表作业实现
1.1 作业需求回顾
本次作业基于已有的双向链表框架,需完成3个核心功能,同时确保代码可运行、无内存泄露:
- 双向链表销毁(支持valgrind内存泄露检测);
- 双向链表逆序操作;
- 单向链表特殊操作(不使用长度字段
clen):- 查找倒数第k个节点;
- 查找中间节点。
1.2 核心功能实现(关键代码)
(1)双向链表销毁(解决内存泄露)
c
// 头文件声明
int DestroyDouLinkList(DouLinkList **dl);
// 函数实现
int DestroyDouLinkList(DouLinkList **dl) {
if (NULL == dl || NULL == *dl) return 1;
DouLinkNode* curr = (*dl)->head;
DouLinkNode* next_node = NULL;
// 遍历释放所有节点
while (curr != NULL) {
next_node = curr->next; // 先保存下一个节点
free(curr); // 释放当前节点
curr = next_node; // 移动到下一个节点
}
free(*dl); // 释放链表结构体
*dl = NULL; // 置空指针,避免野指针
return 0;
}
核心要点 :用二级指针修改原链表地址,遍历释放所有节点后再释放链表本身,确保无内存残留,可通过valgrind检测验证。
(2)双向链表逆序(原地逆序,无额外空间)
c
int ReverseDouLinkList(DouLinkList *dl) {
if (NULL == dl || IsEmptyDouLinkList(dl) || dl->clen == 1) return 1;
DouLinkNode* curr = dl->head;
DouLinkNode* temp = NULL;
// 交换每个节点的prev和next指针
while (curr != NULL) {
temp = curr->prev;
curr->prev = curr->next;
curr->next = temp;
curr = curr->prev; // 移动到原下一个节点
}
dl->head = temp->prev; // 头节点更新为原尾节点
return 0;
}
核心思路 :通过遍历交换每个节点的prev和next方向,最后更新头节点,时间复杂度O(n),空间复杂度O(1)。
(3)单向链表特殊节点查找(快慢指针法)
c
// 查找倒数第k个节点
SinLinkNode* FindKthFromTail(SinLinkList *sl, int k) {
if (NULL == sl || NULL == sl->head || k <= 0) return NULL;
SinLinkNode* fast = sl->head;
SinLinkNode* slow = sl->head;
// 快指针先走k步
for (int i = 0; i < k; i++) {
if (fast == NULL) return NULL; // k大于链表长度
fast = fast->next;
}
// 快慢指针同步走,快指针到尾则慢指针为目标
while (fast != NULL) {
fast = fast->next;
slow = slow->next;
}
return slow;
}
// 查找中间节点
SinLinkNode* FindMiddleNode(SinLinkList *sl) {
if (NULL == sl || NULL == sl->head) return NULL;
SinLinkNode* fast = sl->head;
SinLinkNode* slow = sl->head;
// 快指针走2步,慢指针走1步
while (fast->next != NULL && fast->next->next != NULL) {
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
核心技巧 :不依赖长度字段clen,用快慢指针实现O(n)时间复杂度,避免二次遍历。
1.3 代码编译与运行
编译命令(添加调试信息 -g)
bash
gcc main.c doulinklist.c -o link_test -Wall -g
-g:生成调试信息,供GDB使用;-Wall:显示所有警告,提前排查潜在问题。
运行结果
========== 原有双向链表功能测试 ==========
--------- 初始链表(正向)-------------
name:zhangsan age:20 sex:f score:80
name:lisi age:21 sex:m score:82
name:wangmazi age:22 sex:m score:85
...(省略中间输出)...
========== 作业2:双向链表逆序测试 ==========
逆序前(正向):
name:zhangsan age:20 sex:f score:80
name:lisi age:21 sex:m score:82
name:wangmazi age:22 sex:m score:85
name:guanerge age:50 sex:m score:89
逆序后(正向):
name:guanerge age:50 sex:m score:89
name:wangmazi age:22 sex:m score:85
name:lisi age:21 sex:m score:82
name:zhangsan age:20 sex:f score:80
...(省略后续输出)...
二、GDB调试工具实战
作为Linux开发必备工具,GDB能精准定位程序的逻辑错误、段错误、内存问题,以下结合今日链表代码,详解核心用法。
2.1 GDB常用命令速查表
| 命令 | 说明 | 实战示例(基于link_test程序) |
|---|---|---|
r/run |
运行程序,支持传递命令行参数 | r(直接运行)、r 10 20(传参) |
where/bt |
查看栈结构,显示函数调用关系(逆序) | 段错误后输入bt定位错误行 |
b/break |
设置断点(行号、函数名、多文件指定) | b 50、b ReverseDouLinkList、b doulinklist.c:80 |
n/next |
单步执行,跳过函数调用 | 循环中用n快速执行单步 |
s/step |
单步执行,进入自定义函数体 | s进入FindMiddleNode函数 |
p/print |
查看变量/指针/结构体内容 | p sl->head、p slow->data.name |
display |
持续显示变量,每次单步后自动刷新 | display slow->data.age |
c/continue |
继续运行到下一个断点 | 跳出循环时用c |
return |
强制跳出当前函数,回到调用处 | 无需执行完函数时用return |
l/list |
显示源代码(默认10行,支持指定行号) | l(显示main函数附近)、l 60 |
q/quit |
退出GDB调试 | q |
layout src |
开启源码可视化界面,更直观查看代码执行过程 | 调试时输入layout src |
2.2 调试核心步骤
(1)一般逻辑错误调试(以链表逆序功能为例)
假设逆序后链表输出异常,用GDB排查:
- 编译时添加
-g调试信息(已在1.3中完成); - 启动GDB:
gdb ./link_test; - 开启源码可视化:
layout src; - 设置断点(逆序函数入口):
b ReverseDouLinkList; - 运行程序:
r; - 单步进入函数:
s(进入ReverseDouLinkList内部); - 查看关键变量:
p curr->data.name(查看当前节点数据)、display curr->prev(持续监控prev指针); - 单步执行观察指针变化:
n逐行执行,发现指针交换逻辑错误时及时修正; - 继续运行到下一个断点:
c(若有多个断点); - 退出调试:
q。
(2)段错误调试(链表常见错误)
段错误(Segmentation fault)多由野指针、数组越界导致,调试步骤:
-
编译添加
-g:gcc main.c doulinklist.c -o link_test -g; -
启动GDB:
gdb ./link_test; -
直接运行程序(无需设断点):
r; -
程序触发段错误后,输入
bt查看栈结构:#0 0x0000555555555289 in InsertTailDouLinkList (dl=0x5555555592a0, data=0x7fffffffe150) at doulinklist.c:86 #1 0x00005555555548e6 in main (argc=1, argv=0x7fffffffe2a8) at main.c:15从输出可知,段错误发生在
doulinklist.c第86行; -
查看错误行代码:
l 86,发现原代码循环条件错误(tmp->next==NULL应改为tmp->next!=NULL); -
修正代码后重新编译运行,问题解决。
2.3 内存泄露检测(结合valgrind)
链表销毁功能需确保无内存泄露,配合valgrind验证:
-
编译程序(保留
-g):gcc main.c doulinklist.c -o link_test -g; -
运行valgrind:
valgrind --leak-check=full ./link_test; -
查看输出结果,若显示:
All heap blocks were freed -- no leaks are possible说明销毁功能正常,无内存泄露。