嵌入式Linux学习 | 数据结构(Day06)全解:线性表 + 栈队列 + 静态库 / 动态库(原理 + 代码 + 编译实战 + 易错点)

本文系统梳理线性结构(顺序表、单链表、双链表、栈、队列)核心知识点,配套完整可运行 C 语言代码、经典作业深度讲解,再详细拆解 Linux 下静态库与动态库的创建、使用、区别及常见问题,补充异常处理、编译报错解决、实战拓展,全干货无冗余,适合数据结构实验、期末复习、Linux 库开发、CSDN 博客直接发布,代码可直接复制编译运行。


1. 线性结构(数据结构基础,重中之重)

线性结构是数据元素之间存在一对一线性关系 的结构,所有元素按顺序排成一条 "连续链条",无分支、无循环,是后续复杂数据结构(树、图)的基础。核心分为两大类存储方式:顺序存储链式存储,两者各有优劣,需根据实际场景选择。

1.1 顺序存储(顺序表)

核心定义与本质

  • 定义:用一段地址连续的内存空间 存储数据元素,数据元素在内存中按逻辑顺序依次排列,底层依赖数组实现。
  • 本质:顺序表 = 封装了操作的数组,数组本身只是存储容器,顺序表在此基础上封装了初始化、增删改查等标准操作,解决了数组操作不规范、易出错的问题。
  • 核心特点:
    1. 数据元素在内存中连续存放,物理地址与逻辑顺序一致;
    2. 支持随机访问(通过数组下标直接访问任意元素),访问效率极高,时间复杂度 O (1);
    3. 插入、删除操作需要移动大量元素(为了保持内存连续性),效率较低,时间复杂度 O (n);
    4. 预先分配固定内存,容量固定,超出容量需手动扩容(易造成内存浪费或溢出)。

顺序表完整 C 语言实现(优化注释 + 异常处理 + 测试用例)

cs 复制代码
// seqlist.c 顺序表完整实现(含增删改查+异常处理)
#include <stdio.h>
#include <stdlib.h>  // 用于扩容函数realloc

#define MAXSIZE 100  // 初始容量
#define INCREASE 50  // 扩容增量(当容量不足时,每次增加50个空间)

// 顺序表结构定义
typedef struct {
    int *data;       // 动态数组(支持扩容,比固定数组更实用)
    int length;      // 当前元素个数
    int capacity;    // 当前顺序表容量(区别于length,避免数组越界)
} SeqList;

/**
 * 1. 初始化顺序表
 * @param list 指向顺序表的指针
 * @return 1:初始化成功;0:初始化失败(内存分配失败)
 */
int InitSeqList(SeqList *list) {
    // 分配初始内存空间
    list->data = (int *)malloc(MAXSIZE * sizeof(int));
    if (list->data == NULL) {
        printf("【错误】顺序表初始化失败,内存分配失败!\n");
        return 0;
    }
    list->length = 0;          // 初始元素个数为0
    list->capacity = MAXSIZE;  // 初始容量为MAXSIZE
    printf("顺序表初始化成功,初始容量:%d\n", list->capacity);
    return 1;
}

/**
 * 2. 顺序表扩容(私有函数,内部调用,无需外部调用)
 * @param list 指向顺序表的指针
 * @return 1:扩容成功;0:扩容失败
 */
static int ExpandSeqList(SeqList *list) {
    // 重新分配内存,容量增加INCREASE
    int *newData = (int *)realloc(list->data, (list->capacity + INCREASE) * sizeof(int));
    if (newData == NULL) {
        printf("【错误】顺序表扩容失败,内存分配失败!\n");
        return 0;
    }
    list->data = newData;                  // 更新数据指针
    list->capacity += INCREASE;            // 更新容量
    printf("顺序表扩容成功,当前容量:%d\n", list->capacity);
    return 1;
}

/**
 * 3. 尾插法添加元素(最常用)
 * @param list 指向顺序表的指针
 * @param val 要添加的元素值
 * @return 1:添加成功;0:添加失败
 */
int InsertSeqList(SeqList *list, int val) {
    // 先判断容量是否充足,不足则扩容
    if (list->length >= list->capacity) {
        if (!ExpandSeqList(list)) {
            return 0;  // 扩容失败,添加失败
        }
    }
    list->data[list->length++] = val;  // 尾部添加元素,长度+1
    printf("元素 %d 尾插成功,当前元素个数:%d\n", val, list->length);
    return 1;
}

/**
 * 4. 按下标删除元素
 * @param list 指向顺序表的指针
 * @param index 要删除的元素下标(0 <= index < length)
 * @return 1:删除成功;0:删除失败(下标非法或顺序表为空)
 */
int DeleteSeqList(SeqList *list, int index) {
    // 校验下标合法性
    if (index < 0 || index >= list->length) {
        printf("【错误】删除失败,下标非法(合法下标:0~%d)\n", list->length - 1);
        return 0;
    }
    // 移动元素,覆盖要删除的元素(从index+1开始,依次前移)
    for (int i = index; i < list->length - 1; i++) {
        list->data[i] = list->data[i + 1];
    }
    list->length--;  // 元素个数-1
    printf("下标 %d 元素删除成功,当前元素个数:%d\n", index, list->length);
    return 1;
}

/**
 * 5. 按值查找元素(返回第一个匹配的下标)
 * @param list 指向顺序表的指针
 * @param val 要查找的元素值
 * @return 找到:返回下标;未找到:返回-1
 */
