学完顺序表后,用 C 语言写了一个通讯录
目录
一、什么是顺序表?它和普通数组有啥区别?
二、为什么用顺序表做通讯录?
三、项目需求
四、设计思路详解(手把手拆解)
Step 1:定义联系人结构体(contact.h)
Step 2:定义动态顺序表(SeqList.h)
Step 3:顺序表核心函数实现(SeqList.c)
Step 4:通讯录业务逻辑(contact.c)
Step 5:主函数(test.c)
五、初学者高频错误 & 避坑指南
六、顺序表的优缺点总结
七、下一步可以怎么优化?
八、动手挑战!
上一篇博客我们讲解了,数据结构中顺序表的知识点,所以这篇博客用顺序表来实现一个通讯录管理系统
📌 本文适合:
- 会基础 C 语言(知道结构体、数组、函数)
- 刚接触“顺序表”概念
- 想通过小项目理解数据结构的实际用途
那我们就,开始吧
首先呢,先回顾一下知识点
一、什么是顺序表?它和普通数组有啥区别?
很多同学一听到“顺序表”,就觉得很高大上。其实——
顺序表 = 数组 + 当前长度 + 容量 + 一套操作函数
比如普通数组:
int arr[100];
你只能自己记“现在用了多少个”,自己写循环插入、删除,很容易出错。
而顺序表把它封装成一个结构体:
typedef struct Seqlist
{Contact* data; //指向动态数组的指针int size ; //容量int capacity; //数组长度}
这样,增删改查都通过函数操作,安全、清晰、可扩展!
这里关于顺序表跟多的内容我们就不多说了,详细知识内容可看《数据结构杂谈》的上一篇博客
二、为什么用顺序表做通讯录?
通讯录的特点:
- 联系人数量不会爆炸(几十到几百个);
- 经常要“找张三的电话”(查找频繁);
- 偶尔增删改。
顺序表的优势正好匹配:
- 查找快:虽然要遍历,但数据连续,CPU 缓存友好;
- 内存紧凑:没有链表那种每个节点都要存指针的开销;
- 实现简单:比链表容易理解,适合入门。
💡 注意:如果未来要做“百万级联系人”,那就要考虑哈希表或数据库了。但对学习阶段,顺序表刚刚好!
三、项目需求
根据 要求,我们的通讯录要支持:
- 存储至少 100 人;
- 每个联系人包含:姓名、性别、年龄、电话、地址;
- 功能:添加、删除(按姓名)、查找(按姓名)、修改、显示全部;
- 程序关闭后,数据不丢失(保存到文件);
- 使用动态顺序表(不够就自动扩容)。
四、设计思路详解(手把手拆解)
Step 1:定义联系人结构体(contact.h)
// contact.h
#pragma once
#include <stdio.h>
#include <string.h>// 定义字段最大长度(避免缓冲区溢出)
#define NAME_MAX 20
#define SEX_MAX 4
#define TEL_MAX 12
#define ADDR_MAX 50// 联系人信息
typedef struct PersonInfo {char name[NAME_MAX];char sex[SEX_MAX]; // "男"/"女"int age;char tel[TEL_MAX];char addr[ADDR_MAX];
} PersonInfo;
✅ 为什么用 char name[20]
而不是 char* name
?
因为初学者用动态字符串容易内存泄漏。固定长度数组更安全、简单。
Step 2:定义动态顺序表(SeqList.h)
//SeqList.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert>
#include "contact"//把 PersonInfo 作为顺序表的数据类型
typedef PersonInfo SQDataType;typedef struct SeqList
{SQDataType; //指向动态分配的数组int size; //当前有效数据的个数int capacity; //当前数组的容量
} Seqlist;//函数声明
void SeqListInit(SeqList* psl);
void SeqListDestroy(SeqList* psl);
void SeqListPushBack(SeqList* psl, SQDataType x);
void SeqListErase(SeqList* psl, int pos);
int SeqListFind(SeqList* psl, const char* name); // 按姓名找
void SeqListPrint(SeqList* psl);
void CheckCapacity(SeqList* psl); // 检查是否需要扩容
🔍 重点:a
是指针!我们要用 malloc
动态申请内存。
Step 3:顺序表核心函数实现(SeqList.c)
// SeqList.c
#include "SeqList.h"void SeqListInit(SeqList* psl) {assert(psl);psl->a = (SQDataType*)malloc(sizeof(SQDataType) * 4); // 初始容量4psl->size = 0;psl->capacity = 4;
}void CheckCapacity(SeqList* psl) {if (psl->size == psl->capacity) {int newCapacity = psl->capacity * 2;SQDataType* tmp = (SQDataType*)realloc(psl->a, sizeof(SQDataType) * newCapacity);if (tmp == NULL) {perror("realloc failed");exit(-1);}psl->a = tmp;psl->capacity = newCapacity;printf("通讯录扩容成功!当前容量:%d\n", newCapacity);}
}void SeqListPushBack(SeqList* psl, SQDataType x) {assert(psl);CheckCapacity(psl); // 先检查是否要扩容psl->a[psl->size] = x;psl->size++;
}void SeqListErase(SeqList* psl, int pos) {assert(psl);assert(pos >= 0 && pos < psl->size);// 从 pos 开始,后面所有元素前移一位for (int i = pos; i < psl->size - 1; i++) {psl->a[i] = psl->a[i + 1];}psl->size--; // ⚠️ 别忘了减 size!
}int SeqListFind(SeqList* psl, const char* name) {for (int i = 0; i < psl->size; i++) {if (strcmp(psl->a[i].name, name) == 0) {return i; // 找到返回下标}}return -1; // 未找到
}void SeqListPrint(SeqList* psl) {if (psl->size == 0) {printf("通讯录为空!\n");return;}printf("\n%-10s %-4s %-4s %-12s %-20s\n", "姓名", "性别", "年龄", "电话", "地址");printf("------------------------------------------------------------\n");for (int i = 0; i < psl->size; i++) {printf("%-10s %-4s %-4d %-12s %-20s\n",psl->a[i].name, psl->a[i].sex,psl->a[i].age, psl->a[i].tel, psl->a[i].addr);}
}void SeqListDestroy(SeqList* psl) {free(psl->a);psl->a = NULL;psl->size = psl->capacity = 0;
}
Step 4:通讯录业务逻辑(contact.c)
// contact.c
#include "SeqList.h"// 从文件加载历史数据
void LoadContact(SeqList* con) {FILE* pf = fopen("contact.dat", "rb");if (pf == NULL) {printf("首次运行,无历史数据。\n");return;}PersonInfo tmp;while (fread(&tmp, sizeof(PersonInfo), 1, pf)) {SeqListPushBack(con, tmp);}fclose(pf);printf("✅ 历史数据加载成功!\n");
}// 保存到文件
void SaveContact(SeqList* con) {FILE* pf = fopen("contact.dat", "wb");if (pf == NULL) {perror("保存失败");return;}for (int i = 0; i < con->size; i++) {fwrite(&(con->a[i]), sizeof(PersonInfo), 1, pf);}fclose(pf);printf("💾 通讯录已保存到文件!\n");
}// 初始化通讯录(含加载历史)
void InitContact(SeqList* con) {SeqListInit(con);LoadContact(con);
}// 添加联系人
void AddContact(SeqList* con) {PersonInfo info;printf("请输入姓名: "); scanf("%s", info.name);printf("请输入性别(男/女): "); scanf("%s", info.sex);printf("请输入年龄: "); scanf("%d", &info.age);printf("请输入电话: "); scanf("%s", info.tel);printf("请输入地址: "); scanf("%s", info.addr);SeqListPushBack(con, info);printf("✅ 添加成功!\n");
}// 删除联系人
void DelContact(SeqList* con) {char name[NAME_MAX];printf("请输入要删除的姓名: "); scanf("%s", name);int pos = SeqListFind(con, name);if (pos == -1) {printf("❌ 未找到该联系人!\n");return;}SeqListErase(con, pos);printf("✅ 删除成功!\n");
}// 查找联系人
void FindContact(SeqList* con) {char name[NAME_MAX];printf("请输入要查找的姓名: "); scanf("%s", name);int pos = SeqListFind(con, name);if (pos == -1) {printf("❌ 未找到!\n");return;}printf("✅ 找到联系人:\n");printf("%-10s %-4s %-4d %-12s %-20s\n",con->a[pos].name, con->a[pos].sex,con->a[pos].age, con->a[pos].tel, con->a[pos].addr);
}// 修改联系人
void ModifyContact(SeqList* con) {char name[NAME_MAX];printf("请输入要修改的姓名: "); scanf("%s", name);int pos = SeqListFind(con, name);if (pos == -1) {printf("❌ 未找到该联系人!\n");return;}printf("请输入新姓名: "); scanf("%s", con->a[pos].name);printf("请输入新性别: "); scanf("%s", con->a[pos].sex);printf("请输入新年龄: "); scanf("%d", &con->a[pos].age);printf("请输入新电话: "); scanf("%s", con->a[pos].tel);printf("请输入新地址: "); scanf("%s", con->a[pos].addr);printf("✅ 修改成功!\n");
}// 显示全部
void ShowContact(SeqList* con) {SeqListPrint(con);
}// 销毁并保存
void DestroyContact(SeqList* con) {SaveContact(con);SeqListDestroy(con);
}
💾 文件持久化说明:
- 使用二进制文件
contact.dat
; - 程序启动时自动加载;
- 退出时自动保存;
- 即使关机,数据也不会丢!
Step 5:主函数(test.c)
// test.c
#include "SeqList.h"void menu() {printf("\n========== 通讯录管理系统 ==========\n");printf("1. 添加联系人\n");printf("2. 删除联系人\n");printf("3. 查找联系人\n");printf("4. 修改联系人\n");printf("5. 显示所有联系人\n");printf("0. 退出\n");printf("====================================\n");printf("请选择操作: ");
}int main() {SeqList con;InitContact(&con);int choice;do {menu();scanf("%d", &choice);switch (choice) {case 1: AddContact(&con); break;case 2: DelContact(&con); break;case 3: FindContact(&con); break;case 4: ModifyContact(&con); break;case 5: ShowContact(&con); break;case 0: printf("再见!\n"); break;default: printf("❌ 无效选项,请重试!\n");}} while (choice != 0);DestroyContact(&con); // 退出前保存+释放内存return 0;
}
五、初学者高频错误 & 避坑指南
错误 | 正确做法 |
用== 比较字符串 | 必须用strcmp(str1, str2) == 0 |
删除后忘记size-- | 删除后一定要size-- ,否则显示异常 |
插入时不检查容量 | 动态顺序表必须在插入前CheckCapacity() |
scanf 读字符串加& | scanf("%s", name) ,name 本身就是地址 |
忘记保存文件 | 退出前调用SaveContact() |
六、顺序表的优缺点总结
✅ 优点:
- 实现简单,适合入门;
- 数据连续,缓存命中率高,遍历快;
- 内存开销小(无额外指针)。
❌ 缺点:
- 中间插入/删除需移动大量元素(O(n));
- 扩容有性能开销(malloc + memcpy);
- 2倍扩容可能导致空间浪费(比如容量200,只用了105)。
七、下一步可以怎么优化?
如果联系人超过 1000 个怎么办?
- ✅ 改用链表:插入删除 O(1),但查找变慢;
- ✅ 改用哈希表:查找 O(1),但实现复杂;
- ✅ 加索引:比如按首字母分组;
- ✅ 支持模糊搜索:比如“张”能搜出“张三”“张伟”。
这些,都是你未来要探索的方向!
八、动手挑战!
现在,轮到你了!试试给通讯录加这些功能:
- 🔍 按电话号码查找;
- 📥 从 CSV 文件批量导入联系人;
- 🎨 美化输出格式(对齐、颜色);
- 🔒 增加密码保护。
🌟 欢迎在评论区贴出你的代码或改进想法!
一起交流,一起进步。数据结构不是背概念,而是用出来才有意义!