数据结构 - 线性表第四篇:C 语言通讯录优化升级全记录(踩坑 + 思考)

前言

基础版通讯录虽然跑通了,但每次用都挺折磨:输入汉字死循环、修改时要全部重输、只能按姓名查找... 这篇博客会记录我按优先级一步步优化的全过程,包括每一个功能的需求来源、遇到的问题、解决方案和代码修改,所有优化都是我当时实际遇到痛点后才做的。

一、优化优先级排序

我把问题按严重程度排了个序,先解决最致命的:

  1. 最高优先级:输入汉字死循环(直接导致程序无法使用)
  2. 次高优先级:修改时必须全部重输(体验最差)
  3. 功能扩展:支持按电话 / 地址查找(最常用的功能缺失)
  4. 细节优化:性别输入简化(手动输入男女太麻烦)
  5. 终极修复:查找功能的连环坑(输入名字报错、明明存在却找不到)
  6. bug 修复:自定义修改无效
  7. 代码健壮性:消除所有编译警告和逻辑漏洞

二、第一优先级:彻底解决输入汉字死循环

这是我当时最头疼的问题,查了资料才搞懂原理。

2.1 问题根源

当用scanf("%d", &input)读取数字时:

  • 如果输入数字:scanf 读取成功,返回 1,数字从缓冲区移除
  • 如果输入汉字 / 字母:scanf 读取失败,返回 0,汉字永远留在缓冲区
  • 下一次循环,scanf 又会读到同一个汉字,再次失败→无限循环

2.2 解决方案:缓冲区清理函数

写一个通用函数,清空输入缓冲区里的所有脏数据:

cpp 复制代码
// Contacts.c 顶部新增
void ClearBuff()
{
	int c;
	// 读到换行或EOF为止,避免缓冲区为空时阻塞
	while ((c = getchar()) != '\n' && c != EOF);
}

2.3 在所有 scanf 失败的地方调用

cpp 复制代码
// 主菜单
if (scanf("%d", &input) != 1)
{
	ClearBuff(); // 清空脏数据
	printf("输入错误!请输入数字\n");
	continue;
}

// 后面所有的scanf("%d")都加上这个判断

效果:输入汉字 / 字母后,只会提示一次错误,然后重新等待输入,再也不会死循环了!

三、次高优先级:修改时回车跳过不修改

当时修改联系人的体验极差:只想改个电话,却要把姓名、性别、年龄全部重输一遍。我想实现:不想改哪一项,直接按回车,就保留原来的值

3.1 问题分析

scanf("%s")会自动跳过空格和换行,无法读取空输入。所以必须用fgets,它可以读取一行,包括空行。

3.2 实现 InputSkip 工具函数

cpp 复制代码
#define MAX_INPUT 200
void InputSkip(char* buf, int len, char* oldVal)
{
	// 先清空之前的缓冲区(因为上一个scanf会留下换行)
	ClearBuff();
	
	char temp[MAX_INPUT];
	// 读取一行输入
	fgets(temp, len, stdin);
	// 去掉fgets读进来的换行符
	temp[strcspn(temp, "\n")] = '\0';

	// 如果输入为空(直接回车),保留原来的值
	if (strlen(temp) == 0)
	{
		strncpy(buf, oldVal, len - 1);
		buf[len - 1] = '\0'; // 强制加终止符,防止越界
	}
	// 否则用新值覆盖
	else
	{
		strncpy(buf, temp, len - 1);
		buf[len - 1] = '\0';
	}
}

3.3 修改 ModDat 函数

cpp 复制代码
void ModDat(contact* Con)
{
	printf("提示:不需要修改的项直接回车即可\n");
	char name[NAME_Max];
	printf("请输入要修改的联系人姓名:");
	scanf("%s", name);
	int index = FinNa(Con, name);
	if (index < 0)
	{
		printf("联系人不存在!\n");
		return;
	}

	printf("新姓名:");
	InputSkip(Con->a[index].name, NAME_Max, Con->a[index].name);
	
	printf("新性别:");
	char buf[20] = {0};
	InputSkip(buf, 20, Con->a[index].Gender);
	strcpy(Con->a[index].Gender, buf);
	
	printf("新年龄:");
	InputSkip(Con->a[index].age, AGE_Max, Con->a[index].age);
	
	printf("新电话:");
	InputSkip(Con->a[index].Tel, TEL_Max, Con->a[index].Tel);
	
	printf("新住址:");
	InputSkip(Con->a[index].Address, ADDRESS_Max, Con->a[index].Address);
	
	printf("修改成功!\n");
}

