数据结构|线性表(五):C 语言通讯录终版——踩坑总结与完整可运行代码
本文介绍了C语言通讯录项目的终版实现,通过三篇系列文章完整闭环:基础版实现增删查改和文件存储,优化版修复核心bug并新增多条件查找等功能,终版进行代码规范重构和全bug修复。项目采用标准C语言分文件写法,包含头文件声明、功能实现和主程序入口。重点解决了输入异常处理、回车跳过修改、重名校验等常见问题,新增了自动保存、多条件查询等实用功能。通过该项目,开发者可以掌握结构体使用、顺序表实现、文件操作等C语言核心技能,以及调试技巧和工程组织方式。
系列回顾
本系列三篇完整闭环:
- 第一篇(基础版):从零实现增删查改 + 文件存储,踩遍新手所有坑(格式符乱码、文件闪退、输入死循环);
- 第二篇(优化版):修复核心 bug,新增多条件查找、回车跳过修改等进阶功能;
- 本篇(终版):对底层代码做规范重构 + 全 bug 修复,同时加入逐功能详细讲解。
一、工程结构说明
标准 C 语言项目分文件写法,把不同功能的代码拆到不同文件里,方便维护和复用:
| 文件 | 作用 | 包含内容 |
|---|---|---|
| contact.h | 头文件 | 所有宏定义、结构体声明、函数声明(相当于项目的 "说明书") |
| contact.c | 功能实现文件 | 所有通讯录功能的具体代码(增删查改、文件操作等) |
| main.c | 主程序入口 | 菜单界面 + 主循环,只负责调用功能函数 |
二、完整代码 + 逐功能讲解
1. contact.h(头文件:项目声明)
// 防止头文件重复包含(新手必加,否则会报"重定义"错误)
#ifndef __CONTACT_H__
#define __CONTACT_H__
// 引入需要的头文件
#include <stdio.h> // 输入输出
#include <stdlib.h> // 内存管理、exit函数
#include <string.h> // 字符串操作(strcmp、strcpy等)
// 宏定义:统一管理常量,后期修改只改这里
#define MAX_NAME 20 // 姓名最大长度
#define MAX_SEX 6 // 性别最大长度
#define MAX_TEL 12 // 手机号最大长度(11位+终止符)
#define MAX_ADDR 30 // 地址最大长度
#define MAX_SL 1000 // 通讯录最大容量
// 联系人信息结构体:存储一个人的所有信息
typedef struct UserData
{
char name[MAX_NAME]; // 姓名
char sex[MAX_SEX]; // 性别
int age; // 年龄(修复第一篇char数组存年龄的bug)
char tel[MAX_TEL]; // 手机号
char addr[MAX_ADDR]; // 地址
} UserData;
// 通讯录结构体:本质是一个顺序表(数组+有效元素个数)
typedef struct Contact
{
UserData data[MAX_SL]; // 存储所有联系人的数组
int size; // 当前通讯录中有效联系人的个数
} Contact;
// 函数声明:所有功能函数都在这里声明,其他文件才能调用
// 基础功能
void InitContact(Contact* con); // 初始化通讯录
void AddContact(Contact* con); // 添加联系人
void ShowContact(Contact* con); // 展示所有联系人
void DelContact(Contact* con); // 删除联系人
// 新增功能(第二篇实现)
void SearchContact(Contact* con); // 多条件查找(姓名/电话/地址)
void ModifyContact(Contact* con); // 修改联系人(支持回车跳过)
void ClearAllContact(Contact* con);// 清空所有联系人
void SortContact(Contact* con); // 按姓名排序
// 文件持久化功能
void SaveContact(Contact* con); // 保存数据到文件
void LoadContact(Contact* con); // 从文件加载数据
// 工具函数(解决输入bug)
void ClearBuff(); // 清空输入缓冲区(解决死循环)
void InputSkip(char* dest, int maxLen); // 回车跳过输入(不用全部重输)
#endif // !__CONTACT_H__
2. contact.c(功能实现:核心代码)
#include "contact.h"
/************************** 工具函数:解决输入bug **************************/
// 清空输入缓冲区(解决第一篇"输入汉字死循环"的核心bug)
// 原理:把缓冲区里残留的换行、汉字等脏数据全部读走,直到遇到换行符
void ClearBuff()
{
while (getchar() != '\n');
}
// 新增功能:回车跳过输入(第二篇核心优化)
// 作用:修改联系人时,直接回车就保留原来的值,不用全部重输
// 参数:dest-要赋值的目标数组,maxLen-数组最大长度
void InputSkip(char* dest, int maxLen)
{
char tmp[100] = { 0 }; // 临时数组存储输入
fgets(tmp, maxLen, stdin); // 读取一行输入(包括空行)
// 去掉fgets自动读入的换行符
if (tmp[strlen(tmp) - 1] == '\n')
tmp[strlen(tmp) - 1] = '\0';
// 如果输入不为空(不是直接回车),才覆盖原来的值
if (strlen(tmp) > 0)
strcpy(dest, tmp);
}
// 内部工具函数:按姓名查找联系人(复用,避免重复代码)
// 返回值:找到返回下标,没找到返回-1
static int FindByName(Contact* con, char* name)
{
for (int i = 0; i < con->size; i++)
{
if (strcmp(con->data[i].name, name) == 0)
return i;
}
return -1;
}
/************************** 基础功能实现 **************************/
// 初始化通讯录:程序启动时调用,加载历史数据
void InitContact(Contact* con)
{
con->size = 0; // 初始有效个数为0
LoadContact(con); // 从文件加载历史数据(修复第一篇"首次运行闪退"bug)
}
// 添加联系人
void AddContact(Contact* con)
{
// 先判断通讯录是否已满
if (con->size >= MAX_SL)
{
printf("通讯录已满!\n");
return;
}
// 输入姓名(支持带空格的名字,修复第一篇scanf不能输空格的bug)
printf("请输入姓名:");
ClearBuff(); // 先清空缓冲区残留的换行
fgets(con->data[con->size].name, MAX_NAME, stdin);
// 去掉fgets读入的换行符
if (con->data[con->size].name[strlen(con->data[con->size].name) - 1] == '\n')
con->data[con->size].name[strlen(con->data[con->size].name) - 1] = '\0';
// 新增功能:重名校验(防止添加重复联系人)
if (FindByName(con, con->data[con->size].name) != -1)
{
printf("该联系人已存在!\n");
return;
}
// 输入其他信息
printf("请输入性别:");
scanf("%s", con->data[con->size].sex);
printf("请输入年龄:");
scanf("%d", &con->data[con->size].age); // 用int存年龄,修复格式符乱码bug
printf("请输入手机号:");
scanf("%s", con->data[con->size].tel);
printf("请输入地址:");
scanf("%s", con->data[con->size].addr);
con->size++; // 有效个数加1
printf("添加成功!\n");
SaveContact(con); // 添加后自动保存到文件
}
// 展示所有联系人
void ShowContact(Contact* con)
{
if (con->size == 0)
{
printf("通讯录暂无联系人\n");
return;
}
// 打印表头
printf("%-10s %-6s %-4s %-12s %-20s\n", "姓名", "性别", "年龄", "手机号", "地址");
// 遍历打印所有联系人
for (int i = 0; i < con->size; i++)
{
printf("%-10s %-6s %-4d %-12s %-20s\n",
con->data[i].name, con->data[i].sex, con->data[i].age,
con->data[i].tel, con->data[i].addr);
}
}
// 删除联系人
void DelContact(Contact* con)
{
char name[MAX_NAME];
printf("请输入要删除的姓名:");
scanf("%s", name);
// 先查找联系人是否存在
int pos = FindByName(con, name);
if (pos == -1)
{
printf("未找到联系人\n");
return;
}
// 顺序表删除逻辑:后面的元素往前覆盖
for (int i = pos; i < con->size - 1; i++)
con->data[i] = con->data[i + 1];
con->size--; // 有效个数减1
printf("删除成功\n");
SaveContact(con); // 删除后自动保存
}
/************************** 新增功能实现(第二篇) **************************/
// 新增功能:多条件查找(支持按姓名/手机号/地址查找)
void SearchContact(Contact* con)
{
int choose = 0;
printf("1.按姓名查找 2.按手机号查找 3.按地址查找\n请选择查找方式:");
scanf("%d", &choose);
if (choose == 1)
{
// 按姓名查找
char name[MAX_NAME];
printf("请输入查找姓名:");
scanf("%s", name);
int pos = FindByName(con, name);
if (pos != -1)
{
printf("%-10s %-6s %-4d %-12s %-20s\n",
con->data[pos].name, con->data[pos].sex, con->data[pos].age,
con->data[pos].tel, con->data[pos].addr);
}
else printf("未找到联系人\n");
}
else if (choose == 2)
{
// 按手机号查找
char tel[MAX_TEL];
printf("请输入查找手机号:");
scanf("%s", tel);
for (int i = 0; i < con->size; i++)
{
if (strcmp(con->data[i].tel, tel) == 0)
{
printf("%-10s %-6s %-4d %-12s %-20s\n",
con->data[i].name, con->data[i].sex, con->data[i].age,
con->data[i].tel, con->data[i].addr);
return;
}
}
printf("未找到联系人\n");
}
else if (choose == 3)
{
// 按地址查找
char addr[MAX_ADDR];
printf("请输入查找地址:");
scanf("%s", addr);
for (int i = 0; i < con->size; i++)
{
if (strcmp(con->data[i].addr, addr) == 0)
{
printf("%-10s %-6s %-4d %-12s %-20s\n",
con->data[i].name, con->data[i].sex, con->data[i].age,
con->data[i].tel, con->data[i].addr);
return;
}
}
printf("未找到联系人\n");
}
else
{
printf("输入错误!\n");
}
}
// 新增功能:修改联系人(支持回车跳过不修改)
void ModifyContact(Contact* con)
{
char name[MAX_NAME];
printf("请输入要修改的姓名:");
scanf("%s", name);
int pos = FindByName(con, name);
if (pos == -1)
{
printf("未找到联系人\n");
return;
}
printf("修改信息(直接回车=不修改该项)\n");
// 调用InputSkip函数,实现回车跳过
printf("新姓名:");
ClearBuff();
InputSkip(con->data[pos].name, MAX_NAME);
printf("新性别:");
InputSkip(con->data[pos].sex, MAX_SEX);
// 年龄单独处理:int类型不能直接用InputSkip
printf("新年龄:");
char tmp[10] = { 0 };
fgets(tmp, 10, stdin);
if (strlen(tmp) > 1) // 输入不为空才修改
con->data[pos].age = atoi(tmp); // 字符串转int
printf("新手机号:");
InputSkip(con->data[pos].tel, MAX_TEL);
printf("新地址:");
InputSkip(con->data[pos].addr, MAX_ADDR);
printf("修改成功\n");
SaveContact(con); // 修改后自动保存
}
// 新增功能:清空所有联系人
void ClearAllContact(Contact* con)
{
con->size = 0; // 直接把有效个数设为0即可
SaveContact(con); // 保存空数据到文件
printf("已清空所有联系人!\n");
}
// 新增功能:按姓名拼音排序(冒泡排序)
void SortContact(Contact* con)
{
for (int i = 0; i < con->size - 1; i++)
{
for (int j = 0; j < con->size - i - 1; j++)
{
// strcmp比较字符串大小,按拼音升序排列
if (strcmp(con->data[j].name, con->data[j + 1].name) > 0)
{
// 交换两个联系人的位置
UserData temp = con->data[j];
con->data[j] = con->data[j + 1];
con->data[j + 1] = temp;
}
}
}
printf("排序完成!\n");
}
/************************** 文件持久化功能 **************************/
// 保存数据到文件(二进制方式)
void SaveContact(Contact* con)
{
FILE* pf = fopen("contact.dat", "wb"); // wb:二进制写模式
if (pf == NULL)
{
perror("fopen"); // 打印错误信息
return;
}
// 把整个通讯录数组写入文件
fwrite(con->data, sizeof(UserData), con->size, pf);
fclose(pf); // 关闭文件
}
// 从文件加载历史数据(修复第一篇"文件不存在闪退"bug)
void LoadContact(Contact* con)
{
FILE* pf = fopen("contact.dat", "rb"); // rb:二进制读模式
if (pf == NULL)
{
// 文件不存在(第一次运行),直接返回,不崩溃
return;
}
UserData tmp;
// 循环读取文件中的每个联系人
while (fread(&tmp, sizeof(UserData), 1, pf))
{
con->data[con->size] = tmp;
con->size++;
}
fclose(pf); // 关闭文件
}
3. main.c(主程序入口)
#include "contact.h"
void menu()
{
printf("====================\n");
printf("1.添加联系人 2.删除联系人\n");
printf("3.查找联系人 4.修改联系人\n");
printf("5.展示联系人 0.退出程序\n");
printf("====================\n");
printf("请选择:");
}
int main()
{
Contact con; // 创建一个通讯录变量
InitContact(&con); // 初始化通讯录,加载历史数据
int input = 0;
// 主循环:直到用户输入0退出
do
{
menu(); // 打印菜单
scanf("%d", &input); // 读取用户选择
switch (input)
{
case 1:AddContact(&con); break;
case 2:DelContact(&con); break;
case 3:SearchContact(&con); break;
case 4:ModifyContact(&con); break;
case 5:ShowContact(&con); break;
case 0:SaveContact(&con); printf("已保存,退出程序\n"); break;
default:printf("输入错误,请重新选择\n"); ClearBuff(); break;
}
} while (input != 0);
return 0;
}
三、所有新增功能汇总
| 新增功能 | 解决的问题 | 实现位置 |
|---|---|---|
| 多条件查找 | 只能按姓名查找,忘记姓名找不到人 | SearchContact函数 |
| 回车跳过修改 | 修改时必须全部重输,体验极差 | InputSkip工具函数 |
| 重名校验 | 可以添加多个同名联系人 | AddContact函数中调用FindByName |
| 支持空格姓名 | scanf("%s")遇到空格截断 | 用fgets读取姓名 |
| 自动保存数据 | 每次操作后手动保存 | 增删改后自动调用SaveContact |
| 输入异常处理 | 输入汉字 / 字母死循环 | ClearBuff工具函数 |
| 首次运行不闪退 | 文件不存在直接 exit 崩溃 | LoadContact中文件不存在直接返回 |
四、新手学习总结
通过这个完整的通讯录项目,你能掌握以下 C 语言核心技能:
- 结构体的使用:用结构体封装复杂数据
- 顺序表的实现:数组 + 有效元素个数的线性表结构
- 分文件编程:C 语言工程的标准组织方式
- 输入输出处理:
scanf/fgets的区别、缓冲区问题 - 文件操作:二进制读写实现数据持久化
- 调试技巧:定位并解决常见 bug(乱码、闪退、死循环)
五、放在最后
本篇代码GitLuminous/Luminousbegin
通讯录系列正式完结,三篇博客从零基础写代码、踩坑排查、功能优化、标准工程化,完整走完了一个 C 语言小项目的全流程。非常适合大一同学跟着敲、复盘、积累项目经验。下面接着进行数据结构的学习,大家一起加油鸭!