当前位置: 首页 > news >正文

C 语言通讯录(终版)|新手踩坑全总结 + 最终可运行代码博客简介

系列回顾

本系列三篇完整闭环:

  1. 第一篇(基础版):从零实现增删查改 + 文件存储,踩遍新手所有坑(格式符乱码、文件闪退、输入死循环);
  2. 第二篇(优化版):修复核心 bug,新增多条件查找、回车跳过修改等进阶功能;
  3. 本篇(终版):对底层代码做规范重构 + 全 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 语言核心技能:

  1. 结构体的使用:用结构体封装复杂数据
  2. 顺序表的实现:数组 + 有效元素个数的线性表结构
  3. 分文件编程:C 语言工程的标准组织方式
  4. 输入输出处理scanf/fgets的区别、缓冲区问题
  5. 文件操作:二进制读写实现数据持久化
  6. 调试技巧:定位并解决常见 bug(乱码、闪退、死循环)

五、放在最后

本篇代码GitLuminous/Luminousbegin

通讯录系列正式完结,三篇博客从零基础写代码、踩坑排查、功能优化、标准工程化,完整走完了一个 C 语言小项目的全流程。非常适合大一同学跟着敲、复盘、积累项目经验。下面接着进行数据结构的学习,大家一起加油鸭!

http://www.jsqmd.com/news/862805/

相关文章:

  • MySql存储引擎与索引
  • AI API 实践三:为什么要关注 Token,而不只是请求次数?
  • 淮南家长必看:淮南哪里学少儿编程靠谱?原来这样选才不踩坑。
  • 油雾净化设备哪家技术更专业
  • VMware虚拟机安装及配置
  • AI API 中转站完全指南:从 Claude、GPT 到“满血”“翻车”,一次搞懂整个 AI API 圈子
  • 2026年想做美缝施工?专业靠谱的美缝施工究竟哪家好?
  • 阿盖洛印相×真实银盐底片对比实测:27组DxO基准图像分析证明——MJ v6.2已逼近1930年代Kodak Azo纸动态范围(附测试集下载)
  • 一幅精细绝伦的[城市或地点]微缩模型
  • 从CDP“3A”到千亿美元目标:联想集团的创新路径与AI原生转型
  • python中二维数组初始化陷阱
  • (QBuffer配合 QDataStream)二进制序列化
  • 影刀RPA 从0到1:自动化系统架构收敛与工程化演进总结
  • 面向诊断场景的云产品知识库设计方案
  • 今日实测有效的淘宝闪购外卖/京东外卖/美团外卖红包天天领取口令怎么领今天可用的外卖红包神券?
  • GPT5.5位置编码从绝对到相对的演进这个变化影响了上下文质量
  • 如何找到最适合你的私有化IM?
  • DDD 中的代码组织:按技术层分 vs 按领域模块分,哪种才是正解?
  • Light: Science Applications | 从平坦能带到量子行走:非阿贝尔Thouless泵浦的新篇章
  • 搜索引擎精准找免费行业报告?掌握这些关键词技巧就够了
  • 随钻连斜传感器操作手册:定向探管安装调试、故障排查与保养要点
  • 2026最新诚信优选 安庆市迎江区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 如何让Mac永不休眠:自动鼠标移动器的终极指南
  • 【零基础部署】Docker 部署 n8n 自动化工作流保姆级教程
  • 深入解析Hash碰撞:原理、成因与主流解决方案
  • 今天实测有效!2026淘宝京东天猫618红包领取口令最新推荐怎么天天领618淘宝京东天猫红包?
  • 2026最新诚信优选 安顺市平坝区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026最新诚信优选 安顺市西秀区黄金回收白银回收铂金回收彩金回收门店TOP5排行榜+联系方式推荐_转自TXT - 盛世金银回收
  • 2026年设计行业必备!兴弘实战设计培训班速成班究竟有多牛?
  • HYPE分布式水文模型建模方法与案例分析实践技术应用:精准完成子流域划分;系统解锁土地利用、土壤数据提取技巧