← 返回文章列表

数据结构|线性表(五):C 语言通讯录终版——踩坑总结与完整可运行代码

本文介绍了C语言通讯录项目的终版实现,通过三篇系列文章完整闭环:基础版实现增删查改和文件存储,优化版修复核心bug并新增多条件查找等功能,终版进行代码规范重构和全bug修复。项目采用标准C语言分文件写法,包含头文件声明、功能实现和主程序入口。重点解决了输入异常处理、回车跳过修改、重名校验等常见问题,新增了自动保存、多条件查询等实用功能。通过该项目,开发者可以掌握结构体使用、顺序表实现、文件操作等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 语言小项目的全流程。非常适合大一同学跟着敲、复盘、积累项目经验。下面接着进行数据结构的学习,大家一起加油鸭!