效果:修改时,哪一项不想改,直接按回车就行,体验提升了 10 倍!

四、功能扩展:多条件查找(默认姓名 + 二级菜单)

原来只能按姓名查找,有时候忘记姓名只记得电话,根本找不到人。我想实现:

  • 默认直接按姓名查找(最常用,不用选菜单)
  • 输入 1 弹出二级菜单,支持按电话 / 地址查找
  • 支持退出查找,不用强制输入内容

4.1 新增按电话和地址查找函数

cpp 复制代码
// 按电话查找
int FinTel(contact* Con, char Tell[])
{
	for (int i = 0; i < Con->size; i++)
	{
		if (strcmp(Con->a[i].Tel, Tell) == 0)
		{
			printf("找到联系人,下标:%d\n", i);
			return i;
		}
	}
	printf("未找到联系人!\n");
	return -1;
}

// 按住址查找
int FinAdd(contact* Con, char Add[])
{
	for (int i = 0; i < Con->size; i++)
	{
		if (strcmp(Con->a[i].Address, Add) == 0)
		{
			printf("找到联系人,下标:%d\n", i);
			return i;
		}
	}
	printf("未找到联系人!\n");
	return -1;
}

这里代码明显冗余:三个查找函数除了比较的字段不同,其他完全一样。但当时作为新手,先实现功能再说,重构是后面的事。

cpp 复制代码
int Menu2(contact* Con)
{
	int choose = 0;
	int index = -1;
	char search[100] = {0}; // 初始化,消除字符串无终止符警告

	printf("默认按姓名查找,输入1切换更多方式:");
	if (scanf("%d", &choose) != 1)
	{
		ClearBuff();
		printf("输入错误!\n");
		return -1;
	}

	// 输入1弹出二级菜单
	if (choose == 1)
	{
		printf("1.姓名  2.电话  3.住址  0.退出\n");
		printf("请选择:");
		if (scanf("%d", &choose) != 1)
		{
			ClearBuff();
			printf("输入错误!\n");
			return -1;
		}

		switch (choose)
		{
		case 1:
			printf("请输入姓名:");
			scanf("%s", search);
			index = FinNa(Con, search);
			break;
		case 2:
			printf("请输入电话:");
			scanf("%s", search);
			index = FinTel(Con, search);
			break;
		case 3:
			printf("请输入住址:");
			scanf("%s", search);
			index = FinAdd(Con, search);
			break;
		case 0:
			printf("退出查找!\n");
			return -2; // 用-2标记用户主动退出
		default:
			printf("选项错误!\n");
			return -1;
		}
	}
	// 不输入1,默认按姓名查找
	else
	{
		printf("请输入姓名:");
		scanf("%s", search);
		index = FinNa(Con, search);
	}

	return index;
}

4.3 关键逻辑修复:区分用户退出和未找到

一开始我用-1同时表示 "未找到" 和 "用户退出",导致用户退出查找时,程序会提示 "联系人不存在",逻辑错误。解决 :用-2标记用户主动退出,-1标记未找到,调用处分别判断:

cpp 复制代码
// 删除函数
void DelCon(contact* Con)
{
	int index = Menu2(Con);
	if (index == -2)
	{
		printf("已取消删除!\n");
		return;
	}
	if (index < 0)
	{
		printf("联系人不存在!\n");
		return;
	}
	SLDeleteSpecify(Con, index);
	printf("删除成功!\n");
}

// 查找和修改函数同理,都加上这两个判断

五、细节优化:性别输入智能转换

原来要手动输入 "男""女",容易输错成 "男男""女女"。我想实现:

  • 输入0→自动变成 "女"
  • 输入1→自动变成 "男"
  • 输入其他文字→原样保存(支持 "保密""中性" 等自定义)

5.1 添加联系人优化

