前言
接上篇,让我们正式进入实战项目 :基于完整版动态顺序表,手写实现简易通讯录,支持联系人增删改查、查找、修改、保存等功能,把顺序表知识真正落地应用。这期间,我踩了不少坑:编译报错连环炸、运行直接崩溃、输入汉字无限死循环、数据显示乱码...
这篇博客会100% 还原我的真实开发过程,包括每一步的错误想法、踩的坑、排查过程和最终解决方案,所有代码都是我当时一行行写的,保留了新手特有的代码冗余和逻辑漏洞。
一、先回顾:我之前写的通用顺序表代码
通讯录的底层完全复用了我之前写的动态顺序表,这也是后来代码冗余的根源 ------ 顺序表是通用设计,但通讯录只用到了其中几个函数。
1.1 顺序表头文件(SeqList.h)
cpp
#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)
cpp
#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:
cpp
// SeqList.h 错误写法
#include"Contact.h"
typedef struct UserData SLType;
然后Contact.h里又包含SeqList.h:
cpp
// Contact.h 错误写法
#include"SeqList.h"
typedef struct UserData
{
char name[100];
// ...其他字段
}UseDat;
typedef struct SeqList contact;
编译直接报错:"struct SeqList" 重定义。
2.2 解决方案(前向声明)
经过检查,我发现头文件互相包含了,应该用前向声明:
cpp
// 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:
cpp
// 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 初始化和文件加载
当时想做数据持久化,程序启动时自动加载之前的联系人:
cpp
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 添加联系人
cpp
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 展示通讯录
cpp
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 删除和修改联系人
cpp
// 按姓名查找
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 退出时保存数据
cpp
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 函数:
cpp
// 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,文件不存在时直接返回,不崩溃:
cpp
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:
cpp
// 添加联系人
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
下一篇博客,我会记录如何一步步把这个 "能用但难用" 的基础版,优化成一个真正好用的通讯录系统。