int SearchSeqList(SeqList *list, int val) {
    for (int i = 0; i < list->length; i++) {
        if (list->data[i] == val) {
            return i;  // 找到,返回下标
        }
    }
    return -1;  // 未找到
}

/**
 * 6. 遍历顺序表
 * @param list 指向顺序表的指针
 */
void ShowSeqList(SeqList *list) {
    if (list->length == 0) {
        printf("顺序表为空,无元素可遍历!\n");
        return;
    }
    printf("顺序表元素(共%d个):", list->length);
    for (int i = 0; i < list->length; i++) {
        printf("%d ", list->data[i]);
    }
    printf("\n");
}

/**
 * 7. 销毁顺序表(释放内存,避免内存泄漏)
 * @param list 指向顺序表的指针
 */
void DestroySeqList(SeqList *list) {
    free(list->data);  // 释放动态数组内存
    list->data = NULL; // 置空指针,避免野指针
    list->length = 0;
    list->capacity = 0;
    printf("顺序表已销毁,内存已释放!\n");
}

// 测试用例(可直接运行,验证所有操作)
int main() {
    SeqList list;
    // 初始化
    if (!InitSeqList(&list)) {
        return 1;
    }
    // 尾插元素
    InsertSeqList(&list, 10);
    InsertSeqList(&list, 20);
    InsertSeqList(&list, 30);
    InsertSeqList(&list, 40);
    ShowSeqList(&list);
    // 按下标删除
    DeleteSeqList(&list, 1);  // 删除下标1(元素20)
    ShowSeqList(&list);
    // 按值查找
    int index = SearchSeqList(&list, 30);
    if (index != -1) {
        printf("找到元素30,下标:%d\n", index);
    } else {
        printf("未找到元素30\n");
    }
    // 销毁顺序表
    DestroySeqList(&list);
    return 0;
}

运行示例

cs 复制代码
# 编译
gcc seqlist.c -o seqlist
# 运行
./seqlist
# 输出
顺序表初始化成功,初始容量:100
元素 10 尾插成功,当前元素个数:1
元素 20 尾插成功,当前元素个数:2
元素 30 尾插成功,当前元素个数:3
元素 40 尾插成功,当前元素个数:4
顺序表元素(共4个):10 20 30 40 
下标 1 元素删除成功,当前元素个数:3
顺序表元素(共3个):10 30 40 
找到元素30,下标:1
顺序表已销毁,内存已释放!

1.2 链式存储(链表)

核心定义与本质

  • 定义:无需连续内存空间,通过 "节点" 存储数据,每个节点包含两部分:数据域 (存储元素值)和指针域(存储下一个 / 上一个节点的地址),节点之间通过指针连接形成链条。
  • 本质:链表 = 节点的集合,节点在内存中可分散存储,通过指针维系逻辑顺序,解决了顺序表内存连续、扩容麻烦的问题。
  • 核心特点:
    1. 数据元素在内存中不连续,物理地址无序,逻辑顺序通过指针维系;
    2. 不支持随机访问,只能从头节点开始遍历,访问效率低,时间复杂度 O (n);
    3. 插入、删除操作无需移动元素,只需修改指针指向,效率高,时间复杂度 O (1);
    4. 动态扩容,无需预先分配内存,元素个数可灵活变化(只要内存足够);
    5. 每个节点需额外存储指针域,空间开销略大于顺序表。

(1)单链表(最基础、最常用)

  • 定义:每个节点只有一个指针域,仅指向下一个节点,尾节点指针域为 NULL,只能从头节点向后遍历,无法快速找到前驱节点。
  • 核心优势:结构简单、空间开销小,适合只需单向遍历、频繁增删的场景。
  • 完整 C 语言实现(优化注释 + 异常处理 + 测试用例)
cs 复制代码
// linklist.c 单链表完整实现(含增删改查+异常处理)
#include <stdio.h>
#include <stdlib.h>

// 单链表节点结构定义
typedef struct LinkNode {
    int data;                  // 数据域:存储元素值
    struct LinkNode *next;     // 指针域:指向后一个节点
} LinkNode, *LinkList;

/**
 * 1. 初始化单链表(带头节点,避免空指针异常,更规范)
 * @param head 指向单链表头指针的指针
 */
void InitLinkList(LinkList *head) {
    // 创建头节点(不存储实际数据,仅用于统一操作)
    *head = (LinkNode *)malloc(sizeof(LinkNode));
    if (*head == NULL) {
        printf("【错误】单链表初始化失败,内存分配失败!\n");
        exit(1);  // 内存分配失败,直接退出程序
    }
    (*head)->next = NULL;  // 头节点next置空,初始为空链表
    printf("单链表初始化成功(带头节点)\n");
}

/**
 * 2. 创建单个节点
 * @param val 节点数据值
 * @return 指向新节点的指针;NULL:创建失败
 */
LinkNode* CreateLinkNode(int val) {
    LinkNode *newNode = (LinkNode *)malloc(sizeof(LinkNode));
    if (newNode == NULL) {
        printf("【错误】节点创建失败,内存分配失败!\n");
        return NULL;
    }
    newNode->data = val;    // 赋值数据域
    newNode->next = NULL;   // 新节点next置空
    return newNode;
}

/**
 * 3. 尾插法添加节点(最常用,保持元素顺序)
 * @param head 单链表头指针
 * @param val 要添加的元素值
 * @return 1:添加成功;0:添加失败
 */