cpp 复制代码
void AddCon(contact* Con)
{
	UseDat data;
	printf("请输入姓名:");
	scanf("%s", data.name);

	char buf[20] = {0}; // 初始化,消除警告
	printf("请输入性别(0=女 1=男 也可自定义):");
	scanf("%s", buf);
	if (strcmp(buf, "0") == 0)
		strcpy(data.Gender, "女");
	else if (strcmp(buf, "1") == 0)
		strcpy(data.Gender, "男");
	else
		strcpy(data.Gender, buf);

	// 后面不变
}

5.2 修改联系人优化

cpp 复制代码
// ModDat函数中性别部分
char buf[20] = {0};
printf("新性别(0=女 1=男 也可自定义):");
InputSkip(buf, 20, Con->a[index].Gender);
if (strcmp(buf, "0") == 0)
	strcpy(Con->a[index].Gender, "女");
else if (strcmp(buf, "1") == 0)
	strcpy(Con->a[index].Gender, "男");
else if(strlen(buf) > 0) // 空输入保留原值
	strcpy(Con->a[index].Gender, buf);

效果:输入 0 或 1 就能快速选择性别,也支持自定义,非常方便。

六、终极踩坑:查找功能的连环崩溃

本以为到这里就大功告成了,结果查找功能又出了两个致命 bug,整整调试了一天才解决。

6.1 第一个坑:输入名字直接报错

现象 :提示 "默认按姓名查找,输入 1 切换方式:",我直接输入 "王一",结果提示 "输入错误!联系人不存在!"原因 :我写的 Menu2 逻辑完全反了!代码里先调用scanf("%d", &choose),要求必须输入数字,一输中文就会读取失败,直接返回 - 1。排查过程 :我在代码里加了很多 printf 调试,才发现原来我写的 "默认按姓名查找" 根本是骗人的,必须先输数字,不能直接输名字。解决方案:重写 Menu2 逻辑,先读取字符串,判断是不是 "1" 再切换方式:

cpp 复制代码
// 重写后的Menu2(第一版修复)
int Menu2(contact* Con)
{
	int choose = 0;
	char search[100] = { 0 };

	printf("默认按姓名查找,直接输入名字;输入1切换方式:");
	if (scanf("%d", &choose) == 1)
	{
		ClearBuff();

		if (choose == 1)
		{
			printf("1.姓名 2.电话 3.住址 0.退出\n");
			printf("请选择:");
			if (scanf("%d", &choose) != 1)
			{
				ClearBuff();
				printf("输入错误!\n");
				return -1;
			}
			ClearBuff();

			switch (choose)
			{
			case 1:
				printf("请输入姓名:");
				fgets(search, sizeof(search), stdin);
				search[strcspn(search, "\n")] = 0;
				index = FinNa(Con, search);
				break;
			// 其他case同理
			}
			return index;
		}
	}

	// 输入了名字,直接查找
	ClearBuff();
	printf("请输入姓名:");
	fgets(search, sizeof(search), stdin);
	search[strcspn(search, "\n")] = 0;
	return FinNa(Con, search);
}

6.2 第二个坑:明明存在却提示 Not found

现象 :添加了联系人 "王一",查找时输入 "王一",结果提示 "Not found"原因fgets会把回车一起读进去,我查的是 "王一 \n",但存的是 "王一",strcmp直接判定不相等!排查过程 :我用 printf 打印了输入的字符串和存储的字符串,发现输入的字符串后面多了一个看不见的换行符。解决方案:在所有查找函数开头加一行代码,去掉字符串最后的换行符:

cpp 复制代码
// 姓名查找
int FinNa(contact* Con, char name[])
{
    // 必须加这一行!去掉换行符
    name[strcspn(name, "\n")] = 0;

	for (int i = 0; i < Con->size; i++)
	{
		if (strcmp(Con->a[i].name, name) == 0)
		{
			printf("Found it,subscript %d\n", i);
			return i;
		}
	}
	printf("Not found\n");
	return -1;
}

// 电话查找和住址查找同理,都加这一行
int FinTel(contact* Con, char Tell[])
{
    Tell[strcspn(Tell, "\n")] = 0;
	// 后面不变
}

