学完顺序表后,用 C 语言写了一个通讯录

目录

一、什么是顺序表?它和普通数组有啥区别?

二、为什么用顺序表做通讯录?

三、项目需求

四、设计思路详解(手把手拆解)

[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 缓存友好;
  • 内存紧凑:没有链表那种每个节点都要存指针的开销;
  • 实现简单:比链表容易理解,适合入门。

💡 注意:如果未来要做"百万级联系人",那就要考虑哈希表或数据库了。但对学习阶段,顺序表刚刚好!

三、项目需求

根据 要求,我们的通讯录要支持:

  1. 存储至少 100 人;
  2. 每个联系人包含:姓名、性别、年龄、电话、地址
  3. 功能:添加、删除(按姓名)、查找(按姓名)、修改、显示全部;
  4. 程序关闭后,数据不丢失(保存到文件);
  5. 使用动态顺序表(不够就自动扩容)。

四、设计思路详解(手把手拆解)

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 文件批量导入联系人
  • 🎨 美化输出格式(对齐、颜色)
  • 🔒 增加密码保护

🌟 欢迎在评论区 贴出你的代码或改进想法!
一起交流,一起进步。数据结构不是背概念,而是用出来才有意义!

相关推荐
拱-卒3 小时前
VB.NET入门学习教程
学习
爱偷懒的。。4 小时前
基于 WebSocket 协议的实时弹幕通信机制分析-抖音
网络·python·websocket·网络协议·学习·js
独自破碎E4 小时前
Leetcode2166-设计位集
java·数据结构·算法
Cikiss4 小时前
LeetCode160.相交链表【最通俗易懂版双指针】
java·数据结构·算法·链表
东方芷兰4 小时前
LLM 笔记 —— 08 Embeddings(One-hot、Word、Word2Vec、Glove、FastText)
人工智能·笔记·神经网络·语言模型·自然语言处理·word·word2vec
知识分享小能手4 小时前
微信小程序入门学习教程,从入门到精通,自定义组件与第三方 UI 组件库(以 Vant Weapp 为例) (16)
前端·学习·ui·微信小程序·小程序·vue·编程
Rock_yzh4 小时前
AI学习日记——深度学习
人工智能·python·深度学习·神经网络·学习
@小张要努力4 小时前
STM32学习记录-0.1 STM32外设
stm32·嵌入式硬件·学习
li星野5 小时前
打工人日报#20251010
笔记·程序人生·fpga开发·学习方法