目录
[Step 1:定义联系人结构体(contact.h)](#Step 1:定义联系人结构体(contact.h))
[Step 2:定义动态顺序表(SeqList.h)](#Step 2:定义动态顺序表(SeqList.h))
[Step 3:顺序表核心函数实现(SeqList.c)](#Step 3:顺序表核心函数实现(SeqList.c))
[Step 4:通讯录业务逻辑(contact.c)](#Step 4:通讯录业务逻辑(contact.c))
[Step 5:主函数(test.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); // 初始容量4
psl->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 文件批量导入联系人;
- 🎨 美化输出格式(对齐、颜色);
- 🔒 增加密码保护。
🌟 欢迎在评论区 贴出你的代码或改进想法!
一起交流,一起进步。数据结构不是背概念,而是用出来才有意义!