int FinAdd(contact* Con, char Add[])
{
    Add[strcspn(Add, "\n")] = 0;
	// 后面不变
}

结合上面两个修复,我写出了最终版的 Menu2,彻底解决了所有查找问题:

cpp 复制代码
int Menu2(contact* Con)
{
	int choose = 0;
	char search[100] = { 0 };

	printf("默认按姓名查找,直接输入名字;输入1切换方式:");
	ClearBuff();
	fgets(search, sizeof(search), stdin);
	search[strcspn(search, "\n")] = '\0';

	// 如果输入的是1,进入切换菜单
	if (strcmp(search, "1") == 0)
	{
		printf("1.姓名 2.电话 3.住址 0.退出\n");
		printf("请选择:");
		scanf("%d", &choose);
		ClearBuff();

		switch (choose)
		{
		case 1:
			printf("请输入姓名:");
			fgets(search, sizeof(search), stdin);
			search[strcspn(search, "\n")] = '\0';
			return FinNa(Con, search);
		case 2:
			printf("请输入电话:");
			fgets(search, sizeof(search), stdin);
			search[strcspn(search, "\n")] = '\0';
			return FinTel(Con, search);
		case 3:
			printf("请输入住址:");
			fgets(search, sizeof(search), stdin);
			search[strcspn(search, "\n")] = '\0';
			return FinAdd(Con, search);
		case 0:
			printf("退出查找!\n");
			return -2;
		default:
			printf("选项错误!\n");
			return -1;
		}
	}

	// 否则直接按姓名查找
	return FinNa(Con, search);
}

七、修复性别自定义修改无效

现象 :修改联系人时,性别输入 "保密",结果展示时还是原来的性别。原因 :我在修改函数里,自定义性别没有真正赋值到结构体里!原来的代码里,InputSkip读取了输入到buf,但只有输入 0 或 1 的时候才会赋值,输入其他内容的时候什么都没做。解决方案:完善性别修改的逻辑,空输入保留原值,自定义内容正确赋值:

cpp 复制代码
// 修复后的性别修改逻辑
char buf[20] = { 0 };
printf("请输入新性别(0=女 1=男 直接回车保留旧值):");
InputSkip(buf, sizeof(buf), Con->a[index].Gender);

// 空输入(直接回车)→ 不修改,保留原来的值
if (strlen(buf) == 0) {
    // 什么都不做
}
// 输入 0 → 女
else if (strcmp(buf, "0") == 0) {
    strcpy(Con->a[index].Gender, "女");
}
// 输入 1 → 男
else if (strcmp(buf, "1") == 0) {
    strcpy(Con->a[index].Gender, "男");
}
// 输入其他 → 直接保存输入内容
else {
    strcpy(Con->a[index].Gender, buf);
}

八、最后一步:消除所有编译警告

8.1 警告 1:"可能没有为字符串添加零终止符"

原因 :局部 char 数组未初始化,里面是随机垃圾值,编译器担心没有\0解决 :所有 char 数组定义时都加= {0}初始化:

cpp 复制代码
// 错误
char buf[20];
char search[100];
// 正确
char buf[20] = {0};
char search[100] = {0};

8.2 警告 2:"函数必须返回值"

原因 :一开始写 Menu2 时,有些分支没有 return。解决:确保所有路径都有返回值,上面的 Menu2 已经修复。

8.3 警告 3:"SLTYPE_PRINT_FMT 未使用 / 格式错误"

原因 :SeqList.h 里定义了#define SLTYPE_PRINT_FMT "%d",但 SLType 是结构体,无法用 % d 打印。解决:把这个宏改成空字符串:

cpp 复制代码
// SeqList.h
#define SLTYPE_PRINT_FMT ""

九、最终效果和学习总结

经过整整一周的调试和优化,我的通讯录终于变成了一个真正好用的工具:1. 输入汉字不会死循环;2.修改时回车跳过不修改;3.支持按姓名 / 电话 / 地址查找;4.性别输入智能转换(0 女 1 男 + 自定义);5. 查找功能 100% 正常,不会出现找不到的情况;6. 性别自定义修改正常;7.数据持久化正常;8. 增删改查全功能正常。

