前言
接上篇,让我们正式进入实战项目:基于完整版动态顺序表,手写实现简易通讯录,支持联系人增删改查、查找、修改、保存等功能,把顺序表知识真正落地应用。这期间,我踩了不少坑:编译报错连环炸、运行直接崩溃、输入汉字无限死循环、数据显示乱码...
这篇博客会还原我的真实开发过程,包括每一步的错误想法、踩的坑、排查过程和最终解决方案,所有代码都是我当时一行行写的,保留了新手特有的代码冗余和逻辑漏洞。
一、先回顾:我之前写的通用顺序表代码
通讯录的底层完全复用了我之前写的动态顺序表,这也是后来代码冗余的根源 —— 顺序表是通用设计,但通讯录只用到了其中几个函数。
1.1 顺序表头文件(SeqList.h)
#pragma once #define _CRT_SECURE_NO_WARNINGS #include<stdlib.h> #include<stdio.h> #include<assert.h> #include"Contact.h" // 当时不知道怎么解耦,先留空,后面让通讯录定义 typedef int SLType; // 错误的宏定义,当时以为所有数据都能用%d打印 #define SLTYPE_PRINT_FMT "%d" typedef struct SeqList { SLType* a; // 动态数组 int size; // 有效数据个数 int capacity; // 总容量 }SL; // 当时写了所有通用函数,其实通讯录只用到了5个 void SLInit(SL* ps); void Destroy(SL* ps); void Print(SL* ps); // 完全没用 void SLExp(SL* ps); void PushFront(SL* ps, SLType x); // 没用 void SLPopFront(SL* ps); // 没用 void PushBack(SL* ps, SLType x); // 有用 void SLPopBack(SL* ps); // 没用 int FindSpecify(SL* ps, SLType x); // 没用,结构体不能用==比较 void PushSpecify(SL* ps,int pos, SLType x); // 没用 void SLDeleteSpecify(SL* ps,int pos); // 有用 void Modifyspecified(SL* ps, int pos, SLType x); // 没用1.2 顺序表实现(SeqList.c)
#include"SeqList.h" void SLInit(SL* ps) { assert(ps); ps->a = NULL; ps->capacity = 0; ps->size = 0; } void Destroy(SL* ps) { assert(ps); if (ps->a) { free(ps->a); ps->a = NULL; ps->capacity = 0; ps->size = 0; } } void SLExp(SL* ps) { assert(ps); if (ps->capacity == ps->size) { int Newspace = ps->capacity == 0 ? 4 : 2 * ps->capacity; SLType* Newps = (SLType*)realloc(ps->a, Newspace * sizeof(SLType)); if (Newps == NULL) { perror("扩容失败"); exit(1); } ps->a = Newps; ps->capacity = Newspace; } } void PushBack(SL* ps, SLType x) { assert(ps); SLExp(ps); ps->a[ps->size++] = x; } void SLDeleteSpecify(SL* ps,int pos) { assert(ps); assert(pos >= 0 && pos < ps->size); assert(ps->size > 0); for (int i = pos; i < ps->size - 1; i++) { ps->a[i] = ps->a[i + 1]; } ps->size--; } // 后面还有一堆没用的函数,全部注释掉了,代码冗余严重二、第一步:把顺序表改成通讯录的底层
当时的想法很简单:把顺序表的SLType从int改成联系人结构体就行。结果第一步就踩了头文件循环包含的坑。
2.1 错误的尝试(循环包含)
我直接在SeqList.h里包含Contact.h,然后把SLType改成struct UserData:
// SeqList.h 错误写法 #include"Contact.h" typedef struct UserData SLType;然后Contact.h里又包含SeqList.h:
// Contact.h 错误写法 #include"SeqList.h" typedef struct UserData { char name[100]; // ...其他字段 }UseDat; typedef struct SeqList contact;编译直接报错:"struct SeqList" 重定义。
2.2 解决方案(前向声明)
经过检查,我发现头文件互相包含了,应该用前向声明:
// SeqList.h 正确写法 #pragma once #define _CRT_SECURE_NO_WARNINGS #include<stdlib.h> #include<stdio.h> #include<assert.h> // 前向声明UserData,不用包含Contact.h typedef struct UserData SLType; typedef struct SeqList { SLType* a; int size; int capacity; }SL; // 只保留通讯录用到的函数声明 void SLInit(SL* ps); void Destroy(SL* ps); void SLExp(SL* ps); void PushBack(SL* ps, SLType x); void SLDeleteSpecify(SL* ps,int pos);然后Contact.h正常包含SeqList.h:
// Contact.h 正确写法 #pragma once #define _CRT_SECURE_NO_WARNINGS #include"SeqList.h" #define NAME_Max 100 #define Gender_Max 4 #define AGE_Max 4 #define TEL_Max 15 #define ADDRESS_Max 100 typedef struct UserData { char name[NAME_Max]; char Gender[Gender_Max]; char age[AGE_Max]; // 当时图省事,直接用char数组存年龄,后面踩了大坑 char Tel[TEL_Max]; // 电话也用char数组,因为有11位,int存不下 char Address[ADDRESS_Max]; }UseDat; typedef struct SeqList contact; // 通讯录函数声明 void InitAdd(contact* Con); void AddCon(contact* Con); void DelCon(contact* Con); void ShoCon(contact* Con); void FinCon(contact* Con); void ModDat(contact* Con); void DesDat(contact* Con);终于解决了循环包含的问题,编译通过了第一步。
三、第二步:实现通讯录基础功能
我当时的思路是:先把增删改查的架子搭起来,能跑通就行,不管代码好不好看。
3.1 初始化和文件加载
当时想做数据持久化,程序启动时自动加载之前的联系人:
void LoadCon(contact* Con) { FILE* pf = fopen("contact.txt", "rb"); if (pf == NULL) { // 当时写的是exit(1),第一次运行直接崩溃 perror("打开文件失败"); exit(1); } UseDat data; while (fread(&data, sizeof(UseDat), 1, pf) == 1) { PushBack(Con, data); } printf("历史数据加载完成!\n"); fclose(pf); } void InitAdd(contact* Con) { SLInit(Con); LoadCon(Con); }3.2 添加联系人
void AddCon(contact* Con) { UseDat data; printf("请输入姓名:"); scanf("%s", data.name); printf("请输入性别:"); scanf("%s", data.Gender); printf("请输入年龄:"); // 当时犯了低级错误:age是char数组,用了%d格式符 scanf("%d", data.age); printf("请输入电话:"); // 电话也是char数组,同样用了%d scanf("%d", data.Tel); printf("请输入住址:"); scanf("%s", data.Address); PushBack(Con, data); printf("添加成功!\n"); }3.3 展示通讯录
void ShoCon(contact* Con) { printf("%-10s %-10s %-10s %-15s %-20s\n", "姓名", "性别", "年龄", "电话", "住址"); for (int i = 0; i < Con->size; i++) { // 同样的错误:age和Tel用了%d打印 printf("%-10s %-10s %-10d %-15d %-20s\n", Con->a[i].name, Con->a[i].Gender, Con->a[i].age, Con->a[i].Tel, Con->a[i].Address); } printf("展示完成!\n"); }3.4 删除和修改联系人
// 按姓名查找 int FinNa(contact* Con, char name[]) { for (int i = 0; i < Con->size; i++) { if (strcmp(Con->a[i].name, name) == 0) { return i; } } return -1; } void DelCon(contact* Con) { char name[NAME_Max]; printf("请输入要删除的联系人姓名:"); scanf("%s", name); int index = FinNa(Con, name); if (index < 0) { printf("联系人不存在!\n"); return; } SLDeleteSpecify(Con, index); printf("删除成功!\n"); } void ModDat(contact* Con) { char name[NAME_Max]; printf("请输入要修改的联系人姓名:"); scanf("%s", name); int index = FinNa(Con, name); if (index < 0) { printf("联系人不存在!\n"); return; } // 当时必须全部重输,不能跳过,体验极差 printf("请输入新的姓名:"); scanf("%s", Con->a[index].name); printf("请输入新的性别:"); scanf("%s", Con->a[index].Gender); printf("请输入新的年龄:"); scanf("%d", Con->a[index].age); printf("请输入新的电话:"); scanf("%d", Con->a[index].Tel); printf("请输入新的住址:"); scanf("%s", Con->a[index].Address); printf("修改成功!\n"); }3.5 退出时保存数据
void SaveCon(contact* Con) { FILE* pf = fopen("contact.txt", "wb"); if (pf == NULL) { perror("保存失败"); return; } for (int i = 0; i < Con->size; i++) { fwrite(&Con->a[i], sizeof(UseDat), 1, pf); } printf("数据保存完成!\n"); fclose(pf); } void DesDat(contact* Con) { SaveCon(Con); Destroy(Con); }四、主菜单和第一次运行:连环报错
我写完主菜单,信心满满地点击运行,结果迎来了连环暴击。
4.1 错误 1:无法解析的外部符号 main
报错信息:无法解析的外部符号 main,函数 "int __cdecl invoke_main(void)" 中引用了该符号原因:光顾着写通讯录函数,忘记写main函数了!C 程序必须有main作为入口。解决:加上主菜单和 main 函数:
// test.c #define _CRT_SECURE_NO_WARNINGS #include "SeqList.h" #include"Contact.h" void menu() { contact Con; InitAdd(&Con); int input = 0; do{ printf("欢迎使用通讯录系统!\n"); printf("1.添加 2.删除 3.展示 4.查找 5.修改 6.退出\n"); printf("请输入操作:"); scanf("%d", &input); switch (input) { case 1: AddCon(&Con); break; case 2: DelCon(&Con); break; case 3: ShoCon(&Con); break; case 4: FinCon(&Con); break; case 5: ModDat(&Con); break; case 6: printf("退出系统!\n"); break; default: printf("输入错误!\n"); break; } } while (input != 6); DesDat(&Con); } int main() { menu(); return 0; }4.2 错误 2:运行直接崩溃
现象:点击运行,直接弹出 “程序已停止工作”,控制台显示打开文件失败: No such file or directory原因:第一次运行时contact.txt不存在,LoadCon函数里直接调用exit(1)终止了程序。解决:修改LoadCon,文件不存在时直接返回,不崩溃:
void LoadCon(contact* Con) { FILE* pf = fopen("contact.txt", "rb"); if (pf == NULL) { printf("没有找到历史数据,创建新通讯录!\n"); return; // 不exit,继续运行 } // 后面不变 }4.3 错误 3:输入汉字直接无限循环
现象:主菜单输入汉字,控制台无限刷屏 “输入错误!”原因:scanf("%d", &input)读取汉字失败,汉字留在输入缓冲区,下一次循环又读到同样的脏数据。临时解决:当时还不会写缓冲区清理函数,只能重启程序,后面优化篇会详细解决。
4.4 错误 4:年龄和电话显示乱码
现象:添加联系人后,展示时年龄和电话显示一堆乱码。原因:age和Tel是char数组,但我用了%d格式符输入输出,类型不匹配导致数据错乱。解决:把所有%d改成%s:
// 添加联系人 printf("请输入年龄:"); scanf("%s", data.age); // %d→%s printf("请输入电话:"); scanf("%s", data.Tel); // %d→%s // 展示通讯录 printf("%-10s %-10s %-10s %-15s %-20s\n", Con->a[i].name, Con->a[i].Gender, Con->a[i].age, // %d→%s Con->a[i].Tel, // %d→%s Con->a[i].Address);五、基础版终于跑通了!但问题一大堆
经过一下午的调试,基础版终于能跑了:1.可以添加联系人;2. 可以展示联系人;3. 可以删除和修改;4.退出时自动保存数据。
但是体验差到极致:1. 输入汉字直接死循环;2.修改时必须全部重输,不能跳过;3. 只能按姓名查找,不能按电话 / 地址;4. 性别必须手动输入 “男”“女”,容易输错;5.没有任何输入校验,输入超长会崩溃;6.代码冗余严重,顺序表里一堆没用的函数。
六、通讯录系统运行演示
6.1 程序启动
运行程序后,首先检测本地contact.txt文件,首次运行无历史数据,提示创建新通讯录,随后弹出功能菜单:
6.2 功能分步演示
1. 添加联系人(序号 1)
输入1,依次填写联系人信息:姓名、性别、年龄、电话、住址,回车后提示添加成功。示例:添加联系人【张三】
2. 删除联系人(序号 2)
输入2,输入要删除的姓名,即可精准删除对应联系人。示例:删除联系人【张三】
3. 展示所有联系人(序号 3)
输入3,系统格式化打印通讯录内全部联系人信息,无联系人时提示为空。示例:查看联系人【李四】
4. 修改联系人(序号 5)
输入5,输入待修改的原姓名,依次填写新信息,即可更新联系人全部内容。示例:将【李四】修改为【王五】
修改后再次执行展示功能,可看到信息已更新:
5. 退出系统并保存(序号 6)
输入6,程序自动将当前通讯录数据写入本地contact.txt文件,持久化保存,下次启动可自动加载历史数据。
6.3、核心功能总结
- 动态扩容:联系人数量无上限,顺序表自动扩容;
- 持久化存储:退出自动保存、启动自动加载;
- 精准操作:按姓名实现增删改查,操作简单直观;
- 数据隔离:5 个文件分层编写,结构清晰、易维护。
七、基础版学习总结
- 头文件循环包含是新手高频坑:一定要用前向声明解决,不要互相包含
- scanf 格式符必须和类型严格匹配:char 数组用 % s,int 用 % d,否则必乱码
- 文件操作要考虑不存在的情况:不要直接 exit,要优雅降级
- 输入缓冲区问题是 C 语言的噩梦:新手一定要提前了解
- 通用代码复用会带来冗余:顺序表的通用函数很多在通讯录里没用,但先实现功能再优化。
本篇Gitee链接为Luminous/Luminousbegin
下一篇博客,我会记录如何一步步把这个 “能用但难用” 的基础版,优化成一个真正好用的通讯录系统。