int InsertLinkListTail(LinkList head, int val) {
    LinkNode *newNode = CreateLinkNode(val);
    if (newNode == NULL) {
        return 0;
    }
    // 找到尾节点(next为NULL的节点)
    LinkNode *p = head;
    while (p->next != NULL) {
        p = p->next;
    }
    p->next = newNode;  // 尾节点next指向新节点
    printf("单链表尾插元素 %d 成功\n", val);
    return 1;
}

/**
 * 4. 头插法添加节点(插入速度最快,元素顺序与插入顺序相反)
 * @param head 单链表头指针
 * @param val 要添加的元素值
 * @return 1:添加成功;0:添加失败
 */
int InsertLinkListHead(LinkList head, int val) {
    LinkNode *newNode = CreateLinkNode(val);
    if (newNode == NULL) {
        return 0;
    }
    // 新节点next指向头节点的下一个节点
    newNode->next = head->next;
    // 头节点next指向新节点
    head->next = newNode;
    printf("单链表头插元素 %d 成功\n", val);
    return 1;
}

/**
 * 5. 按值删除节点(删除第一个匹配的节点)
 * @param head 单链表头指针
 * @param val 要删除的元素值
 * @return 1:删除成功;0:删除失败(未找到元素)
 */
int DeleteLinkListByVal(LinkList head, int val) {
    LinkNode *p = head;  // 前驱节点(当前节点的前一个节点)
    LinkNode *q = head->next;  // 当前节点(用于查找要删除的节点)
    
    // 遍历查找要删除的节点
    while (q != NULL && q->data != val) {
        p = q;          // 前驱节点后移
        q = q->next;    // 当前节点后移
    }
    
    if (q == NULL) {
        printf("【错误】删除失败,未找到元素 %d\n", val);
        return 0;
    }
    
    p->next = q->next;  // 前驱节点next指向当前节点的下一个节点
    free(q);            // 释放当前节点内存,避免内存泄漏
    q = NULL;           // 置空指针,避免野指针
    printf("单链表删除元素 %d 成功\n", val);
    return 1;
}

/**
 * 6. 遍历单链表
 * @param head 单链表头指针
 */
void ShowLinkList(LinkList head) {
    LinkNode *p = head->next;  // 跳过头节点,从第一个有效节点开始遍历
    if (p == NULL) {
        printf("单链表为空,无元素可遍历!\n");
        return;
    }
    printf("单链表元素:");
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;  // 节点后移
    }
    printf("\n");
}

/**
 * 7. 销毁单链表(释放所有节点内存)
 * @param head 指向单链表头指针的指针
 */
void DestroyLinkList(LinkList *head) {
    LinkNode *p = *head;  // 当前节点
    LinkNode *q;          // 临时节点,用于存储下一个节点
    
    // 遍历所有节点,逐一释放
    while (p != NULL) {
        q = p->next;  // 记录下一个节点
        free(p);      // 释放当前节点
        p = q;        // 当前节点后移
    }
    *head = NULL;  // 头指针置空,避免野指针
    printf("单链表已销毁,所有节点内存已释放!\n");
}

// 测试用例
int main() {
    LinkList head;
    // 初始化
    InitLinkList(&head);
    // 尾插元素
    InsertLinkListTail(head, 10);
    InsertLinkListTail(head, 20);
    InsertLinkListTail(head, 30);
    ShowLinkList(head);
    // 头插元素
    InsertLinkListHead(head, 5);
    ShowLinkList(head);
    // 按值删除
    DeleteLinkListByVal(head, 20);
    ShowLinkList(head);
    // 销毁单链表
    DestroyLinkList(&head);
    return 0;
}

(2)双链表(双向遍历,更灵活)

  • 定义:每个节点有两个指针域,一个指向前驱节点 (prev),一个指向后继节点(next),头节点 prev 为 NULL,尾节点 next 为 NULL,支持向前、向后双向遍历。
  • 核心优势:可快速找到前驱节点,插入、删除操作比单链表更灵活(无需遍历查找前驱节点),适合需要双向遍历、频繁增删的场景。
  • 完整 C 语言实现(优化注释 + 异常处理)
cs 复制代码
// double_linklist.c 双链表完整实现
#include <stdio.h>
#include <stdlib.h>

// 双链表节点结构定义
typedef struct DoubleLinkNode {
    int data;                          // 数据域
    struct DoubleLinkNode *prev;       // 前驱指针:指向前一个节点
    struct DoubleLinkNode *next;       // 后继指针:指向后一个节点
} DoubleLinkNode, *DoubleLinkList;

/**
 * 1. 初始化双链表(带头节点)
 * @param head 指向双链表头指针的指针
 */
void InitDoubleLinkList(DoubleLinkList *head) {
    *head = (DoubleLinkNode *)malloc(sizeof(DoubleLinkNode));
    if (*head == NULL) {
        printf("【错误】双链表初始化失败,内存分配失败!\n");
        exit(1);
    }
    (*head)->prev = NULL;  // 头节点前驱置空
    (*head)->next = NULL;  // 头节点后继置空
    printf("双链表初始化成功(带头节点)\n");
}

/**
 * 2. 创建双链表节点
 * @param val 节点数据值
 * @return 指向新节点的指针;NULL:创建失败
 */