9.1 整个过程的收获

  1. 对 C 语言字符串处理的理解加深了:终于搞懂了换行符、字符串终止符的重要性,以及 scanf 和 fgets 的区别
  2. 对输入输出缓冲区的理解彻底通透了:之前一直觉得缓冲区很神秘,现在终于知道为什么会出现输入吞掉、无限循环的问题
  3. 学会了调试技巧:从只会重启程序,到会用 printf 加断点调试,一步步定位问题
  4. 理解了用户体验的重要性:一个功能能不能用是一回事,好不好用是另一回事,很多细节优化能极大提升体验
  5. 接受了代码的不完美:新手先实现功能,再考虑优化和重构,不要一开始就追求完美

9.2 后续可以继续优化的方向

  1. 把年龄改成 int 类型,支持按年龄排序
  2. 增加手机号格式校验(11 位数字)
  3. 增加重复联系人校验
  4. 支持批量删除和导入导出
  5. 重构三个查找函数,消除代码冗余

十、最终完整可运行代码(分文件)

1. SeqList.h(顺序表头文件)

cpp 复制代码
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
#include"Contact.h"

typedef struct UserData SLType;
#define SLTYPE_PRINT_FMT ""
typedef struct SeqList
{
	SLType* a;
	int size;//有效数据个数
	int capacity;//储存空间
}SL;

//初始化和销毁
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);//尾部删除
//在指定位置之前插入/删除数据
void PushSpecify(SL* ps,int pos, SLType x);//插入
void SLDeleteSpecify(SL* ps,int pos);//删除
//修改指定位置内容
void Modifyspecified(SL* ps, int pos, SLType x);

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("Failed to apply for space");
			exit(1);
		}
		else
		{
			ps->a = Newps;
			ps->capacity = Newspace;
		}
	}
}
void PushBack(SL* ps, SLType x)//尾部插入
{
	assert(ps);
	SLExp(ps);//判断是否扩容
	ps->a[ps->size++] = x;
}
void PushFront(SL* ps, SLType x)//头部插入
{
	assert(ps);
	SLExp(ps);//判断是否扩容
	for (int i = ps->size; i > 0; i--)
	{
		ps->a[i] = ps->a[i - 1];
	}
	ps->a[0] = x;
	ps->size++;
}
void SLPopFront(SL* ps)//头部删除
{
	assert(ps);
	assert(ps->size);//数据不能为空
	for (int i = 0; i < ps->size-1; i++)
	{
		ps->a[i] = ps->a[i + 1];
	}
	ps->size--;
}
void SLPopBack(SL* ps)//尾部删除
{
	assert(ps);
	assert(ps->size);//数据不能为空
	ps->size--;
}
//在指定位置之前插入/删除数据
void PushSpecify(SL* ps,int pos, SLType x)//插入
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);
	SLExp(ps);//判断是否扩容
	for (int i = ps->size; i > pos; i--)
	{
		ps->a[i] = ps->a[i - 1];
	}
	ps->a[pos] = x;
	ps->size++;
}
void SLDeleteSpecify(SL* ps,int pos)//删除
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);
	assert(ps->size);//数据不能为空
	for (int i = pos; i < ps->size - 1; i++)
	{
		ps->a[i] = ps->a[i + 1];
	}
	ps->size--;
}
void Modifyspecified(SL* ps, int pos, SLType x)//修改指定位置内容
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);
	ps->a[pos] = x;
}
void Print(SL* ps)//打印
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		printf(SLTYPE_PRINT_FMT " ", ps->a[i]);
	}
	printf("\n");
}

3. Contact.h(通讯录头文件)

cpp 复制代码
#pragma once
#define NAME_Max 100//姓名
#define Gender_Max 4//性别
#define AGE_Max 4//年龄
#define TEL_Max 15//电话
#define ADDRESS_Max 100//住址
#define _CRT_SECURE_NO_WARNINGS
typedef struct UserData
{
	char name[NAME_Max];//姓名
	char Gender[Gender_Max];//性别
	char age[AGE_Max];//年龄
	char Tel[TEL_Max];//电话
	char Address[ADDRESS_Max];//住址
}UseDat;
void ClearBuff(void); // 清空直到换行
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);

