本文系统梳理线性结构(顺序表、单链表、双链表、栈、队列)核心知识点,配套完整可运行 C 语言代码、经典作业深度讲解,再详细拆解 Linux 下静态库与动态库的创建、使用、区别及常见问题,补充异常处理、编译报错解决、实战拓展,全干货无冗余,适合数据结构实验、期末复习、Linux 库开发、CSDN 博客直接发布,代码可直接复制编译运行。
1. 线性结构(数据结构基础,重中之重)
线性结构是数据元素之间存在一对一线性关系 的结构,所有元素按顺序排成一条 "连续链条",无分支、无循环,是后续复杂数据结构(树、图)的基础。核心分为两大类存储方式:顺序存储 和链式存储,两者各有优劣,需根据实际场景选择。
1.1 顺序存储(顺序表)
核心定义与本质
- 定义:用一段地址连续的内存空间 存储数据元素,数据元素在内存中按逻辑顺序依次排列,底层依赖数组实现。
- 本质:顺序表 = 封装了操作的数组,数组本身只是存储容器,顺序表在此基础上封装了初始化、增删改查等标准操作,解决了数组操作不规范、易出错的问题。
- 核心特点:
- 数据元素在内存中连续存放,物理地址与逻辑顺序一致;
- 支持随机访问(通过数组下标直接访问任意元素),访问效率极高,时间复杂度 O (1);
- 插入、删除操作需要移动大量元素(为了保持内存连续性),效率较低,时间复杂度 O (n);
- 预先分配固定内存,容量固定,超出容量需手动扩容(易造成内存浪费或溢出)。
顺序表完整 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 链式存储(链表)
核心定义与本质
- 定义:无需连续内存空间,通过 "节点" 存储数据,每个节点包含两部分:数据域 (存储元素值)和指针域(存储下一个 / 上一个节点的地址),节点之间通过指针连接形成链条。
- 本质:链表 = 节点的集合,节点在内存中可分散存储,通过指针维系逻辑顺序,解决了顺序表内存连续、扩容麻烦的问题。
- 核心特点:
- 数据元素在内存中不连续,物理地址无序,逻辑顺序通过指针维系;
- 不支持随机访问,只能从头节点开始遍历,访问效率低,时间复杂度 O (n);
- 插入、删除操作无需移动元素,只需修改指针指向,效率高,时间复杂度 O (1);
- 动态扩容,无需预先分配内存,元素个数可灵活变化(只要内存足够);
- 每个节点需额外存储指针域,空间开销略大于顺序表。
(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)
- 核心定义:只能在表的同一端进行插入和删除操作,不允许在其他位置操作。
- 关键术语:
- 栈顶(Top):允许插入、删除的一端,栈顶元素是最后入栈、最先出栈的元素;
- 栈底(Bottom):固定不变的一端,栈底元素是最先入栈、最后出栈的元素;
- 入栈(Push):向栈顶添加元素,入栈后栈顶指针上移(顺序栈)或新增节点(链式栈);
- 出栈(Pop):从栈顶删除元素,出栈后栈顶指针下移(顺序栈)或删除节点(链式栈);
- 空栈:栈内无元素,栈顶指针指向栈底(顺序栈 top=-1,链式栈 head=NULL)。
- 核心特性:LIFO(Last In First Out,后进先出),类比生活中的 "叠盘子"------ 最后放的盘子,最先被拿走;也类比 "函数调用栈"------ 最后调用的函数,最先执行完毕。
- 存储方式:顺序栈(数组实现)、链式栈(单链表实现),前面章节已提供完整代码,此处补充核心应用场景。
(2)队列(Queue)
- 核心定义:只能在表的一端插入,另一端删除,两端操作相互独立,不允许在中间位置操作。
- 关键术语:
- 队尾(Rear):允许插入元素的一端,入队后队尾指针移动;
- 队头(Front):允许删除元素的一端,出队后队头指针移动;
- 入队(EnQueue):向队尾添加元素;
- 出队(DeQueue):从队头删除元素;
- 空队:队列内无元素,队头与队尾指针重合(循环队列 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:简答题(高频面试 / 期末题)------ 顺序表与链表的区别
核心答案(规范且简洁,适合答题)
- 存储方式:顺序表采用连续内存空间(数组实现);链表采用非连续内存空间(节点 + 指针实现)。
- 访问效率:顺序表支持随机访问(下标直接访问),时间复杂度 O (1);链表仅支持顺序访问(遍历),时间复杂度 O (n)。
- 增删效率:顺序表增删需移动大量元素,时间复杂度 O (n);链表增删只需修改指针,时间复杂度 O (1)(找到目标节点后)。
- 内存开销:顺序表预先分配固定内存,可能造成浪费或溢出;链表动态分配内存,无浪费,但每个节点需额外存储指针,空间开销略大。
- 适用场景:顺序表适合频繁查找、少增删的场景(如学生成绩查询);链表适合频繁增删、少查找的场景(如消息队列)。
作业 2:编程题(实验必做)------ 单链表反转(笔试高频)
题目要求
给定一个单链表,将其反转(如:1→2→3→4 → 4→3→2→1),要求不使用额外空间(原地反转)。
核心思路
利用三个指针(prev、curr、next),依次遍历链表,修改每个节点的 next 指针,使其指向前驱节点,逐步完成反转。
- 初始化 prev 为 NULL(反转后尾节点的 next 为 NULL),curr 指向头节点的下一个节点(第一个有效节点);
- 保存 curr 的下一个节点(next = curr->next),避免反转后丢失后续节点;
- 将 curr 的 next 指向 prev(完成当前节点的反转);
- prev 和 curr 依次后移(prev = curr,curr = next);
- 循环直至 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:编程题(实验必做)------ 用栈实现括号匹配
题目要求
给定一个字符串(仅包含 '('、')'、'['、']'、'{'、'}'),判断字符串中的括号是否完全匹配(成对出现,且嵌套正确)。
核心思路
利用栈的 "后进先出" 特性,遍历字符串,遇到左括号入栈,遇到右括号则弹出栈顶元素,判断是否匹配,最终栈为空则匹配成功。
- 遍历字符串的每个字符;
- 若为左括号('('、'['、'{'),入栈;
- 若为右括号(')'、']'、'}'),判断栈是否为空(为空则不匹配),弹出栈顶元素,判断是否为对应的左括号;
- 遍历结束后,若栈为空,说明所有括号都匹配;否则不匹配。
完整代码实现
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:简答题 ------ 为什么说栈和队列是特殊的线性表?
核心答案(规范答题)
- 栈和队列的本质都是线性表,它们的数据元素之间都存在一对一的线性关系,符合线性表的定义;
- 它们与普通线性表的区别在于操作受限:普通线性表允许在任意位置进行插入和删除操作,而栈仅允许在一端操作,队列仅允许在两端分别进行插入和删除操作;
- 可以理解为:栈和队列是 "限制了操作范围的线性表",其底层存储结构(顺序、链式)与普通线性表完全一致,只是操作规则更严格。
3. 动态库与静态库(Linux C 必学,实战重点)
在实际开发中,我们会将常用的模块化功能(如前面的链表、栈、队列代码)封装成 "库",方便复用、共享,减少重复编码和编译开销,这是 Linux C 开发的基础技能,也是实验和笔试的高频考点。
3.1 什么是库(Library)
核心定义
库是将多个模块化的功能代码(函数、结构体)封装成的二进制文件,不暴露源码,仅提供调用接口(头文件),供其他程序调用。
核心作用(补充细节)
- 复用性:将常用功能(如链表操作、工具函数)封装成库,多个程序可直接调用,无需重复编写代码,提高开发效率;
- 共享性:多个程序可共用同一个库文件(尤其是动态库),节省内存空间,避免重复存储;
- 减少编译开销:库文件提前编译好,调用时无需重新编译库代码,仅编译主程序,大幅缩短编译时间;
- 隐藏实现细节:仅暴露调用接口(头文件),隐藏源码,提高代码安全性和可维护性(如第三方库)。
库的分类
Linux 下的库主要分为两类:静态库(.a 后缀) 和 动态库(.so 后缀),两者的打包方式、使用方式、特性差异较大,需重点区分。
3.2 动态库(共享库,Dynamic Library)
核心特点(补充原理细节)
- 定义:动态库是运行期间才动态加载到内存的库,编译主程序时,仅记录库的调用接口,不将库的代码编译进主程序的可执行文件中;
- 文件后缀:Linux 下为
.so(Shared Object),Windows 下为.dll; - 加载机制:程序运行时,系统会通过动态链接器(ld-linux.so)找到对应的动态库文件,加载到内存中,供程序调用;若未找到动态库,程序会运行失败。
优势(补充细节)
- 可执行文件体积小:仅包含主程序代码和库的调用接口,不包含库的完整代码,节省磁盘空间;
- 支持热更新:库文件升级后,无需重新编译主程序,只需替换旧的库文件,程序运行时会自动加载新的库(适合需要频繁升级的场景,如服务器程序);
- 内存共享:多个程序可共用同一个动态库在内存中的副本,避免重复加载,节省内存空间(如多个程序都调用 printf 函数,只需加载一次 libc.so 库)。
缺点(补充细节)
- 移植性差:程序运行时必须依赖对应的动态库文件,若目标机器上没有该动态库,或库的版本不兼容,程序无法运行;
- 运行效率略低:程序运行时需要动态加载库,增加了一定的系统开销(相较于静态库);
- 版本依赖:若库升级后修改了接口(如函数名、参数),主程序会调用失败,需保证库的接口兼容性。
动态库的创建、使用(完整实战,补充报错解决)
第一步:准备源码文件(以链表、栈、队列为例)
假设我们有 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 种,任选一种)
-
临时指定动态库路径(仅当前终端有效):
csexport LD_LIBRARY_PATH=. # . 表示当前目录,告诉系统在当前目录查找动态库 ./main # 此时可正常运行 -
永久指定动态库路径(所有终端有效):
cs# 编辑环境变量配置文件 vi ~/.bashrc # 在文件末尾添加一行 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. # 生效配置 source ~/.bashrc # 之后可直接运行 ./main -
将动态库复制到系统默认库目录(推荐,永久有效):
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; - 加载机制:程序运行时,无需依赖静态库文件(因为库代码已被打包进可执行文件),可独立运行。
优势(补充细节)
- 移植性极强:可执行文件包含了库的所有代码,无需依赖外部库文件,发给其他机器可直接运行(无需安装对应的库);
- 运行效率高:无需动态加载库,减少了系统运行时的开销,运行速度比动态库快;
- 无版本依赖:库代码直接打包进可执行文件,即使外部静态库文件被删除、升级,也不影响可执行文件的运行。
缺点(补充细节)
- 可执行文件体积大:包含了主程序代码和静态库的完整代码,若多个程序都使用同一个静态库,会各自打包一份,造成磁盘空间和内存的浪费;
- 不支持热更新:若静态库升级,必须重新编译主程序(将新的库代码打包进可执行文件),否则主程序仍使用旧的库代码;
- 编译时间长:每次编译主程序,都需要重新编译静态库代码(或重新打包),编译开销大。
静态库的创建、使用(完整实战,补充细节)
第一步:准备源码文件(与动态库相同)
linklist.c、stack.c、queue.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.o、stack.o、queue.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参数:csgcc main.c -o main -L. -lmylinear -static
3.4 静态库与动态库核心对比(补充细节,笔试高频)
| 对比维度 | 静态库(.a) | 动态库(.so) |
|---|---|---|
| 打包时机 | 编译时,将库代码复制到可执行文件 | 运行时,动态加载到内存,不复制到可执行文件 |
| 可执行文件大小 | 大(包含库完整代码) | 小(仅包含库调用接口) |
| 运行依赖 | 无(不依赖外部库文件) | 有(必须存在对应的.so 文件) |
| 移植性 | 极强(可独立运行,无需安装库) | 差(需目标机器有对应动态库) |
| 运行效率 | 高(无需动态加载,无额外开销) | 略低(需动态加载,有系统开销) |
| 库升级 | 需重新编译主程序(重新打包库代码) | 无需重新编译主程序,直接替换.so 文件 |
| 内存占用 | 高(多个程序各自打包一份库代码) | 低(多个程序共享内存中的库副本) |
| 编译时间 | 长(每次编译需打包库代码) | 短(仅编译主程序,不打包库代码) |
| 适用场景 | 移植性要求高、程序体积不敏感、无需频繁升级(如工具类程序) | 程序体积敏感、需频繁升级、多个程序共用库(如服务器程序、大型项目) |