DoubleLinkNode* CreateDoubleNode(int val) {
    DoubleLinkNode *newNode = (DoubleLinkNode *)malloc(sizeof(DoubleLinkNode));
    if (newNode == NULL) {
        printf("【错误】双链表节点创建失败,内存分配失败!\n");
        return NULL;
    }
    newNode->data = val;
    newNode->prev = NULL;
    newNode->next = NULL;
    return newNode;
}

/**
 * 3. 尾插法添加节点
 * @param head 双链表头指针
 * @param val 要添加的元素值
 * @return 1:添加成功;0:添加失败
 */
int InsertDoubleTail(DoubleLinkList head, int val) {
    DoubleLinkNode *newNode = CreateDoubleNode(val);
    if (newNode == NULL) {
        return 0;
    }
    // 找到尾节点
    DoubleLinkNode *p = head;
    while (p->next != NULL) {
        p = p->next;
    }
    // 尾节点与新节点建立双向关联
    p->next = newNode;
    newNode->prev = p;
    printf("双链表尾插元素 %d 成功\n", val);
    return 1;
}

/**
 * 4. 按值删除节点
 * @param head 双链表头指针
 * @param val 要删除的元素值
 * @return 1:删除成功;0:删除失败
 */
int DeleteDoubleByVal(DoubleLinkList head, int val) {
    DoubleLinkNode *p = head->next;  // 从第一个有效节点开始查找
    while (p != NULL && p->data != val) {
        p = p->next;
    }
    if (p == NULL) {
        printf("【错误】删除失败,未找到元素 %d\n", val);
        return 0;
    }
    // 处理前驱节点和后继节点的关联
    p->prev->next = p->next;  // 前驱节点的next指向当前节点的后继
    if (p->next != NULL) {    // 若当前节点不是尾节点,处理后继节点的prev
        p->next->prev = p->prev;
    }
    free(p);  // 释放节点内存
    p = NULL;
    printf("双链表删除元素 %d 成功\n", val);
    return 1;
}

/**
 * 5. 双向遍历双链表
 * @param head 双链表头指针
 */
void ShowDoubleLinkList(DoubleLinkList head) {
    DoubleLinkNode *p = head->next;
    if (p == NULL) {
        printf("双链表为空!\n");
        return;
    }
    // 正向遍历(从前往后)
    printf("双链表正向遍历:");
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
    // 反向遍历(从后往前)
    p = head;
    while (p->next != NULL) {
        p = p->next;  // 找到尾节点
    }
    printf("双链表反向遍历:");
    while (p != head) {
        printf("%d ", p->data);
        p = p->prev;
    }
    printf("\n");
}

/**
 * 6. 销毁双链表
 * @param head 指向双链表头指针的指针
 */
void DestroyDoubleLinkList(DoubleLinkList *head) {
    DoubleLinkNode *p = *head;
    DoubleLinkNode *q;
    while (p != NULL) {
        q = p->next;
        free(p);
        p = q;
    }
    *head = NULL;
    printf("双链表已销毁,所有节点内存已释放!\n");
}

// 测试用例
int main() {
    DoubleLinkList head;
    InitDoubleLinkList(&head);
    InsertDoubleTail(head, 10);
    InsertDoubleTail(head, 20);
    InsertDoubleTail(head, 30);
    ShowDoubleLinkList(head);
    DeleteDoubleByVal(head, 20);
    ShowDoubleLinkList(head);
    DestroyDoubleLinkList(&head);
    return 0;
}

单链表与双链表对比(补充细节,便于理解和选择)

对比维度 单链表 双链表
指针域 1 个(仅 next) 2 个(prev + next)
遍历方向 仅正向(从头到尾) 正向、反向均可
查找前驱节点 需从头遍历,时间复杂度 O (n) 直接通过 prev 指针,时间复杂度 O (1)
插入 / 删除效率 需查找前驱节点(O (n)),实际操作 O (1) 无需查找前驱节点,整体 O (1)
空间开销 小(仅一个指针) 大(两个指针,额外占用内存)
适用场景 单向遍历、频繁增删(无需反向操作) 双向遍历、频繁增删(需快速找到前驱)
易错点 尾节点 next 未置空、删除时未释放内存 指针关联错误(prev/next 颠倒)、尾节点删除时漏处理 prev

1.3 特殊的线性结构(操作受限的线性表)

在线性表的基础上,限制插入和删除的位置,就形成了两种特殊的线性结构 ------ 栈和队列,它们本质还是线性表,只是操作规则更严格,广泛应用于各类场景。

(1)栈(Stack)

  • 核心定义:只能在表的同一端进行插入和删除操作,不允许在其他位置操作。
  • 关键术语:
    1. 栈顶(Top):允许插入、删除的一端,栈顶元素是最后入栈、最先出栈的元素;
    2. 栈底(Bottom):固定不变的一端,栈底元素是最先入栈、最后出栈的元素;
    3. 入栈(Push):向栈顶添加元素,入栈后栈顶指针上移(顺序栈)或新增节点(链式栈);
    4. 出栈(Pop):从栈顶删除元素,出栈后栈顶指针下移(顺序栈)或删除节点(链式栈);
    5. 空栈:栈内无元素,栈顶指针指向栈底(顺序栈 top=-1,链式栈 head=NULL)。
  • 核心特性:LIFO(Last In First Out,后进先出),类比生活中的 "叠盘子"------ 最后放的盘子,最先被拿走;也类比 "函数调用栈"------ 最后调用的函数,最先执行完毕。
  • 存储方式:顺序栈(数组实现)、链式栈(单链表实现),前面章节已提供完整代码,此处补充核心应用场景。