4. Contacts.c(通讯录核心实现)

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include"SeqList.h"
#include"Contact.h"
#include <string.h>
#define MAX_INPUT 200
void ClearBuff()
{
	int c;
	while ((c = getchar()) != '\n' && c != EOF);
}
void InputSkip(char* buf, int len, char* oldVal)
{
	int c;
	while ((c = getchar()) != '\n' && c != EOF);
	char temp[MAX_INPUT];
	int read_len = (len < MAX_INPUT) ? len : MAX_INPUT;
	fgets(temp, read_len, stdin);
	temp[strcspn(temp, "\n")] = '\0';
	if (strlen(temp) == 0)
	{
		strncpy(buf, oldVal, len - 1); // 只拷贝len-1个字符
		buf[len - 1] = '\0'; // 强制终止符,避免越界
	}
	else
	{
		strncpy(buf, temp, len - 1);
		buf[len - 1] = '\0';
	}
}
//查找函数
int FinNa(contact* Con, char name[])//查找姓名
{
	name[strcspn(name, "\n")] = '\0';
	for (int i = 0; i < Con->size; i++)
	{
		if (strcmp(Con->a[i].name, name) == 0)
		{
			printf("Found it,subscript %d\n", i);
			return i;
		}
	}
	printf("Not found\n");
	return -1; // 没找到返回-1
}
int FinTel(contact* Con, char Tell[])//查找电话
{
	Tell[strcspn(Tell, "\n")] = '\0';
	for (int i = 0; i < Con->size; i++)
	{
		if (strcmp(Con->a[i].Tel, Tell) == 0)
		{
			printf("Found it,subscript %d\n", i);
			return i;
		}
	}
	printf("Not found\n");
	return -1; // 没找到返回-1
}
int FinAdd(contact* Con, char Add[])//查找姓名
{
	Add[strcspn(Add, "\n")] = '\0';
	for (int i = 0; i < Con->size; i++)
	{
		if (strcmp(Con->a[i].Address, Add) == 0)
		{
			printf("Found it,subscript %d\n", i);
			return i;
		}
	}
	printf("Not found\n");
	return -1; // 没找到返回-1
}
// 查找菜单:默认按姓名,输入1弹出二级选择
int Menu2(contact* Con)
{
	int choose = 0;
	char search[100] = { 0 };
	printf("默认按姓名查找,输入1切换方式:");
	// 直接读取姓名(不再判断数字)
	ClearBuff();
	fgets(search, sizeof(search), stdin);
	search[strcspn(search, "\n")] = '\0';
	// 如果输入的是 1,进入切换菜单
	if (strcmp(search, "1") == 0)
	{
		printf("1.姓名 2.电话 3.住址 0.退出\n");
		printf("请选择:");
		scanf("%d", &choose);
		ClearBuff();
		switch (choose)
		{
		case 1: printf("请输入姓名:"); fgets(search, sizeof(search), stdin); search[strcspn(search, "\n")] = '\0';
			return FinNa(Con, search);
		case 2: printf("请输入电话:"); fgets(search, sizeof(search), stdin); search[strcspn(search, "\n")] = '\0';
			return FinTel(Con, search);
		case 3: printf("请输入住址:"); fgets(search, sizeof(search), stdin); search[strcspn(search, "\n")] = '\0';
			return FinAdd(Con, search);
		case 0: return -2;
		default: return -1;
		}
	}
	// 否则直接按姓名查找
	return FinNa(Con, search);
}
void LoadCon(contact* Con)
{
	// 尝试打开文件,不存在就直接返回
	FILE* pf = fopen("contact.txt", "rb");
	if (pf == NULL)
	{
		printf("没有找到历史数据文件,创建新的通讯录!\n");
		return;
	}
	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);
}
//添加联系人
void AddCon(contact* Con)
{
	UseDat data;
	//写入文件数据
	printf("请输入姓名:");
	scanf("%s", data.name);/*scanf("%s", data.Gender); */
	                       //printf("请输入性别:");
	char buf[20] = { 0 };
	printf("请输入性别(0=女 1=男 也可自定义输入):");// 性别:0女 1男,其他自定义
	scanf("%s", buf);
	if (strcmp(buf, "0") == 0)
	{
		strcpy(data.Gender, "女");
	}
	else if (strcmp(buf, "1") == 0)
	{
		strcpy(data.Gender, "男");
	}
	else
	{
		strcpy(data.Gender, buf);// 输入不是0/1,保存自己输入的内容
	}
	printf("请输入年龄:");
	scanf("%s", data.age);
	printf("请输入电话:");
	scanf("%s", data.Tel);
	printf("请输入住址:");
	scanf("%s", data.Address);
	PushBack(Con, data);
	printf("添加成功!\n");
}
//删除联系人
void DelCon(contact* Con)
{
	int index = Menu2(Con); // 调用查找菜单,获取下标
	if (index == -2)
	{
		printf("已取消删除!\n");
		return;
	}
	if (index < 0)
	{
		printf("联系人不存在,删除失败!\n");
		return;
	}
	// 3. 找到执行删除
	SLDeleteSpecify(Con, index);
	printf("删除成功!\n");
}
//展示通讯录
void ShoCon(contact* Con)
{
	printf("%-10s %-10s %-10s %-15s %-20s\n", "姓名", "性别", "年龄", "电话", "住址");
	for (int i = 0; i < Con->size; i++)
	{ 
		printf("%-10s %-10s %-10s %-15s %-20s\n", 
			Con->a[i].name,
			Con->a[i].Gender,
			Con->a[i].age,
			Con->a[i].Tel,
			Con->a[i].Address);
	}
	printf("展示完成!\n");
}
//查找联系人
void FinCon(contact* Con)
{
	int index = Menu2(Con);
	if (index == -2)
	{
		printf("已取消查找!\n");
		return;
	}
	if (index < 0)
	{
		printf("联系人不存在!\n");
		return;
	}
	// 打印找到的联系人
	printf("%-10s %-10s %-10s %-15s %-20s\n",
		Con->a[index].name,
		Con->a[index].Gender,
		Con->a[index].age,
		Con->a[index].Tel,
		Con->a[index].Address);
	printf("打印完成!\n");
}
//修改联系人数据
void ModDat(contact* Con)
{
	printf("如果不需要修改某一项,直接回车即可。\n");
	int index = Menu2(Con);
	if (index == -2)
	{
		printf("已取消修改!\n");
		return;
	}
	if (index < 0)
	{
		printf("联系人不存在!\n");
		return;
	}
	printf("请输入新的姓名:");
	InputSkip(Con->a[index].name, NAME_Max, Con->a[index].name);/*scanf("%s", Con->a[index].name);*/
	/*printf("请输入新的性别:");
	scanf("%s", Con->a[index].Gender);*/
	// 性别:0女 1男,其他自定义
	char buf[20] = { 0 };
	printf("请输入新性别(0=女 1=男 也可自定义输入):");
	InputSkip(buf, 20, Con->a[index].Gender);/*scanf("%s", buf);*/
	if (strcmp(buf, "0") == 0) {
		strcpy(Con->a[index].Gender, "女");
	}
	else if (strcmp(buf, "1") == 0) {
		strcpy(Con->a[index].Gender, "男");
	}
	else {
		strcpy(Con->a[index].Gender, buf);
	}
	printf("请输入年龄:");
	InputSkip(Con->a[index].age, AGE_Max, Con->a[index].age);/*scanf("%s", data.age); */
	printf("请输入电话:");
	InputSkip(Con->a[index].Tel, TEL_Max, Con->a[index].Tel);/*scanf("%s", data.Tel);*/
	printf("请输入新的住址:");
	InputSkip(Con->a[index].Address, ADDRESS_Max, Con->a[index].Address);/*scanf("%s", Con->a[index].Address);*/
	printf("修改成功!\n");
}
void SaveCon(contact* Con)
{
	FILE* pf = fopen("contact.txt", "wb");
	if (pf == NULL)
	{
		perror("Failed to open file");
		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);
}

