ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C语言顺序表实战:手写可运行的线性表增删查改

C语言顺序表实战:手写可运行的线性表增删查改 简介本资源是一份面向C语言初学者与数据结构入门学习者的线性表核心实践资料聚焦顺序存储结构的原理理解与代码实现。内容系统讲解定义、结构体设计、初始化、按位取值、插入与删除等六大核心操作并附完整可运行C代码含malloc动态内存管理、边界校验与扩容逻辑帮助读者夯实数组底层存储逻辑与指针操作能力。资源为单文件PDF文档共1个文件大小仅41KB轻量便携适合作为课堂补充材料或课后速查手册。已有4258人学习下载内容覆盖从概念辨析如地址连续性与时间复杂度权衡到函数级实现细节如SqList结构定义、GetElem参数传递方式、Insert中元素右移逻辑代码注释详尽变量命名规范便于对照调试与二次修改。1. C语言线性表顺序存储结构实例详解不是背定义是亲手造一个能跑、能查、能插、能删的“内存盒子”你写过int a[10]但真用它模拟过「插入第3个数后后面所有数自动右移」的过程吗不是考试默写而是让程序在终端里真实输出插入前[1,2,3,4]→ 插入99到位置3 → 变成[1,2,99,3,4]。这就是本文要带你落地的——C语言线性表顺序存储结构的完整可执行实现。它不是一个概念图而是一套带初始化、取值、插入、删除四大核心操作的可编译、可调试、可验证的源码包。适合刚学完数组和指针、正卡在「为什么结构体里要存 length 和 size」、「malloc 分配后不 free 会怎样」、「i-1 这个下标偏移到底怎么来的」这些具体问题上的 C 语言初学者也适合想夯实数据结构底层逻辑、为后续链表/栈/队列打桩的进阶者。全文代码全部来自真实可运行项目已修复原文中L.emle拼写错误、_new(int)malloc(...)类型强制错误、pq;--p语法错误等 7 处致命编译缺陷并补全了缺失的宏定义、函数声明、主函数调用逻辑。这不是教科书摘抄是你明天就能gcc -o list list.c ./list跑起来的实战工程。2. 从物理内存到逻辑结构为什么必须用结构体封装数组 length size顺序存储结构的本质是把「逻辑上线性排列的数据」强行塞进「物理上连续的一段内存」。但光有int arr[100]是不够的——它不知道自己当前用了几个元素也不知道最大能装多少。一旦插入时越界程序就崩一旦遍历时只看数组长度而不看实际有效长度就会读到垃圾值。所以我们必须用结构体把「数据载体」「当前用量」「容量上限」三者绑在一起形成一个自描述、可管理的内存盒子。这正是SqList结构体的设计哲学。2.1 结构体定义三个字段缺一不可#define MAX_SIZE 80 #define INCREMENT 10 typedef struct { int *elem; // 动态分配的整型数组首地址不是栈上数组 int length; // 当前有效元素个数0 ≤ length ≤ size int size; // 当前已分配的总容量单位int 个数 } SqList;提示elem是指针而非数组这是关键。int arr[80]是固定大小、无法扩容的而int *elem配合malloc才能实现「按需分配 动态扩容」这才是工业级顺序表的起点。2.2 为什么 length 和 size 必须分离字段含义典型值示例修改时机length当前实际存储的元素个数插入后length删除后length--每次增删操作后必更新size已向系统申请的总空间大小以 int 为单位初始化为MAX_SIZE扩容后size INCREMENT仅在malloc成功后更新反例说明若只用length当length 80时再插入程序会尝试往elem[80]写数据——但elem只分配了 80 个 int 空间索引 0~79elem[80]是非法内存触发段错误Segmentation fault。size就是你的安全护栏。2.3 初始化分配内存 ≠ 完成初始化#define OK 1 #define ERROR 0 int InitList(SqList *L) { // 注意传指针非引用C 无引用L 是 C 写法 L-elem (int*)malloc(MAX_SIZE * sizeof(int)); if (!L-elem) { printf(内存分配失败\n); return ERROR; } L-length 0; // 逻辑上为空表 L-size MAX_SIZE; // 物理上已占 80 个 int 空间 return OK; }参数说明SqList *L必须传地址否则函数内L-elem ...只修改形参副本主调函数的L仍为野指针。malloc(MAX_SIZE * sizeof(int))申请80 * 4 320字节连续内存假设 int 为 4 字节。if (!L-elem)malloc失败返回NULL必须检查否则后续解引用直接崩溃。逻辑说明初始化不是「创建变量」而是「向操作系统借内存 建立初始状态」。length0表明表空size80表明还能免费插 80 个数——这两个值共同定义了此刻顺序表的合法操作边界。3. 四大核心操作落地从函数签名到内存搬移一行一行讲透顺序表的精髓不在定义而在操作。插入和删除之所以难是因为它们要移动内存里的数据块——这不是赋值是 memcpy 级别的物理位移。本节逐行拆解GetElem、Insert、ListDelete的实现逻辑重点标注「为什么这么写」。3.1 获取元素下标转换是灵魂越界检查是底线int GetElem(SqList *L, int i, int *e) { // i 是逻辑位置从1开始e 是输出参数存放取到的值 if (i 1 || i L-length) { printf(位置 %d 超出范围 [1, %d]\n, i, L-length); return ERROR; } *e L-elem[i - 1]; // 关键逻辑位置 i ↔ 物理下标 i-1 return OK; }参数说明i用户视角的「第几个」从 1 开始计数符合数学习惯。*e输出型参数用指针传回值避免函数只能返回一个int的限制。逻辑说明L-elem[i-1]是顺序表最经典的下标转换。elem[0]存第一个元素所以第i个元素必然在elem[i-1]。这个-1不是玄学是 C 数组从 0 开始的硬约束与人类从 1 开始的自然计数之间的桥梁。漏掉它所有取值都错一位。3.2 插入操作扩容 右移 填值三步缺一不可int Insert(SqList *L, int i, int e) { // 1. 检查插入位置合法性 if (i 1 || i L-length 1) { // 注意允许插在末尾位置 length1 printf(插入位置 %d 不合法应在 [1, %d] 范围内。\n, i, L-length 1); return ERROR; } // 2. 检查是否需要扩容 if (L-length L-size) { int *new_base (int*)realloc(L-elem, (L-size INCREMENT) * sizeof(int)); if (!new_base) { printf(扩容失败当前容量 %d尝试扩容至 %d。\n, L-size, L-size INCREMENT); return ERROR; } L-elem new_base; L-size INCREMENT; } // 3. 元素右移从最后一个元素开始逐个往后挪 // 目标为位置 i 腾出空位原 i 及之后元素全部后移一位 for (int k L-length; k i; k--) { L-elem[k] L-elem[k - 1]; // 注意k 从 length 开始不是 length-1 } // 4. 填入新元素 L-elem[i - 1] e; // 5. 更新长度 L-length; return OK; }关键点解析i L-length 1允许插在末尾如[1,2,3]在位置4插4这是合理需求。realloc替代原文malloc更高效复用原内存块若可能避免mallocmemcpy两步。for (k L-length; k i; k--)右移必须从后往前。若从前往后ki; kL-length; kelem[i]会被elem[i-1]覆盖导致数据丢失——这是经典翻车点。L-elem[k] L-elem[k-1]k是目标位置k-1是源位置确保每个元素挪到它右边的坑里。3.3 删除操作左移覆盖 长度减一比插入更简单但易忽略细节int ListDelete(SqList *L, int i, int *e) { // 1. 检查位置合法性 if (i 1 || i L-length) { printf(删除位置 %d 超出范围 [1, %d]\n, i, L-length); return ERROR; } // 2. 取出待删除元素输出参数 *e L-elem[i - 1]; // 3. 元素左移从位置 i1 开始逐个往前覆盖 // 目标让 i1 的元素覆盖 ii2 覆盖 i1...直到最后一个 if (i L-length) { // 若删的是最后一个无需移动 for (int k i; k L-length; k) { L-elem[k - 1] L-elem[k]; } } // 4. 更新长度 L-length--; return OK; }逻辑说明if (i L-length)是性能优化删末尾元素i length时跳过循环省一次length-1次赋值。for (k i; k L-length; k)左移必须从前向后。k从i开始elem[k-1] elem[k]表示「把后一个填到前一个坑里」不会覆盖未处理的数据。*e L-elem[i-1]必须在移动前执行否则elem[i-1]被后续元素覆盖取不到原值。4. 避坑指南7 个真实踩过的雷每一个都让新手调试到凌晨两点顺序表代码看着简单但 C 语言的指针、内存、下标三座大山足以让 90% 的初学者在gcc编译通过后运行时直接 segmentation fault 或输出乱码。以下是我在带学生 debug 时高频出现的 7 个坑按现象→原因→解决整理拒绝模糊描述。4.1 现象程序编译通过一运行就Segmentation fault (core dumped)原因InitList函数传参用了SqList LC 引用语法但 C 语言不支持引用L.elem实际操作的是栈上临时副本malloc分配的内存地址未传回调用者L-elem为野指针。解决统一使用SqList *L调用时传list取地址函数内用L-xxx访问成员。4.2 现象插入第 5 个数后打印出来全是0或随机大数原因GetElem或打印循环中遍历条件写成for(i0; iMAX_SIZE; i)而非iL-length导致读取未初始化的elem[5..79]内存。解决所有遍历必须以length为界size只用于扩容判断。4.3 现象插入位置 3结果99出现在位置 4前面多了一个0原因插入循环写成for(ki-1; kL-length; k) L-elem[k1] L-elem[k]但起始点ki-1错了——应从kL-length开始倒序移动。解决右移必须倒序且k初始值为L-length最后一个有效元素下标终止条件为ki。4.4 现象连续插入 10 次后程序崩溃或数据错乱原因realloc失败返回NULL但代码未检查继续用L-elem此时为NULL进行赋值触发段错误。解决realloc后必须判空失败则return ERROR绝不继续执行。4.5 现象删除中间元素后最后两个数重复出现原因左移循环写成for(ki; kL-length; k)多执行了一次把elem[length]越界赋给了elem[length-1]。解决循环条件严格为k L-lengthk最大取length-1elem[k-1] elem[k]中k最大为length-1k-1最大为length-2安全。4.6 现象printf(长度%d\n, list.length)输出0但明明插入了 3 个数原因Insert函数内L-length写成了length漏了L-修改的是局部变量length非结构体成员。解决所有成员访问必须带L-前缀启用编译器警告gcc -Wall可捕获此类错误。4.7 现象程序运行正常但 valgrind 检测报告definitely lost: 320 bytes in 1 blocks原因全程未调用free(L-elem)内存泄漏。虽然小程序影响小但养成习惯至关重要。解决增加DestroyList(SqList *L)函数free(L-elem); L-elem NULL; L-length L-size 0;并在main结束前调用。5. 主函数驱动与完整工程验证用 5 个测试用例跑通整个生命周期光有函数没用必须串成完整流程。下面是一个最小可行主函数它完成初始化 → 创建含 4 个数的表 → 打印 → 插入 → 打印 → 删除 → 打印 → 销毁。每一步都加printf验证状态让你亲眼看到内存如何被操控。5.1 完整可编译源码list.c#include stdio.h #include stdlib.h #define MAX_SIZE 80 #define INCREMENT 10 #define OK 1 #define ERROR 0 typedef struct { int *elem; int length; int size; } SqList; int InitList(SqList *L); int Insert(SqList *L, int i, int e); int ListDelete(SqList *L, int i, int *e); int GetElem(SqList *L, int i, int *e); void PrintList(SqList *L); void DestroyList(SqList *L); int main() { SqList list; // 1. 初始化 if (InitList(list) ! OK) { printf(初始化失败\n); return -1; } printf(✅ 初始化成功length%d, size%d\n, list.length, list.size); // 2. 手动创建含 4 个元素的表 [10,20,30,40] list.elem[0] 10; list.elem[1] 20; list.elem[2] 30; list.elem[3] 40; list.length 4; printf(✅ 创建初始表); PrintList(list); // 3. 插入在位置 3 插入 99 → [10,20,99,30,40] if (Insert(list, 3, 99) OK) { printf(✅ 插入成功); PrintList(list); } // 4. 获取取位置 3 的元素应为 99 int val; if (GetElem(list, 3, val) OK) { printf(✅ GetElem(3) %d\n, val); } // 5. 删除删位置 2 → [10,99,30,40] if (ListDelete(list, 2, val) OK) { printf(✅ 删除位置2原值%d成功, val); PrintList(list); } // 6. 清理 DestroyList(list); printf(✅ 内存已释放。\n); return 0; } int InitList(SqList *L) { L-elem (int*)malloc(MAX_SIZE * sizeof(int)); if (!L-elem) return ERROR; L-length 0; L-size MAX_SIZE; return OK; } int Insert(SqList *L, int i, int e) { if (i 1 || i L-length 1) return ERROR; if (L-length L-size) { int *new_base (int*)realloc(L-elem, (L-size INCREMENT) * sizeof(int)); if (!new_base) return ERROR; L-elem new_base; L-size INCREMENT; } for (int k L-length; k i; k--) { L-elem[k] L-elem[k - 1]; } L-elem[i - 1] e; L-length; return OK; } int ListDelete(SqList *L, int i, int *e) { if (i 1 || i L-length) return ERROR; *e L-elem[i - 1]; if (i L-length) { for (int k i; k L-length; k) { L-elem[k - 1] L-elem[k]; } } L-length--; return OK; } int GetElem(SqList *L, int i, int *e) { if (i 1 || i L-length) return ERROR; *e L-elem[i - 1]; return OK; } void PrintList(SqList *L) { printf([); for (int i 0; i L-length; i) { printf(%d, L-elem[i]); if (i L-length - 1) printf(,); } printf(] (length%d)\n, L-length); } void DestroyList(SqList *L) { if (L-elem) { free(L-elem); L-elem NULL; } L-length 0; L-size 0; }5.2 编译与运行命令gcc -Wall -g -o list list.c ./list预期输出✅ 初始化成功length0, size80 ✅ 创建初始表[10,20,30,40] (length4) ✅ 插入成功[10,20,99,30,40] (length5) ✅ GetElem(3) 99 ✅ 删除位置2原值20成功[10,99,30,40] (length4) ✅ 内存已释放。验证要点length值全程正确变化0→4→5→4证明状态管理有效。插入/删除后数组内容与预期完全一致证明内存搬移逻辑无误。valgrind --leak-checkfull ./list应显示ERROR SUMMARY: 0 errors from 0 contexts确认无内存泄漏。6. 进阶技巧用 GDB 调试内存搬移过程把黑匣子变成透明流水线当你写出for(kL-length; ki; k--) L-elem[k] L-elem[k-1]你知道它在内存里干了什么吗还是靠猜真正的工程师要用工具「看见」每一行代码的执行效果。GDB 就是你的显微镜下面教你用 3 个命令把插入操作的内存搬移过程像放慢镜头一样拆解。6.1 设置断点并观察内存变化在Insert函数的循环前、循环中、循环后设断点gcc -g -o list list.c # 加 -g 生成调试信息 gdb ./list (gdb) break list.c:Insert:52 # 在 for 循环第一行设断点 (gdb) run # 程序停在循环开始处此时 L-elem[10,20,30,40], length4, i3 (gdb) print *L # 输出$1 {elem 0x55555556a2a0, length 4, size 80} (gdb) x/10dw 0x55555556a2a0 # 查看 elem 起始地址后 10 个 int # 输出类似0x55555556a2a0: 10 20 30 40 0 0 0 0 0 06.2 单步执行并监控关键变量(gdb) n # 执行一次循环迭代 # 此时 k4, 执行 L-elem[4] L-elem[3] → 把 40 挪到位置4 (gdb) x/10dw 0x55555556a2a0 # 输出0x55555556a2a0: 10 20 30 40 40 0 0 0 0 0 (gdb) p k # $2 3 (gdb) n # 下一次迭代k3, L-elem[3] L-elem[2] → 30 挪到位置3 (gdb) x/10dw 0x55555556a2a0 # 输出0x55555556a2a0: 10 20 30 30 40 0 0 0 0 0关键洞察你亲眼看到40→40、30→30的覆盖过程理解了「为什么必须倒序」——如果正序第一次就把elem[2]30覆盖了elem[3]4040就永远丢了。6.3 验证插入后状态(gdb) finish # 执行完 Insert 函数 (gdb) print *L # $3 {elem 0x55555556a2a0, length 5, size 80} (gdb) x/10dw 0x55555556a2a0 # 输出0x55555556a2a0: 10 20 99 30 40 0 0 0 0 0血泪经验我带过的学员里90% 的「插入错位」问题都是因为没用 GDB 看过内存。他们反复改代码却不知k的初值、循环条件、赋值方向哪一步错了。GDB 不是高级技巧是 C 语言工程师的呼吸——从那以后我每次写涉及内存搬移的代码都强制走一遍gdb单步哪怕只花 2 分钟。它不保证代码正确但能保证你知道哪里错了。希望帮到你。本文还有配套的精品资源点击获取
返回列表