(2)队列(Queue)

  • 核心定义:只能在表的一端插入,另一端删除,两端操作相互独立,不允许在中间位置操作。
  • 关键术语:
    1. 队尾(Rear):允许插入元素的一端,入队后队尾指针移动;
    2. 队头(Front):允许删除元素的一端,出队后队头指针移动;
    3. 入队(EnQueue):向队尾添加元素;
    4. 出队(DeQueue):从队头删除元素;
    5. 空队:队列内无元素,队头与队尾指针重合(循环队列 front=rear,链队列 front=rear=NULL)。
  • 核心特性:FIFO(First In First Out,先进先出),类比生活中的 "排队买票"------ 最先排队的人,最先买到票;也类比 "消息队列"------ 最先发送的消息,最先被处理。
  • 存储方式:循环队列(数组实现,解决普通顺序队列的假溢出问题)、链队列(单链表实现),前面章节已提供完整代码,此处补充核心应用场景。

栈与队列核心对比(补充细节,笔试高频)

特性 栈(Stack) 队列(Queue)
操作端 仅栈顶(一端操作) 队头(删除)、队尾(插入)(两端操作)
核心规则 LIFO(后进先出) FIFO(先进先出)
存储方式 顺序栈、链式栈 循环队列、链队列
关键判断 栈空(top==-1/NULL)、栈满(仅顺序栈) 队空(front==rear/NULL)、队满(仅循环队列)
插入 / 删除名称 入栈(Push)、出栈(Pop) 入队(EnQueue)、出队(DeQueue)
经典应用 进制转换、表达式转换、括号匹配、函数调用栈、浏览器前进后退 任务调度、消息缓冲、排队系统、广度优先搜索(BFS)、打印机队列
笔试考点 顺序栈 / 链式栈实现、表达式转换、括号匹配 循环队列实现、假溢出问题、队列应用

2. 经典作业讲解(深度拆解,覆盖实验 + 笔试)

结合线性结构核心知识点,补充 4 道经典作业题(含编程题、简答题),拆解思路、代码实现和易错点,直接适配实验报告和笔试复习。

作业 1:简答题(高频面试 / 期末题)------ 顺序表与链表的区别

核心答案(规范且简洁,适合答题)

  1. 存储方式:顺序表采用连续内存空间(数组实现);链表采用非连续内存空间(节点 + 指针实现)。
  2. 访问效率:顺序表支持随机访问(下标直接访问),时间复杂度 O (1);链表仅支持顺序访问(遍历),时间复杂度 O (n)。
  3. 增删效率:顺序表增删需移动大量元素,时间复杂度 O (n);链表增删只需修改指针,时间复杂度 O (1)(找到目标节点后)。
  4. 内存开销:顺序表预先分配固定内存,可能造成浪费或溢出;链表动态分配内存,无浪费,但每个节点需额外存储指针,空间开销略大。
  5. 适用场景:顺序表适合频繁查找、少增删的场景(如学生成绩查询);链表适合频繁增删、少查找的场景(如消息队列)。

作业 2:编程题(实验必做)------ 单链表反转(笔试高频)

题目要求

给定一个单链表,将其反转(如:1→2→3→4 → 4→3→2→1),要求不使用额外空间(原地反转)。

核心思路

利用三个指针(prev、curr、next),依次遍历链表,修改每个节点的 next 指针,使其指向前驱节点,逐步完成反转。

  1. 初始化 prev 为 NULL(反转后尾节点的 next 为 NULL),curr 指向头节点的下一个节点(第一个有效节点);
  2. 保存 curr 的下一个节点(next = curr->next),避免反转后丢失后续节点;
  3. 将 curr 的 next 指向 prev(完成当前节点的反转);
  4. prev 和 curr 依次后移(prev = curr,curr = next);
  5. 循环直至 curr 为 NULL,最后将头节点的 next 指向 prev(反转后的头节点)。

完整代码实现(集成到单链表代码中,可直接运行)

cs 复制代码
// 新增:单链表反转函数(原地反转)
void ReverseLinkList(LinkList head) {
    if (head->next == NULL || head->next->next == NULL) {
        // 空链表或只有一个节点,无需反转
        printf("单链表无需反转(空链表或仅一个节点)\n");
        return;
    }
    LinkNode *prev = NULL;    // 前驱节点
    LinkNode *curr = head->next;  // 当前节点
    LinkNode *next = NULL;    // 后继节点(保存下一个节点)
    
    while (curr != NULL) {
        next = curr->next;    // 保存当前节点的下一个节点
        curr->next = prev;    // 反转当前节点的指针
        prev = curr;          // 前驱节点后移
        curr = next;          // 当前节点后移
    }
    head->next = prev;        // 头节点指向反转后的第一个节点
    printf("单链表反转成功\n");
}

// 测试反转功能(在main函数中添加)
// ReverseLinkList(head);
// ShowLinkList(head);

运行示例(新增反转测试)

cs 复制代码
# 新增输出
单链表尾插元素 10 成功
单链表尾插元素 20 成功
单链表尾插元素 30 成功
单链表元素:10 20 30 
单链表反转成功
单链表元素:30 20 10 

作业 3:编程题(实验必做)------ 用栈实现括号匹配

题目要求

给定一个字符串(仅包含 '('、')'、'['、']'、'{'、'}'),判断字符串中的括号是否完全匹配(成对出现,且嵌套正确)。

核心思路