5. test.c(主函数 / 菜单)

cpp 复制代码
#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.删除联系人 * * * *\n");
		printf("* * * * 3.展示通讯录 * * * * 4.查找联系人 * * * *\n");
		printf("* * * * 5.修改联系人数据 * * 6.退出系统 * * * * *\n");
		printf("请输入操作选项:");
		if (scanf("%d", &input) != 1)
		{
			ClearBuff(); // 清空脏数据
			printf("输入错误!请输入数字\n");
			continue; // 重新循环
		}
		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;
}

十一、升级版通讯录运行演示 & 功能讲解

对比第一篇基础版,本篇做了大量实用升级,结合运行截图,给你逐点演示讲解。

核心新增升级点

  1. 查找支持 姓名 / 电话 / 住址 3 种方式,不再只能按姓名操作
  2. 性别快捷输入:0=女、1=男,也可以自定义文字
  3. 修改信息时,直接回车可跳过不修改项,不用重复输入
  4. 增加输入容错,非法字符不会导致程序崩溃
  5. 增删改查复用同一套查找逻辑,代码复用性更强
  6. 保留持久化存储,启动自动加载、退出自动保

分步运行演示

1. 程序启动

运行后自动读取本地contact.txt历史文件,加载完毕弹出主菜单:

新增:性别快捷输入功能 输入1进入添加,性别支持两种模式:

  • 输入1 → 自动保存为
  • 输入0 → 自动保存为
  • 直接输入文字(如 "女"),支持自定义

