动态顺序表 + 单链表 + 通讯录管理系统(C 语言数据结构入门项目:线性表全实现)
本仓库为 C 语言数据结构「线性存储结构」专项学习的完整产出,包含从基础概念梳理、接口代码实现、项目落地开发到问题调试优化的全流程记录。 仓库内附完整可运行源码、编译好的可执行文件,以及完整的学习交流记录,适合刚完成 C 语言基础学习、初次接触数据结构的同学参考学习。
重要提示:
1.本文章主要由三部分组成(小标题中已显示,目录也可快速定位)
2.了解详细学习过程请到数据结构专题:https://blog.csdn.net/luminousone/category_13167841.html?fromshare=blogcolumn&sharetype=blogcolumn&sharerId=13167841&sharerefer=PC&sharesource=Luminousone&sharefrom=from_link
3.所有配套代码请访问:
Luminous/Luminousbegin
一、项目学习目标
本次专项学习围绕数据结构中的线性存储结构展开,在 C 语言语法基础上,系统掌握顺序存储与链式存储的原理、实现与落地应用,同时练习借助工具高效自主学习的方法。核心目标包括:
- 理解线性表的基本概念与逻辑特点;
- 掌握动态顺序表的定义、初始化、增删改查、销毁等全套操作;
- 掌握单链表的结点定义、创建、插入、删除、查找、遍历、销毁等全套操作;
- 能够将数据结构落地到实际项目中,完成小型管理系统的开发;
- 掌握代码调试、问题定位的基本方法,提升工程化代码能力;
- 学会梳理完整的学习过程,形成可复用的学习记录。
核心学习内容
本次学习覆盖线性表两大存储结构的全部核心知识点:
- 线性表基础:线性表定义、线性结构特点、逻辑结构、基本操作、顺序存储与链式存储的核心区别
- 顺序存储(动态顺序表):存储特点、结构体设计、初始化、扩容、插入 / 删除 / 查找 / 遍历、优缺点分析
- 链式存储(单链表):存储思想、结点结构、头指针与头结点、头插 / 尾插、插入 / 删除 / 查找 / 遍历、优缺点分析
第一部分:整体学习总结
通过本次 C 语言专项学习,我围绕数据结构中的线性存储结构进行了系统学习与代码实践,重点掌握了线性表、顺序表和单链表的基本概念、存储特点及常用操作。学习初期,我先理解了线性表的逻辑结构,明确了顺序存储和链式存储的区别,随后完成了动态顺序表的初始化、扩容、插入、删除、查找、修改和销毁等全套接口。
在顺序表学习的基础上,我完成了基于动态顺序表的 C 语言通讯录项目。通讯录使用结构体保存联系人信息,包括姓名、性别、年龄、电话和住址等内容,并通过顺序表统一管理联系人。项目实现了添加、删除、查找、修改、显示、保存和加载等功能,让顺序表的相关操作真正落地到实际项目中。
在调试过程中,我遇到了结构体打印格式不匹配、姓名查找失败、输入异常导致菜单死循环、scanf 和 fgets 混用造成输入错乱、修改联系人信息保存异常等问题。通过逐行分析代码逻辑,我加入了清空输入缓冲区、去除字符串换行符、判断输入返回值、优化修改函数等处理方式,使程序运行更加稳定。
后续我继续学习了单链表,理解了结点、头指针和 next 指针的作用,并练习了头插、尾插、查找、删除、遍历和销毁等操作。通过对比总结,我认识到顺序表适合下标访问方便、数据较稳定的场景,而链表更适合频繁插入和删除的场景。
通过本次学习,我巩固了结构体、指针、数组、函数、动态内存分配、字符串处理和文件操作等 C 语言核心知识,也提高了代码调试和问题分析能力。后续我会继续加强链表和指针相关练习,进一步提升程序设计能力。
二、完整学习交流记录(第二部分:按学习进度整理)
以下为本次学习过程中,借助 AI 工具辅助理解概念、排查代码问题、梳理知识点的完整记录,按学习阶段完整保留,还原从入门到踩坑再到解决的全流程。
记录 1:认识线性表与整体学习方向
提问: 本次 C 语言线性表学习应该先理解哪些核心内容?线性表、顺序表、链表之间是什么关系?
AI 回答整理: 线性表是一种最基本的数据结构,它的特点是数据元素之间存在一对一的线性关系。除了第一个元素没有直接前驱、最后一个元素没有直接后继外,其余元素都有且只有一个直接前驱和一个直接后继。顺序表和链表都属于线性表,它们的逻辑结构相同,都是线性关系,但存储结构不同。顺序表使用连续空间存储数据,链表使用指针把不同位置的结点连接起来。
学习补充: 我先从线性表的概念入手,理解线性表的逻辑结构,再继续学习顺序表和链表两种存储方式。顺序表对应顺序存储,链表对应链式存储,两者都是线性表的具体实现。
个人理解与处理: 通过这次交流,我明确了本次学习的核心内容是线性表。顺序表和链表都是线性表的实现方式,区别主要体现在存储结构上。后续学习可以先从顺序表入手,完成基础接口,再把顺序表应用到通讯录项目中,最后再学习单链表并思考如何优化通讯录底层结构。
记录 2:理解顺序表的基本思想
提问: 什么是顺序表?它和普通数组有什么区别?为什么做通讯录项目之前要先学顺序表?
AI 回答整理: 顺序表可以理解为用数组实现的线性表,特点是数据在内存中连续存放,可以通过下标快速访问元素。普通数组大小固定,而动态顺序表可以通过动态内存申请和扩容来改变容量。顺序表通常需要记录三个信息:数据存放的位置、当前有效数据个数、当前容量。通讯录中每个联系人可以看作一个数据元素,多个联系人组成一个线性表,所以可以用顺序表作为通讯录的底层存储结构。
学习补充: 我把动态顺序表理解为 “动态数组 + size + capacity” 的封装。size 表示当前有效数据个数,capacity 表示当前容量,数组指针负责指向动态申请的空间。
个人理解与处理: 我先整理了线性表和顺序表的基础概念,明确顺序表的核心不是简单数组,而是对数组进行管理和封装。这样后面写通讯录时,联系人信息就可以作为顺序表中的元素进行统一管理。
记录 3:设计动态顺序表结构体
提问: 如果用 C 语言实现动态顺序表,结构体应该怎么写?size 和 capacity 分别代表什么?
AI 回答整理: 动态顺序表一般可以定义一个结构体,里面包含数据指针、有效元素个数 size 和容量 capacity。数据指针用于指向动态申请的空间,size 表示当前顺序表中已经存放了多少个有效数据,capacity 表示当前申请的空间最多能存放多少个数据。插入数据时要看 size 是否等于 capacity,如果相等就需要扩容。遍历和删除时主要依赖 size,不能把 capacity 当作有效数据个数。
学习补充: 顺序表结构体主要包含三个成员:数据指针、有效数据个数和容量。后续初始化、扩容、插入、删除、查找和销毁等操作都围绕这三个成员展开。
个人理解与处理: 我根据 AI 解释写出了动态顺序表结构体,并理解了 size 和 capacity 的区别。size 控制有效数据范围,capacity 控制当前空间大小。如果混淆这两个变量,就容易出现访问无效空间、越界访问或扩容错误等问题。
记录 4:实现顺序表初始化、扩容和销毁
提问: 顺序表初始化、扩容和销毁应该怎么写?动态内存管理有什么要注意的?
AI 回答整理: 初始化时需要把数据指针置为空,把 size 和 capacity 置为 0。扩容时要先判断当前容量是否已满,如果 size 等于 capacity,就申请更大的空间。第一次扩容可以申请 4 个元素空间,之后可以按照 2 倍容量增长。使用 realloc 时最好用临时指针接收返回值,判断申请成功后再赋给原指针,避免扩容失败导致原空间丢失。销毁时需要释放动态申请的空间,并将指针置为空,size 和 capacity 也要清零。
学习补充: 我实现了初始化、扩容和销毁函数。初始化让顺序表从空表状态开始;扩容用于解决容量不足问题;销毁函数用于释放动态申请的空间。
个人理解与处理: 我在顺序表代码中实现了 SLInit、SLExp 和 Destroy 等函数,并注意了 assert 断言和 realloc 失败判断。通过这一步,我理解了动态顺序表比普通数组灵活的原因,也进一步熟悉了 malloc、realloc、free 等动态内存操作。
记录 5:实现顺序表基础增删操作
提问: 顺序表的尾插、头插、尾删、头删应该怎么实现?元素移动时有什么注意事项?
AI 回答整理: 尾插最简单,只需要判断是否需要扩容,然后把新元素放到下标 size 的位置,再让 size 加 1。头插需要从后向前移动数据,为第一个位置腾出空间,再把新元素放到下标 0 处。尾删只需要判断顺序表是否为空,然后 size 减 1。头删需要从前向后移动数据,用后一个元素覆盖前一个元素,最后 size 减 1。插入和删除时都要注意顺序表是否为空、位置是否合法以及移动方向。
学习补充: 我完成了尾插、头插、尾删和头删等基础接口。尾部操作主要改变 size,头部操作需要移动数组元素。
个人理解与处理: 我完成了 PushBack、PushFront、SLPopBack、SLPopFront 等接口。通过调试,我发现头插必须从后往前移动,否则前面的数据会覆盖后面的数据;头删则需要从前往后覆盖。这个过程让我加深了对数组元素移动的理解。
记录 6:实现顺序表指定位置插入、删除、查找和修改
提问: 顺序表进阶接口怎么写?指定位置插入和指定位置删除的边界有什么区别?
AI 回答整理: 指定位置插入时,合法范围是 0 到 size 之间,包括 size,因为 pos 等于 size 时相当于尾插。插入时需要从最后一个元素开始向后移动,直到 pos 位置腾出来。指定位置删除时,合法范围是 0 到 size-1,因为删除只能删除已经存在的元素。查找可以通过遍历顺序表实现,找到返回下标,找不到返回 - 1。修改指定位置元素时,需要先判断下标是否合法,然后直接修改对应位置的数据。
学习补充: 我补充了指定位置插入、指定位置删除、按值查找和按位置修改等接口,使顺序表的增删改查功能更加完整。
个人理解与处理: 我在顺序表中补充了 PushSpecify、SLDeleteSpecify、FindSpecify 和 Modifyspecified 等接口。通过这个过程,我进一步理解了顺序表的增删改查,也认识到边界判断非常重要。插入可以在末尾之后插入,但删除不能删除不存在的位置,这一点在写代码时很容易出错。
记录 7:从顺序表过渡到通讯录项目
提问: 我已经完成了动态顺序表,想用它做一个 C 语言通讯录,应该怎么设计?
AI 回答整理: 通讯录可以看作顺序表的实际应用。每个联系人可以定义为一个结构体,包含姓名、性别、年龄、电话、地址等信息。多个联系人可以存储在动态顺序表中。顺序表负责底层存储,通讯录函数负责具体业务功能,例如添加联系人、删除联系人、查找联系人、修改联系人、显示联系人、保存联系人等。可以把联系人结构体作为顺序表的数据类型,让顺序表管理联系人数组。
学习补充: 我把联系人设计成结构体,包含姓名、性别、年龄、电话、住址等字段。然后把顺序表的数据类型从普通数据改为联系人结构体,使顺序表可以存储多个联系人。
个人理解与处理: 我把原来顺序表中存储普通数据的思路改成存储联系人结构体,并开始编写通讯录项目。这个过程让我认识到,数据结构不是孤立的理论,顺序表可以直接作为小型管理系统的底层结构。
记录 8:通讯录多文件结构设计
提问: 通讯录项目分成多个文件写,头文件和源文件之间应该怎么组织?为什么容易出现头文件包含混乱?
AI 回答整理: 多文件项目中要注意文件职责清晰。顺序表相关声明和实现可以放在 SeqList.h 和 SeqList.c 中,通讯录相关声明和实现可以放在 Contact.h 和 Contact.c 中,主函数和菜单可以放在 test.c 或 main.c 中。头文件要用 #pragma once 或者 #ifndef 防止重复包含。顺序表和通讯录之间如果互相包含,容易出现循环依赖。可以让通讯录使用顺序表结构,但尽量避免两个头文件互相依赖。
学习补充: 我按照五个文件组织代码,分别负责顺序表声明、顺序表实现、通讯录声明、通讯录实现和主函数测试。这样比所有代码写在一个文件里更清晰。
个人理解与处理: 我按照五个文件的结构整理通讯录项目,包括 SeqList.h、SeqList.c、Contact.h、Contact.c 和 test.c。通过 AI 提示,我注意到头文件要尽量减少互相包含,并让各个文件职责更清晰。这样后续调试时也更容易定位问题。
记录 9:通讯录基础功能实现
提问: 通讯录基础功能应该先实现哪些?添加、删除、查找、修改、展示之间应该怎么联系?
AI 回答整理: 基础版通讯录可以先实现菜单循环,再实现添加联系人、删除联系人、显示联系人、查找联系人、修改联系人和退出保存等功能。添加联系人本质上是向顺序表尾插一个联系人结构体;删除联系人需要先查找联系人位置,再调用顺序表指定位置删除;修改联系人需要先查找联系人,再对结构体字段进行修改;显示联系人就是遍历顺序表,把每个联系人信息打印出来。
学习补充: 我先实现了添加、删除、展示、查找、修改和退出系统等基础功能。其中添加联系人对应顺序表尾插,删除联系人对应顺序表指定位置删除,展示联系人对应顺序表遍历。
个人理解与处理: 我完成了通讯录的基础功能,把顺序表的接口用于联系人管理。添加联系人时使用 PushBack,删除联系人时使用 SLDeleteSpecify,展示联系人时遍历 Con->a 数组。这个阶段项目已经可以运行,但输入、查找和修改体验还比较粗糙。
记录 10:查找功能出错,定位顺序表打印格式和类型不匹配
提问: 我把顺序表改成存储联系人结构体后,查找时出现崩溃、乱码或打印错误,应该怎么排查?
AI 回答整理: 需要先检查顺序表的数据类型和打印格式是否匹配。如果原来的顺序表是存整数,打印格式可能定义为 % d,但现在顺序表中存的是联系人结构体,不能再用 % d 打印结构体。通讯录应该单独写展示函数,按照联系人字段分别打印姓名、性别、年龄、电话和地址,而不是继续使用原来顺序表的通用打印格式。查找联系人时也应使用 strcmp 比较字符串字段,而不能直接用普通整数比较方式。
出错代码与修改方向:#define SLTYPE_PRINT_FMT "%d"这个格式适合打印整数,但通讯录中存储的是联系人结构体,因此不能继续按整数格式打印。通讯录中应当单独按字段输出联系人信息,例如:
printf("%-10s %-10s %-10s %-15s %-20s\n", Con->a[index].name, Con->a[index].Gender, Con->a[index].age, Con->a[index].Tel, Con->a[index].Address);个人理解与处理: 我检查后发现,SeqList.h 中原来有类似 SLTYPE_PRINT_FMT “% d” 的设计,但通讯录实际存储的是结构体。于是我把通讯录打印逻辑独立出来,在 ShoCon 和 FinCon 中分别打印联系人字段,避免用整数格式输出结构体。这次问题让我认识到,顺序表可以写成通用结构,但当数据类型改变后,打印、查找和比较方式也必须同步调整。
记录 11:查找菜单输入逻辑问题
提问: 我的查找菜单提示 “默认按姓名查找,输入 1 切换方式”,但是我直接输入姓名时,程序提示输入错误,这是为什么?
AI 回答整理: 这是因为代码中使用 scanf (”% d”, &choose) 读取数字,所以程序一开始期待用户输入数字。如果用户直接输入姓名,例如 “王一”,scanf 读取失败,就会进入输入错误分支。提示语和实际代码逻辑不一致。要么修改提示,让用户先输入数字选择;要么修改逻辑,让程序真正支持直接输入姓名。如果想实现 “默认按姓名查找”,可以先设计清楚输入规则,避免既想接收数字又想直接接收字符串导致混乱。
出错代码与修改方向:
printf("默认按姓名查找,输入 1 切换方式:"); if (scanf("%d", &choose) != 1) { ClearBuff(); printf("输入错误!\n"); return -1; }这段代码本质上要求用户必须先输入数字,但提示语容易让用户直接输入姓名。后来我把查找功能改成明确的二级菜单,让用户先选择按姓名、电话或住址查找。
个人理解与处理: 我根据 AI 解释认识到,问题不是 strcmp 本身,而是查找菜单的输入逻辑和提示不一致。后来我调整了 Menu2 函数,让用户可以明确选择按姓名、电话、地址查找,或者让默认姓名查找逻辑与提示保持一致。这个问题让我明白,交互提示和代码读取方式必须对应。
记录 12:姓名查找失败,处理换行符和输入缓冲区
提问: 我按姓名查找时,明明添加了 “王一”,但是查找还是 Not found,可能是什么原因?
AI 回答整理: 如果使用 fgets 读取字符串,可能会把末尾的换行符一起读进去。例如存储的是 “王一”,查找时输入的可能是 “王一 \n”,strcmp 比较时就会判定不相等。因此需要在读取后使用 strcspn 去掉换行符。同时,如果前面使用 scanf 读取数字,输入缓冲区中可能残留换行符,后面使用 fgets 时可能直接读到空行,所以在 scanf 后要根据情况清理缓冲区。
出错代码与修改方向:fgets(search, sizeof(search), stdin);如果只这样写,输入末尾可能带有换行符,导致 strcmp 比较失败。修改时需要增加:search[strcspn(search, "\n")] = '\0';
查找函数中也可以对传入的字符串再次处理:name[strcspn(name, "\n")] = '\0';
个人理解与处理: 我把查找函数和 Menu2 中的字符串输入逻辑进行了调整,在读取姓名、电话、地址后去掉末尾换行符,并在必要位置调用 ClearBuff 清理输入缓冲区。修复后,按姓名、电话、地址查找都更加稳定。这个问题让我理解了 scanf 和 fgets 混用时为什么容易出错。
记录 13:优化输入异常,解决菜单死循环
提问: 通讯录菜单要求输入数字,如果用户输入汉字或字母,程序会一直死循环,怎么解决?
AI 回答整理: 这是因为 scanf 读取数字失败后,错误输入仍然留在输入缓冲区中。下一次循环时,scanf 会继续读取同样的错误内容,于是不断失败,形成死循环。解决办法是在 scanf 返回值不为 1 时,调用清空输入缓冲区的函数,把当前行的错误输入清理掉,再提示用户重新输入。可以写一个 ClearBuff 函数,循环读取字符直到遇到换行符或 EOF。
出错代码与修改方向:scanf("%d", &input);如果用户输入汉字或字母,scanf 读取失败,错误内容留在缓冲区中。修改后加入返回值判断:
if (scanf("%d", &input) != 1) { ClearBuff(); printf("输入错误!请输入数字\n"); continue; }同时封装清空输入缓冲区函数:
void ClearBuff() { int c; while ((c = getchar()) != '\n' && c != EOF); }个人理解与处理: 我在菜单输入中加入了 scanf 返回值判断,如果输入不是数字,就调用 ClearBuff 清理缓冲区,并提示 “输入错误,请输入数字”。这样即使输入汉字、字母或其他非法内容,程序也不会卡死,而是重新等待用户输入。
记录 14:修改联系人时实现 “回车跳过不修改”
提问: 修改联系人时,如果每一项都必须重新输入,很麻烦。能不能实现不想修改的字段直接回车跳过?
AI 回答整理: 可以使用 fgets 读取一整行输入。如果用户直接按回车,说明不修改当前字段,保留原来的值;如果用户输入了新内容,就去掉换行符并覆盖原字段。可以封装一个 InputSkip 函数,传入目标字段、字段长度和原值。函数内部读取输入后判断是否为空,如果为空就复制旧值,否则复制新值。这样可以让修改功能更灵活。
关键实现思路:
fgets(temp, read_len, stdin); temp[strcspn(temp, "\n")] = '\0'; if (strlen(temp) == 0) { strncpy(buf, oldVal, len - 1); } else { strncpy(buf, temp, len - 1); }个人理解与处理: 我根据这个思路写了 InputSkip 函数,并把修改姓名、年龄、电话、地址等操作改成 “直接回车保留旧值”。这样通讯录修改功能从原来的全部重输,优化成只修改需要变动的字段,使用体验更好。
记录 15:修改姓名输入错乱问题
提问: 修改联系人姓名时出现输入错乱,有时候姓名修改失败,后面的字段也会受影响,这是为什么?
AI 回答整理: 这类问题通常和输入缓冲区处理有关,尤其是 scanf 和 fgets 混用时更容易出现。需要检查在进入修改函数前是否已经清理过缓冲区,如果工具函数内部又重复清理,可能会把用户刚输入的内容提前读走,导致真正的 fgets 读不到数据。清空缓冲区不能随便放在每个输入函数里,而应该根据调用流程确定是否需要清理。输入函数最好只负责读取输入和判断是否保留旧值,缓冲区清理单独放在必要位置。
出错原因与修改方向: 修改姓名、年龄、电话、住址时,统一交给 InputSkip 处理,避免每个地方都重复清空缓冲区。修改时主要保留如下调用方式:
printf("请输入新的姓名:"); InputSkip(Con->a[index].name, NAME_Max, Con->a[index].name); printf("请输入电话:"); InputSkip(Con->a[index].Tel, TEL_Max, Con->a[index].Tel);个人理解与处理: 我重新检查了修改联系人时的调用流程,发现有些地方清理缓冲区的位置不合适,容易造成输入被提前消耗。后来我调整了 InputSkip 函数和相关调用逻辑,让输入函数主要负责读取和判断,减少重复清理缓冲区的情况。这个过程让我认识到,调试输入问题不能只看单个函数,还要看整个函数调用顺序。
记录 16:修改性别时自定义内容无法保存
提问: 我修改联系人性别时,输入 0 代表女、输入 1 代表男,但是输入其他内容有时候改不了,这是为什么?
AI 回答整理: 需要检查自定义输入是否真正赋值到联系人结构体中。性别修改逻辑应该分几种情况:如果用户直接回车,就保留旧值;如果输入 0,就保存为 “女”;如果输入 1,就保存为 “男”;如果输入其他内容,例如 “保密” 或 “未知”,就直接把输入内容复制到 Gender 字段中。不能只读取到临时 buf 里,却没有正确写回结构体字段。
出错代码与修改方向:InputSkip(buf, sizeof(buf), Con->a[index].Gender);这一步只是读取到临时数组 buf 中,后面还需要根据 buf 内容写回结构体字段:
if (strlen(buf) == 0) { // 保留原性别 } else if (strcmp(buf, "0") == 0) { strcpy(Con->a[index].Gender, "女"); } else if (strcmp(buf, "1") == 0) { strcpy(Con->a[index].Gender, "男"); } else { strcpy(Con->a[index].Gender, buf); }个人理解与处理: 我修改了 ModDat 函数中的性别处理逻辑。现在输入 0 会保存 “女”,输入 1 会保存 “男”,输入其他文字也会保存为自定义性别,直接回车则保留原来的值。这个 Bug 让我认识到,输入到临时变量并不代表数据已经更新,必须明确写回结构体成员。
记录 17:通讯录文件保存和加载
提问: 通讯录退出后数据丢失,应该怎么保存?第一次运行文件不存在怎么办?
AI 回答整理: 可以使用文件操作保存联系人数据。退出通讯录时,以二进制写入方式打开文件,把顺序表中的每个联系人结构体写入文件。程序启动时,以二进制读取方式打开文件,如果文件存在,就循环读取联系人并插入到通讯录中;如果文件不存在,说明是第一次运行,可以提示没有历史数据并创建新的空通讯录。文件操作时必须判断 fopen 返回值,避免文件打开失败后继续读写导致错误。
学习补充: 我加入了文件保存和加载功能。程序启动时尝试读取历史联系人数据,退出时把当前通讯录内容保存到文件中。
个人理解与处理: 我实现了 SaveCon 和 LoadCon 函数。程序启动时加载历史数据,退出时保存当前通讯录。第一次运行没有文件时,程序不会崩溃,而是创建新的通讯录。这让我进一步练习了 C 语言文件读写和异常判断。
记录 18:通讯录终版功能整理
提问: 我的通讯录已经多次修改,终版应该怎么总结?哪些功能算已经完成?
AI 回答整理: 可以从底层结构、基础功能、输入优化、查找优化、修改优化、文件保存和 Bug 修复几个方面总结。底层使用动态顺序表存储联系人,基础功能包括添加、删除、查找、修改、显示、保存和退出。输入优化包括处理非法输入、清理缓冲区、使用 fgets 读取字符串。查找优化包括按姓名、电话、地址查找。修改优化包括回车跳过不修改、性别快捷输入和自定义输入。Bug 修复包括解决查找乱码、姓名查找失败、菜单死循环、修改姓名输入错乱、性别自定义无法保存等问题。
学习补充: 学习记录从顺序表基础接口开始,到通讯录基础版、优化版、终版和补坑篇,记录了从实现功能到修复 Bug 的完整过程。终版通讯录已经具备添加、删除、查找、修改、展示、保存、加载和异常输入处理等功能。
个人理解与处理: 我根据学习记录和调试过程,把通讯录项目整理成了一个完整的顺序表应用案例。相比一开始只会写简单接口,后面我能根据实际使用中的问题不断修改程序,说明自己对顺序表和 C 语言项目的理解更深入了。
记录 19:学习单链表,理解链式存储思想
提问: 顺序表通讯录完成后,为什么还要学习单链表?单链表和顺序表最大的区别是什么?
AI 回答整理: 顺序表使用连续空间存储数据,访问方便,但插入和删除时可能需要移动大量元素,并且需要考虑容量和扩容问题。单链表使用结点存储数据,每个结点包含数据域和指针域,指针指向下一个结点。链表不要求物理空间连续,插入和删除时主要修改指针关系,不需要整体移动元素,也不需要像顺序表那样扩容。但是链表不能通过下标直接访问某个元素,查找时通常要从头遍历。
学习补充: 我学习了结点结构、头指针、头插法、尾插法、查找、插入、删除、遍历和销毁等操作。相比顺序表,单链表更强调指针之间的连接关系。
个人理解与处理: 我在顺序表通讯录的基础上继续学习单链表,重点理解结点结构、头指针、头插法、尾插法、查找、插入、删除和释放空间等内容。通过对比,我认识到顺序表和链表各有适用场景,不能简单说谁更好。
记录 20:单链表结点设计
提问: 如果用单链表存储数据,结点应该怎么设计?和顺序表结构体有什么区别?
AI 回答整理: 单链表结点一般由数据域和指针域组成。数据域用于存放具体数据,指针域用于保存下一个结点的地址。如果是普通整数链表,数据域可以是 int;如果是通讯录链表,数据域可以是联系人结构体。顺序表结构体中通常有数组指针、size 和 capacity,而单链表不需要 capacity,因为它不是一次性申请连续空间,而是需要一个结点就申请一个结点。链表主要依赖头指针找到第一个结点,再通过 next 指针依次访问后续结点。
学习补充: 单链表中,每个结点都保存数据和指向下一个结点的指针。链表的整体访问依赖头指针,不像顺序表那样通过数组下标直接访问。
个人理解与处理: 我理解了顺序表和链表在结构体设计上的区别。顺序表重点管理数组空间和有效数据个数,链表重点管理每个结点之间的指针关系。后续如果把通讯录改成链表结构,就要把联系人信息放到结点的数据域中。
记录 21:单链表基本操作学习
提问: 单链表应该先实现哪些基本操作?为什么有些函数要用二级指针?
AI 回答整理: 单链表基本操作包括创建结点、头插、尾插、查找、删除、遍历和销毁。头插和头删可能会改变头指针本身,如果只传一级指针,函数内部修改的只是形参副本,外部头指针不会改变。因此在可能改变头指针的操作中,需要使用二级指针。尾插时需要找到最后一个结点,再把新结点连接到链表尾部。销毁链表时,要先保存下一个结点地址,再释放当前结点,避免释放后找不到后续结点。
学习补充: 我重点练习了头插、尾插、查找、遍历和销毁。链表和顺序表最大的不同是,链表操作经常会改变指针指向,尤其是涉及头结点变化时,需要特别注意参数传递。
个人理解与处理: 我根据 AI 提示练习了单链表的基本接口,理解了为什么链表操作中经常使用二级指针。尤其是头插、头删和销毁操作,都需要特别注意头指针变化和内存释放顺序。
记录 22:单链表插入和删除的指针顺序
提问: 单链表插入和删除时最容易错在哪里?为什么有时候会断链或内存泄漏?
AI 回答整理: 链表插入时要注意连接顺序。比如在 pos 结点后插入新结点,应先让新结点的 next 指向 pos 原来的下一个结点,再让 pos 的 next 指向新结点。如果顺序反了,可能导致后面的链表丢失。删除结点时要先保存待删除结点地址,再修改前驱结点的 next 指针,最后释放待删除结点。如果没有保存地址就修改指针,可能找不到要释放的结点,造成内存泄漏。销毁链表时也要先保存 next,再释放当前结点。
关键代码补充:
newnode->next = pos->next; pos->next = newnode;删除结点时需要先保存待删除结点:
SLTNode* del = pos->next; pos->next = del->next; free(del);个人理解与处理: 我用 “先保存、再连接、后释放” 的思路记忆链表操作。插入时重点防止断链,删除时重点防止丢失待释放结点。通过练习,我对链表指针操作的顺序有了更清楚的认识。
记录 23:用单链表思路优化通讯录底层结构
提问: 如果不用顺序表,而是用单链表优化通讯录,整体结构应该怎么改?
AI 回答整理: 可以把联系人信息作为链表结点的数据域,每个结点中保存一个联系人结构体和一个 next 指针。通讯录不再使用动态数组、size 和 capacity,而是使用头指针指向第一个联系人结点。添加联系人时,可以使用尾插法把新联系人放到链表末尾;删除联系人时,先按姓名、电话或地址找到目标结点,再修改前驱结点的 next 指针并释放目标结点;查找联系人时,从头指针开始遍历;修改联系人时,找到目标结点后直接修改结点中的联系人数据;展示联系人时,遍历整个链表;退出时,遍历释放所有结点。
学习补充: 如果把通讯录底层改为链表,可以把联系人结构体作为链表结点的数据域。每个结点保存一个联系人,结点之间通过 next 指针连接。这样添加联系人时不需要扩容,删除联系人时也不需要移动数组元素。
个人理解与处理: 我把顺序表通讯录和单链表通讯录进行对比,思考如何把原来依赖数组下标的操作改成依赖指针遍历。顺序表版本删除联系人时根据下标调用 SLDeleteSpecify,而链表版本删除联系人时需要记录前驱结点和当前结点。通过这个过程,我对链式存储的插入、删除和遍历有了更清楚的认识。
记录 24:顺序表通讯录和链表通讯录添加功能对比
提问: 顺序表通讯录和链表通讯录在添加联系人时有什么区别?
AI 回答整理: 顺序表通讯录添加联系人时,通常是判断容量是否足够,如果容量满了就扩容,然后把联系人放到数组末尾。链表通讯录添加联系人时,不需要判断容量,也不需要扩容,而是申请一个新结点,把联系人信息存入结点中,再把新结点连接到链表末尾。顺序表添加时主要关注容量问题,链表添加时主要关注结点申请和指针连接问题。
个人理解与处理: 我理解了两种添加方式的区别。顺序表版本添加联系人比较简单,但要考虑扩容;链表版本不需要扩容,但要处理新结点申请和尾部连接。这样可以从实际功能角度体会顺序存储和链式存储的差异。
记录 25:顺序表通讯录和链表通讯录删除功能对比
提问: 顺序表通讯录和链表通讯录在删除联系人时有什么区别?
AI 回答整理: 顺序表通讯录删除联系人时,通常先找到联系人下标,然后把后面的元素依次向前移动,最后 size 减 1。链表通讯录删除联系人时,通常需要同时记录当前结点和前驱结点,找到目标结点后,让前驱结点的 next 指向目标结点的下一个结点,再释放目标结点。如果删除的是第一个结点,还需要单独修改头指针。
关键对比补充: 顺序表删除依赖下标:SLDeleteSpecify(Con, index);链表删除依赖指针:
prev->next = cur->next; free(cur);个人理解与处理: 我认识到删除操作最能体现顺序表和链表的区别。顺序表删除需要移动数据,链表删除需要修改指针。链表删除虽然不用移动大量元素,但要特别注意头结点、空链表、只有一个结点等特殊情况。
记录 26:链表通讯录查找和修改功能设计
提问: 如果通讯录底层改成链表,查找和修改功能应该怎么写?
AI 回答整理: 链表通讯录查找联系人时,需要定义一个临时指针 cur,从头结点开始遍历。每访问一个结点,就比较该结点中的姓名、电话或地址是否与输入内容匹配。如果匹配,就返回该结点指针或直接打印联系人信息。如果要修改联系人,可以先通过查找函数找到目标结点,然后直接修改该结点数据域中的姓名、性别、年龄、电话、地址等字段。链表中没有数组下标,所以操作重点从 “下标” 转为 “结点指针”。
个人理解与处理: 我把顺序表中的按下标访问思路转为链表中的按指针遍历思路。原来修改联系人是Con->a[index],链表中则应该是cur->data。这个变化让我更清楚地理解了数组和链表在访问方式上的区别。
记录 27:顺序表通讯录和链表通讯录的对比总结
提问: 顺序表通讯录和链表通讯录各有什么优缺点?总结里应该怎么写?
AI 回答整理: 顺序表通讯录结构简单,联系人连续存放,按位置访问方便,适合数据量不大、主要进行展示和查找的小型通讯录。但顺序表删除中间元素时需要移动后面的元素,容量不足时还需要扩容。链表通讯录不需要连续空间,也不需要扩容,插入和删除时只需要修改指针,更适合频繁增删联系人或数据量变化较大的情况。但链表不能通过下标直接访问联系人,查找时需要从头遍历,而且每个结点还需要额外的指针空间。
个人理解与处理: 我在总结中把顺序表和单链表的特点进行了对比。顺序表版本适合作为基础项目,便于理解动态数组和增删改查;链表版本则更能体现链式存储的优势,尤其是插入删除时不需要移动大量元素。通过两个版本对比,我对 “线性表逻辑结构相同,但存储结构不同” 这一点理解更深。
记录 28:整理学习过程和交流记录
提问: 我已经把线性表、顺序表、通讯录、单链表学习过程写成学习记录,交流记录应该怎么整理才不像单独拼凑?
AI 回答整理: 交流记录不应该只是一堆零散问答,也不应该把学习笔记和对话完全分开。可以按照学习路线整理:先认识线性表,再学习顺序表,接着实现动态顺序表接口,然后把顺序表用于通讯录项目,在项目中遇到查找、输入、修改、保存等问题,再通过工具辅助调试,最后学习单链表并思考用链表优化通讯录。这样既能体现学习过程,也能体现工具在每个阶段的辅助作用。
个人理解与处理: 我按「学习概念→实现接口→完成项目→发现问题→调试优化→拓展链表→总结对比」的顺序整理交流记录,把调试内容融入整个项目迭代过程中,而不是单独放成一个模块。这样更能完整呈现从学习顺序表到完成通讯录,再到学习单链表优化通讯录的全过程。
三、工具使用与学习总结
3.1 工具使用心得
本次学习过程中,我主要使用 AI 工具辅助学习线性表、动态顺序表、通讯录项目和单链表相关内容。工具在学习过程中主要起到了解释概念、梳理思路、分析代码、辅助调试和总结知识点的作用。
- 基础学习阶段:帮助理清线性表、顺序表和单链表之间的关系,明确顺序存储和链式存储的核心区别,梳理各接口的实现思路。
- 代码实现阶段:提示元素移动方向、边界判断、动态内存管理的注意事项,减少基础语法和逻辑错误。
- 项目开发阶段:指导多文件结构设计、头文件包含规则、结构体类型适配、文件读写的实现方法。
- 调试优化阶段:帮助定位输入缓冲区、字符串处理、函数调用逻辑等隐蔽问题,给出排查方向和解决思路。
- 拓展对比阶段:梳理两种存储结构的差异,整理对比维度,深化对数据结构选型的理解。
注:工具仅作为辅助学习手段,所有代码均经过手动编写、调试与验证,确保完全理解逻辑后再落地到项目中。
3.2 学习完成成果
通过本次专项学习与代码实践,最终完成以下内容:
- 掌握线性表的基本概念和核心特点;
- 理解顺序存储和链式存储的本质区别与适用场景;
- 完整实现动态顺序表的初始化、扩容、插入、删除、查找、修改和销毁全套接口;
- 基于动态顺序表实现完整的 C 语言通讯录项目;
- 完成通讯录添加、删除、查找、修改、显示、保存和加载全功能;
- 修复类型不匹配、输入缓冲区残留、换行符匹配失败等常见字符串问题;
- 优化修改体验,实现回车跳过不修改、性别快捷输入等交互细节;
- 掌握单链表的结点定义、头插、尾插、查找、删除、遍历和销毁全套操作;
- 梳理单链表版本通讯录的实现思路,完成两种存储结构的优缺点对比。
3.3 后续学习方向
目前对链表复杂操作的熟练度还有提升空间,指针边界情况和内存释放顺序仍需加强练习,代码工程化和异常处理能力也有优化空间。后续会继续通过手写代码、运行调试、知识点复盘和习题练习巩固数据结构基础,并进一步学习栈、队列、二叉树等更多数据结构内容。
四、仓库文件说明(第三部分)
Luminous/Luminousbegin(代码仓库地址,点击获取文件)
4.1 文件结构
├── 1.通讯录源代码 ├── SeqList.h # 动态顺序表接口声明 ├── SeqList.c # 动态顺序表接口实现 ├── Contact.h # 通讯录功能声明 ├── Contact.c # 通讯录功能实现 ├── test.c # 主函数、菜单与程序入口 ├── 2.通讯录可执行程序Debug ├──通讯录.exe # 编译好的可执行文件(Windows直接运行) ├── 3.通讯录VSSt2022完整文件 # 所有文件4.2 运行方式
- 直接使用:Windows 系统下双击
通讯录.exe即可运行,数据会自动保存在同目录下的二进制文件中 - 源码编译:使用 gcc 等 C 语言编译器,编译所有.c 文件即可生成可执行程序
bash
gcc SeqList.c Contact.c test.c -o contact.exe
4.3 功能说明
通讯录核心功能
- 联系人增删改查全功能
- 支持按姓名 / 电话 / 地址多条件查找
- 修改支持回车跳过,保留原值
- 性别支持 0/1 快捷输入与自定义输入
- 退出自动保存,启动自动加载历史数据
- 非法输入容错,不会因输入字符导致程序死循环
顺序表 / 单链表接口
- 完整实现增删改查、初始化、销毁等标准接口,代码注释清晰,适合入门学习参考
本项目仅作为学习参考资料,供大家理解数据结构逻辑与 C 语言项目开发思路。请同学们独立完成自己的学习任务哦~