利用栈的 "后进先出" 特性,遍历字符串,遇到左括号入栈,遇到右括号则弹出栈顶元素,判断是否匹配,最终栈为空则匹配成功。

  1. 遍历字符串的每个字符;
  2. 若为左括号('('、'['、'{'),入栈;
  3. 若为右括号(')'、']'、'}'),判断栈是否为空(为空则不匹配),弹出栈顶元素,判断是否为对应的左括号;
  4. 遍历结束后,若栈为空,说明所有括号都匹配;否则不匹配。

完整代码实现

cs 复制代码
// bracket_match.c 栈实现括号匹配
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXSIZE 100

// 栈结构(存储括号字符)
typedef struct {
    char data[MAXSIZE];
    int top;
} Stack;

// 栈基础操作
void InitStack(Stack *s) { s->top = -1; }
int IsEmpty(Stack *s) { return s->top == -1; }
int Push(Stack *s, char c) {
    if (s->top == MAXSIZE - 1) {
        printf("【错误】栈满,括号匹配失败!\n");
        return 0;
    }
    s->data[++s->top] = c;
    return 1;
}
char Pop(Stack *s) {
    if (IsEmpty(s)) return '#';  // '#'作为异常标记
    return s->data[s->top--];
}

/**
 * 括号匹配核心函数
 * @param str 待匹配的括号字符串
 * @return 1:匹配成功;0:匹配失败
 */
int BracketMatch(char *str) {
    Stack s;
    InitStack(&s);
    int len = strlen(str);
    
    for (int i = 0; i < len; i++) {
        char c = str[i];
        // 左括号入栈
        if (c == '(' || c == '[' || c == '{') {
            Push(&s, c);
        }
        // 右括号,判断匹配
        else if (c == ')' || c == ']' || c == '}') {
            if (IsEmpty(&s)) {
                printf("【错误】括号不匹配:右括号多余(位置:%d)\n", i);
                return 0;
            }
            char top = Pop(&s);
            // 判断当前右括号与栈顶左括号是否匹配
            if ((c == ')' && top != '(') || 
                (c == ']' && top != '[') || 
                (c == '}' && top != '{')) {
                printf("【错误】括号不匹配:%c 与 %c 不匹配(位置:%d)\n", top, c, i);
                return 0;
            }
        }
        // 忽略非括号字符(若有)
        else {
            printf("警告:忽略非括号字符 '%c'(位置:%d)\n", c, i);
        }
    }
    
    // 遍历结束后,栈为空则匹配成功,否则左括号多余
    if (IsEmpty(&s)) {
        printf("括号匹配成功!\n");
        return 1;
    } else {
        printf("【错误】括号不匹配:左括号多余\n");
        return 0;
    }
}

// 测试用例
int main() {
    char str1[] = "({[]})";    // 匹配成功
    char str2[] = "({[)]}";    // 匹配失败(嵌套错误)
    char str3[] = "(()";       // 匹配失败(左括号多余)
    char str4[] = "())";       // 匹配失败(右括号多余)
    
    BracketMatch(str1);
    BracketMatch(str2);
    BracketMatch(str3);
    BracketMatch(str4);
    
    return 0;
}

作业 4:简答题 ------ 为什么说栈和队列是特殊的线性表?

核心答案(规范答题)

  1. 栈和队列的本质都是线性表,它们的数据元素之间都存在一对一的线性关系,符合线性表的定义;
  2. 它们与普通线性表的区别在于操作受限:普通线性表允许在任意位置进行插入和删除操作,而栈仅允许在一端操作,队列仅允许在两端分别进行插入和删除操作;
  3. 可以理解为:栈和队列是 "限制了操作范围的线性表",其底层存储结构(顺序、链式)与普通线性表完全一致,只是操作规则更严格。

3. 动态库与静态库(Linux C 必学,实战重点)

在实际开发中,我们会将常用的模块化功能(如前面的链表、栈、队列代码)封装成 "库",方便复用、共享,减少重复编码和编译开销,这是 Linux C 开发的基础技能,也是实验和笔试的高频考点。

3.1 什么是库(Library)

核心定义

库是将多个模块化的功能代码(函数、结构体)封装成的二进制文件,不暴露源码,仅提供调用接口(头文件),供其他程序调用。

核心作用(补充细节)

  1. 复用性:将常用功能(如链表操作、工具函数)封装成库,多个程序可直接调用,无需重复编写代码,提高开发效率;
  2. 共享性:多个程序可共用同一个库文件(尤其是动态库),节省内存空间,避免重复存储;
  3. 减少编译开销:库文件提前编译好,调用时无需重新编译库代码,仅编译主程序,大幅缩短编译时间;
  4. 隐藏实现细节:仅暴露调用接口(头文件),隐藏源码,提高代码安全性和可维护性(如第三方库)。

库的分类

Linux 下的库主要分为两类:静态库(.a 后缀)动态库(.so 后缀),两者的打包方式、使用方式、特性差异较大,需重点区分。

3.2 动态库(共享库,Dynamic Library)

核心特点(补充原理细节)

  • 定义:动态库是运行期间才动态加载到内存的库,编译主程序时,仅记录库的调用接口,不将库的代码编译进主程序的可执行文件中;
  • 文件后缀:Linux 下为 .so(Shared Object),Windows 下为 .dll
  • 加载机制:程序运行时,系统会通过动态链接器(ld-linux.so)找到对应的动态库文件,加载到内存中,供程序调用;若未找到动态库,程序会运行失败。

