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 语言小项目的全流程。非常适合大一同学跟着敲、复盘、积累项目经验。下面接着进行数据结构的学习,大家一起加油鸭!
