22、双向链表作业实现与GDB调试实战

双向链表作业实现与GDB调试实战

一、双向链表作业实现

1.1 作业需求回顾

本次作业基于已有的双向链表框架,需完成3个核心功能,同时确保代码可运行、无内存泄露:

  1. 双向链表销毁(支持valgrind内存泄露检测);
  2. 双向链表逆序操作;
  3. 单向链表特殊操作(不使用长度字段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;
}

核心思路 :通过遍历交换每个节点的prevnext方向,最后更新头节点,时间复杂度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 50b ReverseDouLinkListb doulinklist.c:80
n/next 单步执行,跳过函数调用 循环中用n快速执行单步
s/step 单步执行,进入自定义函数体 s进入FindMiddleNode函数
p/print 查看变量/指针/结构体内容 p sl->headp 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排查:

  1. 编译时添加 -g 调试信息(已在1.3中完成);
  2. 启动GDB:gdb ./link_test
  3. 开启源码可视化:layout src
  4. 设置断点(逆序函数入口):b ReverseDouLinkList
  5. 运行程序:r
  6. 单步进入函数:s(进入ReverseDouLinkList内部);
  7. 查看关键变量:p curr->data.name(查看当前节点数据)、display curr->prev(持续监控prev指针);
  8. 单步执行观察指针变化:n 逐行执行,发现指针交换逻辑错误时及时修正;
  9. 继续运行到下一个断点:c(若有多个断点);
  10. 退出调试:q
(2)段错误调试(链表常见错误)

段错误(Segmentation fault)多由野指针、数组越界导致,调试步骤:

  1. 编译添加 -ggcc main.c doulinklist.c -o link_test -g

  2. 启动GDB:gdb ./link_test

  3. 直接运行程序(无需设断点):r

  4. 程序触发段错误后,输入 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行;

  5. 查看错误行代码:l 86,发现原代码循环条件错误(tmp->next==NULL 应改为 tmp->next!=NULL);

  6. 修正代码后重新编译运行,问题解决。

2.3 内存泄露检测(结合valgrind)

链表销毁功能需确保无内存泄露,配合valgrind验证:

  1. 编译程序(保留 -g):gcc main.c doulinklist.c -o link_test -g

  2. 运行valgrind:valgrind --leak-check=full ./link_test

  3. 查看输出结果,若显示:

    复制代码
    All heap blocks were freed -- no leaks are possible

    说明销毁功能正常,无内存泄露。

相关推荐
dragoooon341 小时前
[优选算法专题九.链表 ——NO.53~54合并 K 个升序链表、 K 个一组翻转链表]
数据结构·算法·链表
xlq223227 小时前
22.多态(上)
开发语言·c++·算法
666HZ6667 小时前
C语言——高精度加法
c语言·开发语言·算法
天宇&嘘月7 小时前
Nginx的https搭建
网络·nginx·https
星释7 小时前
Rust 练习册 100:音乐音阶生成器
开发语言·后端·rust
_星辰大海乀7 小时前
IP 协议
服务器·网络·tcp/ip·nat·子网掩码·ip协议
风生u8 小时前
go进阶语法
开发语言·后端·golang
666HZ6668 小时前
C语言——黑店
c语言·开发语言
Gomiko8 小时前
JavaScript基础(八):函数
开发语言·javascript·ecmascript