ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

顺序表通讯录进阶:修复输入与实现文件存储

顺序表通讯录进阶:修复输入与实现文件存储 上次那篇我们把顺序表的基本框架搭起来了通讯录的基础功能——添加、删除、显示——跑通了当时的程序确实能运行菜单一按联系人能存进去也能列出来。但说句实话那种程度只能叫“完成了作业”离真正能用的通讯录软件还有不小的距离。当时文章评论区里也有不少人在问为什么我输入电话号码时中间有个空格后面就全乱了为什么程序一关刚才录的十几个人全没了这其实不是代码写错而是顺序表通讯录做到“能用”这个阶段必然会撞上的一批现实问题。这篇续篇就围绕这些问题来补刀核心目标有三个第一把输入那块打磨干净让用户在终端里想怎么输就怎么输第二加上文件存储数据不再因为程序退出而丢第三把查找、修改、排序这些日常操作补齐让顺序表除了“能存”还能“好用”。如果你正在写数据结构实验报告或者刚学完顺序表想拿一个小项目练手这篇可以直接照着改。如果你是考研党在复习408那这篇文章同样值得看顺序表的查找、删除、动态扩容都是常考的点后面我会带你逐个从代码里看它们的实际样子。1. 能跑的代码和能用的通讯录到底差在哪1.1 上一篇做完的基础功能如今差什么先花半分钟回顾一下之前做出来的东西。我们的通讯录基于一个顺序表结构底层就是一块连续内存里的结构体数组每个结构体保存一个联系人的姓名、电话、组别什么的。顺序表的基本操作——初始化、尾插、指定位置删除、遍历打印——已经写好了。菜单循环也能正常跑选1添加选2删除选3显示选0退出。这个阶段的问题不在于“数据结构没实现”而在于“用户根本没法正常用”。我举三个最典型的场景你如果已经上手跑过代码应该会有感觉。第一个场景添加联系人的时候姓名一栏输入“张三 李四”这种带空格的名字回车之后发现程序就好像读了一部分剩下的字符莫名跑到了电话那一栏。第二个场景输入电话之后程序显示“请选择操作”结果你还没按数字菜单就自动跳了过去像是有一个看不见的回车偷偷替你做了一次选择。第三个场景程序里增删改查了一会儿觉得差不多了关掉终端重新打开刚才录进去的人一个都不剩全在内存里蒸发了。这三个场景对应三个层面的问题字符串输入的安全与缓冲区处理、程序的状态持久化、以及通讯录功能闭环的缺失。这篇文章就按这三条线来展开。第一条线解决“输入”的问题第二条线解决“存储”的问题第三条线解决“查找、修改、排序”的问题最后再送你一份常见的踩坑记录。1.2 本篇的四刀切在哪些地方我给这次改造做了个规划一共四刀。第一刀砍掉 scanf 这个不省心的输入函数换用 fgets sscanf 的组合彻底解决空格、回车残留、缓冲区串味这些老毛病。第二刀给通讯录加文件读写能力退出前把数据写进一个文本文件启动时自动加载回来让数据跨会话存活。第三刀补充查找、修改、排序三个操作这三件事在顺序表上各有各的经典实现我会带着你把每一步的原理和代码对齐。第四刀处理容量不足和程序健壮性问题让顺序表在数据量超过初始容量时能自动扩容同时把内存管理的细节补干净。这四刀切完之后你再回头看这个通讯录程序它就不只是一个数据结构课的作业了而是一个真正有“产品雏形”的小工具。你可以拿它当数组、指针、文件操作、字符串处理的综合复习材料也可以只提取其中某一块的代码思路用到别处。接下来我们一刀一刀来。2. 第一刀把 scanf 丢掉换 fgets 接管字符串输入2.1 为什么通讯录里用 scanf 是个坑很多教材在讲 scanf 的时候用的都是最简单的例子读一个整数、读一个不含空格的单词看起来一切正常。但通讯录不是这种场景。你让用户输入姓名凭什么规定人家不能姓“欧阳”名“娜娜”中间带个空格现实是姓名字符串里完全可能出现空格。而 scanf(%s, name) 的语义恰好是“读到空白字符就停”所以输入“张三 李四”时它只读走了“张三”剩下的“ 李四”还躺在输入缓冲区里下一个 scanf 读到电话时就直接把这个残渣吃进去了。比空格更要命的是回车残留。scanf 在读取数据时遇到匹配失败或者读完一个数据后会把后面的换行符留在缓冲区里。比如你连续写了两个 scanf第一个读字符串第二个读整数。用户输完字符串敲回车字符串被正确读走了但那个回车还留在缓冲区。轮到第二个 scanf(%d) 读整数时它一看缓冲区开头是个换行符直接匹配失败返回了程序不报错只是整数读了个寂寞变量保持原来的值或者未初始化的垃圾值。于是菜单像被人按了快进键一样自己就跳过去了。这个问题在学C语言的头几个月几乎人人都会踩但很少有人会停下来想一想scanf 到底能不能在交互式输入里做到靠谱答案是它能但需要你小心翼翼地配合清空缓冲区。可清空缓冲区这件事本身又没有标准函数于是很多人只能用 getchar() 循环吃掉多余字符。代码丑不说万一缓冲区里同时有回车又有空格循环条件写不对照样翻车。换 fgets 是更省心的路线。2.2 fgets 的正确读法以及必须处理的换行符fgets 的签名是 char *fgets(char *s, int size, FILE *stream)它从 stream 中最多读 size-1 个字符遇到换行或 EOF 就停下来并在字符串末尾自动加 \0。和 scanf 最大的区别是fgets 会把整一行都吃进去包括中间的空格和末尾的换行符。也就是说不管用户输入“张三”还是“张三 李四”fgets 都能完整拿走。但这里有一个新手必踩的坑fgets 读进来的字符串末尾带着一个 \n。如果我们直接把它存进联系人的 name 数组里那打印的时候倒是看不出来但在做字符串比较strcmp的时候就会翻车——你拿“张三”去和“张三\n”比较结果永远不等于0。所以拿到 fgets 的结果后第一步是把末尾的换行符清理掉。我习惯用 strcspn 这个函数来定位换行符的位置然后把那里替换成 \0。char buf[50]; fgets(buf, sizeof(buf), stdin); buf[strcspn(buf, \n)] \0; // 找到换行符的位置并替换成字符串结束符这里有个细节值得说明。strcspn(buf, \n) 返回的是 buf 中第一次出现 \n 的下标如果没找到返回的是字符串长度。无论是哪种情况把它作为下标赋 \0 都是安全的。有些人喜欢用 strlen(buf) - 1 然后赋值 \0但万一用户输入超长导致 fgets 没读到换行strlen(buf)-1 指向的就不是换行符而是最后一个字符会误杀内容。所以 strcspn 的写法更稳。我一般会把这个操作封装成一个工具函数避免每次输入都重复一遍void read_line(char *dest, int size) { fgets(dest, size, stdin); dest[strcspn(dest, \n)] \0; }调用的时候就很简单了read_line(contact[i].name, sizeof(contact[i].name));。注意sizeof 在数组上才是安全的写法如果 got 一个指针要传长度参数这就是为什么函数里要留 size 参数的原因。2.3 菜单整数的读取也用字符串中转字符串交给我们刚才的 read_line 解决了但菜单选择需要读的是整数。现在的问题是如果直接开一个 int opt; scanf(%d,opt); 那之前的缓冲区问题又会回来。解决办法也简单把菜单输入也当成字符串来读然后用 sscanf 或者 atoi 从字符串里解析出整数。char line[16]; read_line(line, sizeof(line)); int opt atoi(line);用 atoi 的坏处是它无法区分“输入错误”和“输入0”因为非法输入它返回0输入0它也返回0。所以严格一点应该用 sscanf 的返回值来判断int opt -1; char line[16]; read_line(line, sizeof(line)); if (sscanf(line, %d, opt) ! 1) { printf(无效输入请重新选择。\n); continue; }这段代码的含义是sscanf 从 line 字符串里按 %d 格式提取一个整数如果成功提取到返回1没提取到就返回0。这样你就知道用户是不是真的给了一个数。用这个组合菜单里不管用户输入的是“3”、“3 后面乱敲的东西”还是直接回车都不会导致程序失控。菜单输入这关就算彻底过了。3. 第二刀给通讯录加上文件存取让数据不丢3.1 存储格式纯文本优先二进制先放一边决定做文件持久化时第一个要拍板的问题是联系人数据存成什么格式。这个选择直接决定后面读写代码的复杂度和可调试性。方案一是二进制格式直接用 fwrite 把整个结构体数组原封不动写入文件。代码量最少读写也快但坑很多结构体里有 char 数组成员时编译器可能会在成员之间插入填充字节struct padding导致同一个结构体在不同编译器甚至同一个编译器的不同优化选项下写出来的二进制文件字节布局不一致。你今天用一个编译器写的文件明天换了编译器可能就读不回来了。而且二进制文件你用记事本打开全是乱码出了问题排查起来很难受。方案二是纯文本格式每个联系人占一行或几行字段之间用分隔符隔开比如“姓名,电话,组别”。代码量稍微多一点但文件是明文你能直接 cat 出来看到保存的内容对不对出错了也知道去哪改。对于学习阶段的小项目我强烈推荐文本格式。后面你做大项目的时候再根据性能需求去选二进制。我的文本格式设计得很简单每个联系人占一行字段之间用逗号分隔。保存的时候一个 fprintf 就能写一行加载的时候用 fgets 读整行再用 sscanf 按逗号解析。注意为了让 sscanf 能按逗号解析名字和电话里最好不能出现未转义的逗号这个限制在通讯录场景下完全可接受。3.2 保存把顺序表遍历一遍写进文件保存函数的逻辑不复杂打开文件遍历顺序表里的每个有效元素按约定格式逐行写入最后关闭文件。要处理好的只有一个点——打开文件失败的情况比如磁盘满了或者目录没有写权限fopen 会返回 NULL这时候不能直接往 NULL 指针上写数据否则程序立刻崩溃。int save_contacts(const char *filename, Contact *list, int count) { FILE *fp fopen(filename, w); if (fp NULL) { perror(无法打开文件); return -1; } for (int i 0; i count; i) { fprintf(fp, %s,%s,%s\n, list[i].name, list[i].phone, list[i].group); } fclose(fp); return 0; }这里有个经验值得说fprintf 的格式字符串里%s 之间用逗号分隔结尾写一个换行符。这样每条记录正好占一行后面加载的时候一行一行地读天然对齐。字段顺序要和加载代码保持严格一致一旦两个函数之间约定不一致存进去的数据就乱了。所以我习惯把格式定义成宏注释里写清楚// 存储格式约定每行 name,phone,group逗号为分隔符 #define CONTACT_LINE_FORMAT %s,%s,%s\n宏的好处是保存和加载两处都用同一个格式串改起来也只改一处不会改漏。3.3 加载从文件里逐行还原联系人加载函数要处理的事情多一点打开文件、逐行读取、按逗号切分、构造联系人结构体、判断是否已经存满。逐行读用一个 fgets 循环但这里要小心一个语文题fgets 是按“读到换行就停”来工作的所以如果文件最后一行没有换行符它也会正常返回。循环条件要写成 while (fgets(line, sizeof(line), fp) ! NULL)。按逗号切分最稳妥的方法是用 sscanf。比如行内容是“张三,13800138000,朋友”那 sscanf(line, %[^,],%[^,],%[^,\n], name, phone, group) 就能把三个字段拆出来。%[^,] 表示“读取所有不是逗号的字符”这是 C 风格正则里比较常用的一个技巧。char line[128]; while (fgets(line, sizeof(line), fp) ! NULL) { Contact c; if (sscanf(line, %[^,],%[^,],%[^,\n], c.name, c.phone, c.group) 3) { list[count] c; } else { printf(警告第 %d 行格式不正确已跳过。\n, line_no); } }注意 sscanf 的返回值是成功转换的参数个数这里必须是3。如果某一行格式不对我们应该跳过而不是终止整个加载过程这样即使文件末尾有一行空行或者手误改出来的坏数据程序也能尽量恢复已读到的正确内容。不过加载前要记得先检查容量如果文件很大而容量不够会出现数组越界。容量问题我们放到第五刀再处理这里先提个醒。3.4 让程序自动完成“退出保存、启动加载”有了保存和加载两个函数剩下的事情就是把它们挂到程序的生命周期上程序启动后先把全局联系人列表从默认文件里 load 一遍菜单循环正常跑用户选择退出时先 save 再退出。为了让用户少点几次操作最好不要让用户手动指定文件名而是固定一个默认名字比如 contact_book.txt。int main(void) { Contact list[MAX_CAPACITY]; int count 0; load_contacts(DATA_FILE, list, count); while (1) { // 菜单 分支处理 } save_contacts(DATA_FILE, list, count); return 0; }这样整个交互就变成了打开程序之前存的人都在改了一通退出时自动保存下次再打开数据无缝衔接。从用户的角度看通讯录真正变成“长期有效”的东西了。这也是大多数人第一次感受到“程序状态跨会话存在”的时刻朴素但很关键。4. 第三刀补上查找、修改、排序真正把顺序表用起来4.1 按姓名查找strcmp 逐个比对别用 查找一个联系人核心是遍历顺序表逐个用 strcmp 比较名字。注意这里不能写成 if (list[i].name target)因为在 C 语言里两个 char 数组之间用 比较的是数组首元素的地址地址不可能相等所以这种比较永远为假。这是新手最常见的逻辑错误之一。int find_contact(Contact *list, int count, const char *name, int *positions, int max) { int found 0; for (int i 0; i count found max; i) { if (strcmp(list[i].name, name) 0) { positions[found] i; } } return found; }这里我让函数接收一个 positions 数组把查到的所有下标都存进去而不是只返回第一个。为什么因为通讯录里两个“张三”是完全正常的你返回第一个就漏了第二个。返回所有匹配位置的集合后续的删除、修改就能一次性处理所有的同名联系人。查找的时间复杂度是 O(n)。如果你以后数据量大到几千几万条可以考虑把顺序表换成二叉搜索树或者哈希表但在通讯录这种个人场景下线性查找完全够用。这个“数据结构选型要看场景”的道理是这门课反复强调的这里正好体验一下。4.2 修改联系人先定位后覆写顺序表天然支持随机访问修改功能很像数组的“改”操作。顺序表底层是数组所以支持 O(1) 的随机访问直接取下标就能改。这个特性是链表做起来更麻烦的地方链表需要先遍历到目标位置才能改。所以修这个功能你其实是在感受顺序表的最大优势。修改的流程分三步先让用户输入要查找的姓名然后列出查到的联系人让用户选择修改哪个如果有多个最后把该联系人的字段重新用 read_line 读一遍覆盖进去。if (find_result 0) { printf(匹配到 %d 个联系人请选择要修改的编号, find_result); int idx; // 读取 idx Contact *p list[positions[idx]]; printf(新姓名); read_line(p-name, sizeof(p-name)); printf(新电话); read_line(p-phone, sizeof(p-phone)); printf(新组别); read_line(p-group, sizeof(p-group)); }代码里有个常用的模式Contact *p list[positions[idx]];。把数组取地址赋给指针后后面用 p- 来访问字段代码会简洁很多而且语义清晰——p 指向我们要修改的那个元素本体改 p 就是改数组里的数据。这个“指针即别名”的思路在 C 语言里几乎是万能解。4.3 排序先懂冒泡的套路再用 qsort 收尾排序是通讯录里的一个常被忽略但很实用的功能按姓名排个字典序这样电话本才好翻。教材里讲到排序算法时喜欢从冒泡排序开始因为它代码直观能让你理解“比较—交换”这个基本动作。我这里也先给出冒泡的版本因为它的思路对你理解时间复杂度很有帮助。void sort_contacts(Contact *list, int count) { for (int i 0; i count - 1; i) { for (int j 0; j count - 1 - i; j) { if (strcmp(list[j].name, list[j1].name) 0) { Contact tmp list[j]; list[j] list[j1]; list[j1] tmp; } } } }冒泡排序的比较次数大约是 O(n²)数据一多就很吃力。但工程上我们不需要手写排序C 标准库提供了 qsort它的底层是快速排序平均复杂度 O(n log n)。关键是写对比较函数。比较函数接收两个 const void * 参数内部要转成 Contact* 再比较返回值遵循“小于0、等于0、大于0”的约定。int compare_by_name(const void *a, const void *b) { const Contact *ca (const Contact *)a; const Contact *cb (const Contact *)b; return strcmp(ca-name, cb-name); } // 调用qsort(list, count, sizeof(Contact), compare_by_name);这里想提醒两点。第一strcmp 返回的值本身就是一个“负、零、正”的整数和 qsort 要求完全吻合直接 return strcmp(...) 即可不要再画蛇添足做 return strcmp(...) 0 ? 1 : -1。第二qsort 的比较函数里如果要按电话排序就换成 strcmp(ca-phone, cb-phone)想倒序就在返回值前面加个负号非常灵活。如果你在做数据结构实验报告我建议你在报告里写清楚冒泡是教学演示说明排序思路qsort 是工程实现展示标准库的调用。两者都写上老师会觉得你既有底层意识又有工程习惯。4.4 把新功能整合进菜单流程闭环就这样菜单里多出了“查找联系人”“修改联系人”“排序显示”三个选项。整个菜单循环的逻辑会变得越来越清晰。我建议你把每个功能都拆成一个函数菜单里只做输入和调用。这样 main 函数不会膨胀读代码的人也能一眼看到业务逻辑。while (1) { show_menu(); int opt read_menu_option(); switch (opt) { case 1: add_contact(); break; case 2: delete_contact(); break; case 3: search_contact(); break; case 4: modify_contact(); break; case 5: sort_contacts(); break; case 6: list_all(); break; case 0: return 0; default: printf(无效选项\n); } }到这一步通讯录的功能已经比很多课程设计的作业要完整了。不过别急着收工第四刀的内容也很关键容量、内存、文件组织都是后面面试和实操里经常聊到的话题。5. 第四刀让顺序表学会扩容把内存和工程细节收拾干净5.1 容量满了怎么办从固定数组到动态扩容很多学生写的顺序表是静态数组版提前定一个 MAX_CAPACITY比如100。这在数据量小的时候能跑可一旦通讯录录入第101个人程序就崩溃或者直接报错“联系人已满”。真实的需求哪有“100人上限”这种说法所以动态扩容几乎是必须的。动态扩容的核心是 realloc。当 count 等于 capacity 时申请一块更大的内存我习惯按原来的2倍扩容把旧数据搬运过去释放旧内存更新数组指针和容量。倍增而不是每次加1的原因很现实每次加1必然导致频繁的 realloc而 realloc 可能涉及内存拷贝和搬家代价高倍增能让扩容次数从 O(n) 降到 O(log n)总的时间开销摊下来是线性的这就是数据结构里说的“均摊复杂度”。void ensure_capacity(Contact **list, int *capacity, int count) { if (count *capacity) return; int new_cap (*capacity 0) ? 4 : (*capacity) * 2; Contact *new_list (Contact *)realloc(*list, new_cap * sizeof(Contact)); if (new_list NULL) { printf(内存不足扩容失败。\n); exit(1); } *list new_list; *capacity new_cap; }要注意一个常见错误不能直接写成list (Contact)realloc(*list, ...)因为 realloc 失败时返回 NULL同时原内存还没释放。一旦你把 NULL 赋给 *list原来的数组指针就丢了后续既无法访问数据也无法释放内存。所以必须用一个临时变量接住 realloc 的返回值判断非空后再赋值给 *list。这是 C 语言里 realloc 的经典陷阱面试也爱考。配合扩容所有使用顺序表的地方都要把“数组名”改成“指针容量”的组合。函数签名要跟着变。如果你一开始就写的是静态数组版这次改造会涉及好多个函数签名稍微繁琐但值得。5.2 文件加载时的容量联动文件加载逻辑也需要配合扩容。以前我们假设 MAX_CAPACITY 足够大现在要改为每次要往数组里放新元素之前先 llamar ensure_capacity 检查一下。加载函数没办法提前知道文件里有多少行所以变成边读边检查的模式。while (fgets(line, sizeof(line), fp) ! NULL) { ensure_capacity(list, capacity, count); Contact c; if (sscanf(...) 3) { list[count] c; } }这样哪怕文件里有1000行程序也能全部读进来而不是被固定容量卡死。一个小细节读取文件时如果文件里最后有一个空行sscanf 会失败程序会打印一条“格式不正确”的警告。空行在纯文本存储里太常见了所以我一般会把警告级别放低或者只在格式错误时打印空行静默跳过避免用户看到一堆无意义的信息。5.3 程序退出前把内存归还系统动态扩容之后主函数退出前必须 free(list)否则程序一退出操作系统虽然会回收进程的内存但你在长周期运行或者把这个逻辑嵌到服务器代码里时不 free 就会内存泄漏。写 C 程序的习惯应该是谁 malloc 谁 free谁 realloc 谁负责。我们只在 main 里动了这块内存就在 main 结束时释放。save_contacts(DATA_FILE, list, count); free(list); return 0;有些同学会问反正程序退出操作系统会回收我不写 free 行不行行但你是在给自己埋雷。一旦思路迁移到一直在跑的程序后台服务、嵌入式设备不释放内存就会越积越多。把这个习惯建立起来比具体某一次 free 更重要。5.4 代码拆文件头文件、顺序表模块、通讯录模块最后顺手讲一下工程组织。到了这个规模全部代码塞进一个 main.c 已经有点拥挤了。我建议拆成三个文件seqlist.h / seqlist.c顺序表的核心操作初始化、扩容、插入、删除、遍历这些是针对“任意元素顺序表”的通用代码不关心元素是不是联系人。contact.h / contact.c通讯录业务逻辑输入姓名电话、保存文件、加载文件、按姓名查找。这些代码依赖 seqlist 提供的接口。main.c菜单和程序入口。拆文件的好处是复用。你以后写图书管理系统也好写学生成绩管理也好seqlist 这套代码可以直接搬过去只改元素结构体就行。C 语言里那种“造一个通用容器”的思路就是从这种模块划分开始的。如果你在做课程设计一个结构清晰的多文件工程在答辩时是非常加分的。6. 常见问题与排查实录6.1 菜单输入被“跳过”一个回车引发的血案症状添加完一个联系人后回到主菜单用户还没按任何键程序就像自己按了一次回车一样菜单一闪而过。原因printf 提示“按回车继续”时用户敲的回车符被遗留在缓冲区下一个 fgets 本来是想读菜单选项字符串的结果一上来就读到了那个残留的换行符直接把 opt 解析成了无效值。排查在 read_line 里打印调试信息看到读进来的 line 是不是空字符串。修复方案有两个一是每次 read_line 前手动清空缓冲区不推荐可移植性差二是在菜单输入之后用一个循环如果解析失败就让用户重新输入。我们的 read_menu_option 逻辑里已经有 continue 了所以只要确保 sscanf 失败时会提示并重新读这个问题就解决了。6.2 程序崩溃多半是下标越界症状录了十几个联系人后程序突然崩了或者打印出一些奇怪的乱码。原因八成是数组下标越界。比如删除联系人时逻辑写成了 memmove 之后没有把 count 减1或者加载文件时忘了检查容量导致 list[count] 越界还有可能是 sort 函数里循环边界写错j count - 1 - i 写成 j count - i。排查在关键位置打印 count 和 i看遍历时是否越界。或者用 valgrind 跑一遍程序它会精确报出是第几行越界的。说实话valgrind 这个工具值得所有学 C 的人用一次报错输出虽然一开始看着吓人但用顺手之后找 bug 快得惊人。6.3 文件加载后数据重复每次都叠加一遍症状启动程序发现联系人数量比上次保存的多了一倍。仔细看全是重复的。原因main 里启动时 load 了一次后来某个菜单逻辑里又 load 了一次两次加载没有做“覆盖写”而是“追加写”。修复加载函数里先 count 0保证每次加载都是从空列表开始。这是一个典型的初始化遗漏。6.4 中文联系人乱码症状程序里的中文字符串在终端显示正常但写进文件再读出来用记事本打开全是乱码。原因编码问题。终端里通常用的是 UTF-8 或 GBK文件里存的是程序运行时采用的编码。只要读写前后编码一致程序内自洽一般不乱。乱码多发生在 Windows 记事本打开 UTF-8 文件、或 Linux 终端打开 GBK 文件时。解决思路明确你的源代码和运行环境用什么编码存储文件保持同一编码展示端也统一。跨平台时优先 UTF-8。6.5 排序后联系人丢失或错乱症状调用 qsort 后联系人顺序乱了甚至有些联系人“消失”了。原因qsort 的参数搞错。最常见的是 sizeof(Contact) 写成了 sizeof(Contact*) 导致 qsort 按指针大小切分内存整个排序就全乱了。另一个原因是比较函数里把 const void* 转错了类型导致内存读取越界。排查打印 sizeof(Contact) 的实际值再检查比较函数的强制类型转换。只要这两个点对齐qsort 基本不会出问题。写在最后的一个小建议我实操下来最大的体会是顺序表通讯录这个项目真正难的不是顺序表本身而是怎么把它做出“能用”的感觉。很多同学卡在 scanf 和缓冲区上面就放弃了很可惜因为只要跨过那道坎后面就是一片坦途。如果这篇文章能让你少走一次弯路那就值了。代码不要求一次写对照着这个思路多跑几遍出了问题就一步步打日志排查这个过程本身就是最好的复习。后续你还可以把它继续扩展加个分组筛选把查找改成支持模糊匹配甚至接一个图形界面。但核心的顺序表功夫就在这里了。
返回列表