1. 项目概述
电话簿系统是每个C语言学习者都会遇到的经典实战项目。这个看似简单的系统实际上涵盖了C语言中多个核心知识点:结构体定义、文件操作、内存管理、用户交互设计等。我在大学时期完成的第一个像样的课程作业就是电话簿系统,后来在教学中又指导过上百名学生实现过各种版本。今天要分享的是一个经过多年教学实践打磨的"工业级"简易实现方案。
这个版本的特点在于:
- 采用纯C标准库实现,零第三方依赖
- 使用二进制文件存储数据,兼顾效率和可移植性
- 实现完整的CRUD(增删改查)功能
- 包含输入验证和错误处理机制
- 代码风格符合工业规范
2. 核心数据结构设计
2.1 联系人结构体
电话簿的核心是联系人信息的存储。我们定义一个包含基本字段的结构体:
c复制#define MAX_NAME_LEN 50
#define MAX_PHONE_LEN 20
#define MAX_ADDR_LEN 100
typedef struct {
int id; // 唯一标识符
char name[MAX_NAME_LEN];
char phone[MAX_PHONE_LEN];
char address[MAX_ADDR_LEN];
} Contact;
注意:字符串字段使用固定长度数组而非指针,这样可以简化内存管理,避免动态分配带来的复杂性。实际项目中可能需要更灵活的设计,但作为教学示例,这种设计更易于理解。
2.2 电话簿管理结构
为管理联系人集合,我们使用动态数组:
c复制typedef struct {
Contact *contacts; // 动态数组
int count; // 当前联系人数量
int capacity; // 数组容量
} Phonebook;
这种设计相比链表更简单,且在现代计算机上缓存友好。初始容量可以设为16,当空间不足时以1.5倍大小扩容。
3. 文件存储方案
3.1 二进制存储格式
为简化实现,我们采用二进制格式存储数据。文件结构如下:
- 前4字节:魔数"PB01"(用于文件识别)
- 接着4字节:联系人数量(int)
- 后续内容:连续的Contact结构体数据
写入函数示例:
c复制void save_phonebook(Phonebook *pb, const char *filename) {
FILE *fp = fopen(filename, "wb");
if (!fp) {
perror("无法打开文件");
return;
}
// 写入文件头
fwrite("PB01", 4, 1, fp);
fwrite(&pb->count, sizeof(int), 1, fp);
// 写入联系人数据
fwrite(pb->contacts, sizeof(Contact), pb->count, fp);
fclose(fp);
}
3.2 数据加载
加载时需要验证文件格式:
c复制int load_phonebook(Phonebook *pb, const char *filename) {
FILE *fp = fopen(filename, "rb");
if (!fp) return 0;
char magic[4];
if (fread(magic, 4, 1, fp) != 1 || memcmp(magic, "PB01", 4) != 0) {
fclose(fp);
return 0;
}
int count;
if (fread(&count, sizeof(int), 1, fp) != 1) {
fclose(fp);
return 0;
}
// 分配内存
if (!resize_phonebook(pb, count)) {
fclose(fp);
return 0;
}
// 读取数据
if (fread(pb->contacts, sizeof(Contact), count, fp) != count) {
fclose(fp);
return 0;
}
pb->count = count;
fclose(fp);
return 1;
}
4. 核心功能实现
4.1 添加联系人
c复制void add_contact(Phonebook *pb) {
if (pb->count >= pb->capacity) {
if (!resize_phonebook(pb, pb->capacity * 1.5)) {
printf("错误:无法扩容电话簿\n");
return;
}
}
Contact *c = &pb->contacts[pb->count++];
c->id = generate_id(pb); // 生成唯一ID
printf("输入姓名: ");
safe_input(c->name, MAX_NAME_LEN);
printf("输入电话: ");
safe_input(c->phone, MAX_PHONE_LEN);
printf("输入地址: ");
safe_input(c->address, MAX_ADDR_LEN);
printf("联系人添加成功!\n");
}
其中safe_input()是一个安全的输入函数,防止缓冲区溢出:
c复制void safe_input(char *buf, int max_len) {
fgets(buf, max_len, stdin);
buf[strcspn(buf, "\n")] = '\0'; // 移除换行符
}
4.2 查找联系人
实现按姓名查找:
c复制void search_contact(const Phonebook *pb) {
char name[MAX_NAME_LEN];
printf("输入要查找的姓名: ");
safe_input(name, MAX_NAME_LEN);
int found = 0;
for (int i = 0; i < pb->count; i++) {
if (strstr(pb->contacts[i].name, name) != NULL) {
print_contact(&pb->contacts[i]);
found = 1;
}
}
if (!found) {
printf("未找到匹配的联系人\n");
}
}
4.3 删除联系人
c复制void delete_contact(Phonebook *pb) {
int id;
printf("输入要删除的联系人ID: ");
scanf("%d", &id);
while (getchar() != '\n'); // 清空输入缓冲区
int index = -1;
for (int i = 0; i < pb->count; i++) {
if (pb->contacts[i].id == id) {
index = i;
break;
}
}
if (index == -1) {
printf("未找到ID为%d的联系人\n", id);
return;
}
// 将最后一个元素移到删除位置
if (index != pb->count - 1) {
pb->contacts[index] = pb->contacts[pb->count - 1];
}
pb->count--;
printf("联系人删除成功\n");
}
5. 用户界面设计
5.1 主菜单
c复制void show_menu() {
printf("\n=== 电话簿管理系统 ===\n");
printf("1. 添加联系人\n");
printf("2. 显示所有联系人\n");
printf("3. 查找联系人\n");
printf("4. 修改联系人\n");
printf("5. 删除联系人\n");
printf("6. 保存数据\n");
printf("7. 加载数据\n");
printf("0. 退出\n");
printf("请选择操作: ");
}
5.2 主循环
c复制int main() {
Phonebook pb = {0};
init_phonebook(&pb, 16);
int running = 1;
while (running) {
show_menu();
int choice;
scanf("%d", &choice);
while (getchar() != '\n'); // 清空输入缓冲区
switch (choice) {
case 1: add_contact(&pb); break;
case 2: list_contacts(&pb); break;
case 3: search_contact(&pb); break;
case 4: modify_contact(&pb); break;
case 5: delete_contact(&pb); break;
case 6: save_phonebook(&pb, "phonebook.dat"); break;
case 7: load_phonebook(&pb, "phonebook.dat"); break;
case 0: running = 0; break;
default: printf("无效选择\n");
}
}
free_phonebook(&pb);
return 0;
}
6. 高级功能实现
6.1 联系人修改
c复制void modify_contact(Phonebook *pb) {
int id;
printf("输入要修改的联系人ID: ");
scanf("%d", &id);
while (getchar() != '\n');
Contact *c = NULL;
for (int i = 0; i < pb->count; i++) {
if (pb->contacts[i].id == id) {
c = &pb->contacts[i];
break;
}
}
if (!c) {
printf("未找到ID为%d的联系人\n", id);
return;
}
printf("当前信息:\n");
print_contact(c);
printf("输入新姓名(留空保持不变): ");
char input[MAX_NAME_LEN];
safe_input(input, MAX_NAME_LEN);
if (input[0] != '\0') {
strcpy(c->name, input);
}
printf("输入新电话(留空保持不变): ");
safe_input(input, MAX_PHONE_LEN);
if (input[0] != '\0') {
strcpy(c->phone, input);
}
printf("输入新地址(留空保持不变): ");
safe_input(input, MAX_ADDR_LEN);
if (input[0] != '\0') {
strcpy(c->address, input);
}
printf("联系人信息已更新\n");
}
6.2 数据排序
可以添加按姓名排序功能:
c复制int compare_contacts(const void *a, const void *b) {
return strcmp(((const Contact*)a)->name, ((const Contact*)b)->name);
}
void sort_contacts(Phonebook *pb) {
qsort(pb->contacts, pb->count, sizeof(Contact), compare_contacts);
}
7. 错误处理与输入验证
7.1 内存分配检查
c复制int resize_phonebook(Phonebook *pb, int new_capacity) {
Contact *new_contacts = realloc(pb->contacts, new_capacity * sizeof(Contact));
if (!new_contacts && new_capacity > 0) {
return 0;
}
pb->contacts = new_contacts;
pb->capacity = new_capacity;
if (pb->count > new_capacity) {
pb->count = new_capacity;
}
return 1;
}
7.2 输入验证
电话号码验证示例:
c复制int is_valid_phone(const char *phone) {
for (int i = 0; phone[i]; i++) {
if (!isdigit(phone[i]) && phone[i] != '-' && phone[i] != ' ') {
return 0;
}
}
return 1;
}
8. 项目构建与测试
8.1 Makefile示例
makefile复制CC = gcc
CFLAGS = -Wall -Wextra -std=c11
SRC = main.c phonebook.c
OBJ = $(SRC:.c=.o)
TARGET = phonebook
all: $(TARGET)
$(TARGET): $(OBJ)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $<
clean:
rm -f $(OBJ) $(TARGET)
8.2 测试用例
建议测试以下场景:
- 添加10个联系人并保存
- 重新加载并验证数据完整性
- 测试查找功能(精确匹配和部分匹配)
- 测试修改和删除操作
- 测试边界情况(空电话簿、最大容量等)
9. 项目扩展思路
这个基础版本可以进一步扩展:
- 多条件搜索:支持按姓名、电话、地址组合查询
- 分组功能:为联系人添加分组标签(家人、同事等)
- 导入导出:支持CSV等通用格式
- 图形界面:使用GTK或Qt添加GUI
- 网络同步:实现多设备间的数据同步
10. 常见问题与解决方案
10.1 数据文件损坏
问题:加载时发现文件损坏或格式不正确
解决:在load_phonebook()中添加更多验证,如检查联系人数量是否合理,文件大小是否符合预期
10.2 内存泄漏
问题:程序退出时未释放动态分配的内存
解决:确保实现并调用free_phonebook()函数:
c复制void free_phonebook(Phonebook *pb) {
free(pb->contacts);
pb->contacts = NULL;
pb->count = pb->capacity = 0;
}
10.3 输入缓冲区问题
问题:混合使用scanf()和fgets()导致输入错乱
解决:每次使用scanf()后清空输入缓冲区:
c复制while (getchar() != '\n');
在实际教学中发现,这个电话簿项目虽然基础,但能很好地锻炼学生的综合编程能力。特别是文件I/O和内存管理部分,很多学生第一次实现时都会遇到各种问题。建议在开发时逐步添加功能,每完成一个模块就进行测试,而不是一次性写完所有代码再调试。