示例:添加联系人【张三】

3. 删除联系人(序号 2)

核心升级:支持按【电话、住址】删除,不止姓名 默认按姓名查找,输入1切换查找方式,选择2(按电话),输入对应号码即可删除。示例:按电话删除张三:

4. 多条件查找联系人(序号 4)

核心升级:3 种查找方式自由切换 默认姓名查找,输入1切换,支持姓名、电话、住址 精准匹配。示例:添加【李四】,性别直接自定义输入,按电话查找

5. 修改联系人(序号 5)

新增:回车跳过功能不想修改的信息,直接回车,程序自动保留原有数据;性别同样支持快捷输入。

6. 异常输入容错

主菜单输入非数字时,程序自动清空缓冲区,提示输入错误,不会崩溃卡死

7. 退出自动保存(序号 6)

输入6退出,自动将所有联系人数据写入contact.txt,下次启动自动加载,数据永久保存。

整体总结

本篇在基础版上,重点优化了查找灵活性和使用体验,解决了只能按姓名操作的局限,增加了人性化快捷输入、容错处理,代码结构更规范,实用性大幅提升。

本篇代码Git链接Luminous/Luminousbegin

十二、写在最后

这个通讯录项目是我第一个真正意义上的 C 语言项目,从最开始的几百行基础代码,到后来一步步优化成现在的样子,我踩了几乎所有新手会踩的坑。

现在回头看,当时的代码有很多问题:冗余严重、命名不规范、逻辑不够严谨... 但正是这些不完美,让我对 C 语言的理解有了质的提升。

对于初学者来说,不要怕写 bug,每一个 bug 都是最好的老师。当你把所有 bug 都修完,你会发现自己已经不知不觉成长了很多。

相关推荐
魔法阵维护师1 小时前
从零开发游戏需要学习的c#模块,第十四章(保存和加载)
学习·游戏·c#
web3.08889991 小时前
1688 图搜接口(item_search_img / 拍立淘) 接入方法
开发语言·python
AI算法沐枫2 小时前
深度学习python代码处理科研测序数据
数据结构·人工智能·python·深度学习·决策树·机器学习·线性回归
один but you2 小时前
从可变参数到 emplace:现代 C++ 性能优化的核心组合
java·开发语言
_李小白2 小时前
【android opencv学习笔记】Day 17: 目标追踪(MeanShift)
android·opencv·学习
一只机电自动化菜鸟2 小时前
一建机电备考笔记(40) 建筑机电施工—排水管道施工(含考频+题型)
经验分享·笔记·学习·职场和发展·课程设计
2301_818730562 小时前
numpy的学习(笔记)
学习·numpy
你干嘛?哎哟3 小时前
4月工作笔记
笔记
tom02183 小时前
软考中级《嵌入式系统设计师》全套备考资料(真题 + 教材 + 笔记)
笔记·嵌入式·软考·自学·电子技术·电子资料·变成