优势(补充细节)

  1. 可执行文件体积小:仅包含主程序代码和库的调用接口,不包含库的完整代码,节省磁盘空间;
  2. 支持热更新:库文件升级后,无需重新编译主程序,只需替换旧的库文件,程序运行时会自动加载新的库(适合需要频繁升级的场景,如服务器程序);
  3. 内存共享:多个程序可共用同一个动态库在内存中的副本,避免重复加载,节省内存空间(如多个程序都调用 printf 函数,只需加载一次 libc.so 库)。

缺点(补充细节)

  1. 移植性差:程序运行时必须依赖对应的动态库文件,若目标机器上没有该动态库,或库的版本不兼容,程序无法运行;
  2. 运行效率略低:程序运行时需要动态加载库,增加了一定的系统开销(相较于静态库);
  3. 版本依赖:若库升级后修改了接口(如函数名、参数),主程序会调用失败,需保证库的接口兼容性。

动态库的创建、使用(完整实战,补充报错解决)

第一步:准备源码文件(以链表、栈、队列为例)

假设我们有 3 个源码文件,包含常用的线性结构操作:

  • linklist.c:单链表实现(前面的完整代码)
  • stack.c:顺序栈实现(前面的完整代码)
  • queue.c:链队列实现(前面的完整代码)
第二步:创建动态库(一步到位命令)
cs 复制代码
# 命令解析:
# gcc:C语言编译器
# -fPIC:生成位置无关代码(Position-Independent Code),确保库可被多个程序共享加载
# --shared:生成动态库(共享库)
# -o libmylinear.so:指定动态库文件名(命名规范:lib+库名+.so)
# linklist.c stack.c queue.c:需要封装进库的源码文件
gcc -fPIC --shared -o libmylinear.so linklist.c stack.c queue.c
第三步:创建头文件(必须,供主程序调用)

创建 mylinear.h 头文件,声明库中的函数接口(主程序通过头文件调用库函数):

cs 复制代码
// mylinear.h 动态库/静态库头文件
#ifndef MYLINEAR_H
#define MYLINEAR_H

// 单链表接口声明
typedef struct LinkNode {
    int data;
    struct LinkNode *next;
} LinkNode, *LinkList;

void InitLinkList(LinkList *head);
int InsertLinkListTail(LinkList head, int val);
void ShowLinkList(LinkList head);
void DestroyLinkList(LinkList *head);

// 栈接口声明
typedef struct {
    int data[100];
    int top;
} Stack;

void Init(Stack *s);
int Push(Stack *s, int val);
int Pop(Stack *s, int *val);

// 队列接口声明
typedef struct QueueNode {
    int data;
    struct QueueNode *next;
} QueueNode;

typedef struct {
    QueueNode *front;
    QueueNode *rear;
} Queue;

void Init(Queue *q);
void EnQueue(Queue *q, int val);

#endif
第四步:使用动态库编译主程序

创建主程序 main.c,调用库中的函数:

cs 复制代码
// main.c 主程序,调用动态库/静态库中的函数
#include <stdio.h>
#include "mylinear.h"  // 包含库的头文件

int main() {
    // 调用单链表函数
    LinkList head;
    InitLinkList(&head);
    InsertLinkListTail(head, 10);
    InsertLinkListTail(head, 20);
    ShowLinkList(head);
    DestroyLinkList(&head);
    
    return 0;
}

编译主程序(关联动态库):

cs 复制代码
# 命令解析:
# gcc main.c:编译主程序源码
# -o main:指定可执行文件名
# -L.:指定库文件所在路径(. 表示当前目录)
# -lmylinear:指定要链接的库名(库名是mylinear,去掉前面的lib和后面的.so)
gcc main.c -o main -L. -lmylinear
第五步:运行主程序(重点,解决动态库加载失败问题)

直接运行可能会报错(动态库加载失败):

cs 复制代码
./main
# 报错信息(常见):
# ./main: error while loading shared libraries: libmylinear.so: cannot open shared object file: No such file or directory
报错原因

系统默认会在 /lib/usr/lib 等系统目录中查找动态库,当前动态库在当前目录,系统无法找到。

解决方法(3 种,任选一种)
  1. 临时指定动态库路径(仅当前终端有效):

    cs 复制代码
    export LD_LIBRARY_PATH=.  # . 表示当前目录,告诉系统在当前目录查找动态库
    ./main  # 此时可正常运行
  2. 永久指定动态库路径(所有终端有效):

    cs 复制代码
    # 编辑环境变量配置文件
    vi ~/.bashrc
    # 在文件末尾添加一行
    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
    # 生效配置
    source ~/.bashrc
    # 之后可直接运行
    ./main
  3. 将动态库复制到系统默认库目录(推荐,永久有效):

    cs 复制代码
    # 复制动态库到/usr/lib目录(需要root权限)
    sudo cp libmylinear.so /usr/lib
    # 之后可直接运行
    ./main
运行示例(成功)
cs 复制代码
./main
# 输出
单链表初始化成功(带头节点)
单链表尾插元素 10 成功
单链表尾插元素 20 成功
单链表元素:10 20 
单链表已销毁,所有节点内存已释放!

3.3 静态库(Static Library)

核心特点(补充原理细节)

  • 定义:静态库是编译时直接打包进主程序可执行文件的库,编译主程序时,会将静态库的所有代码复制到主程序的可执行文件中;
  • 文件后缀:Linux 下为 .a(Archive),Windows 下为 .lib
  • 加载机制:程序运行时,无需依赖静态库文件(因为库代码已被打包进可执行文件),可独立运行。

优势(补充细节)

  1. 移植性极强:可执行文件包含了库的所有代码,无需依赖外部库文件,发给其他机器可直接运行(无需安装对应的库);
  2. 运行效率高:无需动态加载库,减少了系统运行时的开销,运行速度比动态库快;
  3. 无版本依赖:库代码直接打包进可执行文件,即使外部静态库文件被删除、升级,也不影响可执行文件的运行。

缺点(补充细节)

  1. 可执行文件体积大:包含了主程序代码和静态库的完整代码,若多个程序都使用同一个静态库,会各自打包一份,造成磁盘空间和内存的浪费;
  2. 不支持热更新:若静态库升级,必须重新编译主程序(将新的库代码打包进可执行文件),否则主程序仍使用旧的库代码;
  3. 编译时间长:每次编译主程序,都需要重新编译静态库代码(或重新打包),编译开销大。

静态库的创建、使用(完整实战,补充细节)

第一步:准备源码文件(与动态库相同)

linklist.cstack.cqueue.c 和头文件 mylinear.h(同上)。

第二步:生成目标文件(.o 文件,编译源码但不链接)
cs 复制代码
# 命令解析:
# -c:仅编译源码,生成目标文件(.o),不进行链接
# -o linklist.o:指定目标文件名(与源码文件名对应)
gcc -c linklist.c -o linklist.o
gcc -c stack.c -o stack.o
gcc -c queue.c -o queue.o

执行后,会生成 3 个目标文件:linklist.ostack.oqueue.o

第三步:打包成静态库
cs 复制代码
# 命令解析:
# ar:归档命令,用于打包目标文件为静态库
# -c:创建静态库(若库文件不存在,则创建)
# -r:替换静态库中的目标文件(若目标文件已存在,更新)
# libmylinear.a:静态库文件名(命名规范:lib+库名+.a)
# linklist.o stack.o queue.o:需要打包进静态库的目标文件
ar -cr libmylinear.a linklist.o stack.o queue.o
第四步:使用静态库编译主程序

使用与动态库相同的主程序 main.c,编译命令略有差异(本质相同,编译器会自动区分静态库和动态库):

cs 复制代码
# 命令与动态库一致,编译器会优先链接静态库(若同时存在同名静态库和动态库)
gcc main.c -o main -L. -lmylinear
第五步:运行主程序(无需依赖库文件)

静态库的可执行文件可直接运行,无需指定库路径,因为库代码已被打包进可执行文件:

cs 复制代码
./main
# 输出(与动态库运行结果一致)
单链表初始化成功(带头节点)
单链表尾插元素 10 成功
单链表尾插元素 20 成功
单链表元素:10 20 
单链表已销毁,所有节点内存已释放!
关键细节
  • 若当前目录同时存在同名的静态库(libmylinear.a)和动态库(libmylinear.so),编译器会优先链接动态库

  • 若想强制链接静态库,需在编译命令中添加 -static 参数:

    cs 复制代码
    gcc main.c -o main -L. -lmylinear -static

3.4 静态库与动态库核心对比(补充细节,笔试高频)

对比维度 静态库(.a) 动态库(.so)
打包时机 编译时,将库代码复制到可执行文件 运行时,动态加载到内存,不复制到可执行文件
可执行文件大小 大(包含库完整代码) 小(仅包含库调用接口)
运行依赖 无(不依赖外部库文件) 有(必须存在对应的.so 文件)
移植性 极强(可独立运行,无需安装库) 差(需目标机器有对应动态库)
运行效率 高(无需动态加载,无额外开销) 略低(需动态加载,有系统开销)
库升级 需重新编译主程序(重新打包库代码) 无需重新编译主程序,直接替换.so 文件
内存占用 高(多个程序各自打包一份库代码) 低(多个程序共享内存中的库副本)
编译时间 长(每次编译需打包库代码) 短(仅编译主程序,不打包库代码)
适用场景 移植性要求高、程序体积不敏感、无需频繁升级(如工具类程序) 程序体积敏感、需频繁升级、多个程序共用库(如服务器程序、大型项目)
相关推荐
howareyou236 小时前
Linux中用户态的函数是如何通过系统调用进入内核态的(二)
linux·服务器·linux系统调用
-Springer-6 小时前
STM32 学习 —— 个人学习笔记11-2(SPI 通信外设 & 硬件 SPI 读写 W25Q64)
笔记·stm32·学习
中屹指纹浏览器6 小时前
浏览器指纹内核级篡改技术实现与风险防御
经验分享·笔记
杨云龙UP6 小时前
Oracle 19c多租户架构下设置用户密码永不过期及登录锁定策略说明_20260430
linux·运维·服务器·数据库·oracle
@小码农6 小时前
2026年信息素养大赛【星火征途】图形化编程复赛和决赛模拟题B
开发语言·数据结构·c++·算法
人道领域6 小时前
【LeetCode刷题日记】347.前k个高频元素
java·数据结构·算法·leetcode
雨声不在6 小时前
不连接 USB 远程连接 Android 设备
linux
天天爱吃肉82187 小时前
笔记:同步电机调试时电角度校正方法说明
大数据·人工智能·笔记·功能测试·嵌入式硬件·汽车
念恒123067 小时前
Python(简单判断) —— 从 if 开始
python·学习