ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

顺序表与通讯录实战:C语言数据结构核心操作全解析

顺序表与通讯录实战:C语言数据结构核心操作全解析 把顺序表讲清楚顺带交付一个能跑的通讯录用C语言把这两件事揉在一起是大学里数据结构课最常见、也最值得认真做的一个组合。很多同学上课听“线性表”“顺序存储”听得云里雾里其实代码一上手就明白了顺序表说白了就是一块连续内存里挨个存数据就像一排编号固定的储物柜你打开第5个柜门里面放的就是第5个元素存取都靠下标直接算地址。这个项目以通讯录为载体把顺序表的建表、插入、删除、查找、遍历、扩容这些核心操作全部映射到真实场景里。每添加一个联系人就是一次顺序表尾插按姓名找人就是一趟顺序遍历删除一个联系人就是一次数据搬移覆盖。学完这个项目你不光会写顺序表还能搞清楚数据结构到底在解决什么问题。我直接把标题里的通讯录源码拆开讲透从结构体设计讲到具体函数实现再把容易踩的坑都标出来。不管你是刚学C语言、正在备考期末、还是想拿一个完整项目练手这篇都适用。1. 顺序表的设计思路与核心结构1.1 顺序表和链表到底选谁先理解“连续”这两个字的价值数据结构这门课里“线性结构”是第一个大块而顺序表是线性结构里最朴素的一种实现。为什么先学它因为数组几乎是所有编程语言里最基础的数据组织方式而顺序表其实就是“穿着数据结构外衣的数组”。所谓“顺序存储”核心就一句话逻辑上相邻的元素在物理内存里也相邻。也就是说第i个元素和第i1个元素在内存里是紧挨着的中间不隔任何东西。这个特性带来了两个极其重要的推论第一随机访问代价极低。想拿第i个元素不用从头开始数直接拿起始地址加上 i × sizeof(元素类型) 就是目标地址时间复杂度O(1)。第二插入和删除要付出搬移代价。因为物理上相邻你想在第i个位置塞一个新元素后面的所有元素都得往后挪同理删掉第i个元素后面的全部得往前挪。最坏情况O(n)。这个“读快写慢”的特性和链表正好相反。链表逻辑相邻但物理不一定相邻靠指针串联所以访问第i个元素得从头走O(n)但因为节点之间是独立分配的插入删除只需要改指针O(1)。想想你手机里的通讯录是什么使用场景大部分时候是翻找人、按名字搜索添加是在末尾追加偶尔才会在中间插一个或者删一个。这就是典型的“读多写少、尾部追加为主”。用顺序表恰好扬长避短。很多教材把这个选择逻辑讲得太干巴我这里用一个类比顺序表是连续排列的图书架找第5本直接走过去拿链表是借阅登记链表你得从第一条记录往下数。书架找书快但往中间塞一本新书后面的书全要挪位置。1.2 C语言实现顺序表的结构体设计两层结构别把“表”和“数据”混在一起写顺序表第一步是定义结构体。很多初学者写出来的代码是这样的typedef struct { char name[20]; char phone[12]; } Contact;然后直接定义一个 Contact arr[100]; 就开始用。这种做法不是不行但你在学数据结构得按数据结构的思路来——把“数据元素”和“装着数据元素的表”分开。我们要定义的是两层东西底层数据元素长什么样在通讯录里一个联系人就是一条记录包含姓名、电话、分组等字段。顶层顺序表本身长什么样它得记录三件事元素存哪里指针、当前有多少个元素length、最多能存多少个capacity。顶层结构体这样定义#define MAX_SIZE 100 typedef struct { Contact data[MAX_SIZE]; // 用定长数组存元素 int length; // 当前已存储的联系人个数 } SeqList;这是第一种方式定长数组写起来简单适合作业和考试。缺点是容量写死了100个存满了就再也插不进去。第二种方式是动态数组用一个指针指向堆上申请的内存typedef struct { Contact *data; // 指向堆空间的首地址 int length; // 当前已存储的联系人个数 int capacity; // 当前容量 } SeqList;动态版本需要手动 malloc、realloc代码量增加但容量能随需扩张。通讯录项目我建议直接用动态版本因为你要体现的不只是“能用”而是“知道为什么要这样设计”。后面第2节会专门讲扩容。1.3 为什么通讯录用“结构体套结构体”特别合适通讯录里的一个联系人天然就是一个结构体。这和教材里经典的“学生信息管理”是同一类问题——每条记录有多个字段每个字段类型不同。这个项目里联系人的字段我从简设计你拿到源码后可以自行扩展typedef struct { char name[20]; // 姓名 char phone[15]; // 电话留14个字符加一位结束符 char group[10]; // 分组比如家人、同事、朋友 } Contact;字段为什么这么设计有讲究。name 用 char[20]是因为 C 语言里字符串就是字符数组没有原生的 string 类型。定长数组的好处是结构体大小固定内存布局简单坏处是超出长度会直接溢出破坏相邻数据所以输入时务必限制长度。phone 用 char[15] 而不是 long因为电话号码可能带区号、短号、分机号数字开头的 0 会被整型吃掉比如 010 开头用字符串存最安全。group 字段是给后续扩展用的比如你可以按分组遍历、按分组统计数量。然后把这个结构体作为顺序表的数据元素类型。于是顺序表的完整定义就变成了typedef struct { Contact *data; int length; int capacity; } AddressBook;这个名字一改顺序表就“变成”了通讯录。这就是数据结构里“逻辑结构 存储结构 操作集合”三要素中前两个的落地逻辑结构是线性表存储结构是顺序表数据元素是联系人。代码里的一切设计都围着这三句话转。2. 核心操作的原理与实现细节2.1 初始化为什么返回值要用指针以及“空表”长什么样顺序表的初始化本质就是给结构体成员赋初值。动态版本多一步在堆上申请一块初始内存。void InitList(SeqList *L, int initCapacity) { L-data (Contact *)malloc(initCapacity * sizeof(Contact)); if (L-data NULL) { printf(内存分配失败\n); exit(1); } L-length 0; L-capacity initCapacity; }注意函数签名里传的是SeqList *L也就是指向顺序表结构体的指针。为什么不能直接传SeqList L因为 C 语言函数参数是值传递传值进来你在函数里改的只是副本外面纹丝不动。很多初学者在这里栽跟头明明调了 InitListlength 还是垃圾值因为原表根本没被初始化。看懂这一点你就明白了 C 语言数据结构里一个核心规律要改谁的内容就传谁的地址。改普通变量传指针改指针变量的指向就得传二级指针。后面销毁表的时候还会见到这个规律。初始化完成后这个表的状态是data 指向一块合法内存length 为 0capacity 为初始容量。这就是“空表”的定义不是指针为 NULL而是元素个数为 0。2.2 插入操作的完整流程先检查再挪动最后赋值插入是顺序表最重要的操作它把“物理相邻导致插入要搬移”这个特性展现得淋漓尽致。int InsertContact(SeqList *L, int pos, Contact c) { // 1. 位置合法性检查 if (pos 1 || pos L-length 1) { printf(插入位置非法pos%d, length%d\n, pos, L-length); return 0; } // 2. 容量检查满了就扩容 if (L-length L-capacity) { ExpandList(L); } // 3. 从后往前把元素一个一个往后挪腾出 pos 位置 for (int i L-length - 1; i pos - 1; i--) { L-data[i 1] L-data[i]; } // 4. 赋值length 自增 L-data[pos - 1] c; L-length; return 1; }这里有三个细节值得单独拎出来说。第一位置约定。很多教材里顺序表的插入位置用“位序”表示从1开始而数组下标从0开始。所以插入到第pos个位置实际数组下标是 pos - 1。这个换算关系考试爱考写代码也最容易混。第二为什么从后往前挪。如果你从前往后挪前一个元素还没腾地方后一个已经把它的位置占了数据就被覆盖了。从后往前先把最后一个挪到末尾空位再挪倒数第二个依次类推每个元素都安全着陆。这是顺序表插入操作里最核心的思维点画一张图想清楚就永远不会忘。第三插入位置的边界。pos的范围是1到length1。为什么能等于length1因为表尾插入是合法的等价于append。这保证了顺序表支持在末尾添加联系人也就是通讯录里“新增联系人”这个功能。2.3 删除操作的实现覆盖式删除注意内存回收删除操作和插入是镜像关系先检查位置然后把目标位置后面的元素一个接一个往前挪最后 length 减一。int DeleteContact(SeqList *L, int pos) { if (pos 1 || pos L-length) { printf(删除位置非法\n); return 0; } for (int i pos - 1; i L-length - 1; i) { L-data[i] L-data[i 1]; } L-length--; return 1; }删除要说明两个问题。一是“删除后最后一个元素要不要清空”。我在源码里没有显式清空只是把 length 减一。这意味着最后一个元素在物理上还残留在数组里但因为 length 已经表示“有效元素只有前n个”下次插入时它会自然被覆盖。这一点和“空表的length为0”是同一个逻辑体系的。如果你写的是动态数组且元素是指针务必手动 free 避免内存泄漏但这里元素是结构体不是指针不需要额外处理。二是删除接口设计成按位置删还是按值删。菜单层面用户根本不知道联系人“在第几个位置”所以通讯录的真正删除流程是先按姓名搜索找到下标再调 DeleteContact。这启发了一个更好的接口设计——封装一个“按名字删除”的函数底层用循环查找找到就删。源码里我两个都提供了方便你看清分层逻辑。2.4 查找与修改顺序表里最舒服的两种操作因为有随机访问的能力顺序表的查找和修改实现起来非常直接。按序号查找直接返回 data[i]O(1)这是顺序表碾压链表的地方。按值查找因为数据没有排序只能从头到尾遍历一遍挨个比较O(n)。这在通讯录里就是“按姓名搜索”int FindByName(SeqList *L, char *name) { for (int i 0; i L-length; i) { if (strcmp(L-data[i].name, name) 0) { return i; // 返回下标 } } return -1; // 没找到 }返回值设计成下标而不是直接返回结构体这里是有讲究的。有了下标你既可以读取这个联系人也可以配合 DeleteContact 删除它还可以配合 ModifyContact 修改它。一个查找函数能支撑三个功能这就是底层函数返回”索引“的价值。修改操作就一句话先找到下标然后直接对 data[i] 的字段重新赋值。因为 C 语言的结构体可以直接整体赋值所以也可以L-data[i] newContact;。但实际项目中我建议逐个字段修改这样用户不必重新输入所有字段体验更好。2.5 动态扩容为什么“翻倍增长”而不是“每次加1”动态顺序表的核心是扩容。当 length 快要等于 capacity 时插入函数必须找一个更大的内存空间把旧数据整体搬过去。void ExpandList(SeqList *L) { int newCapacity L-capacity * 2; Contact *newData (Contact *)malloc(newCapacity * sizeof(Contact)); if (newData NULL) { printf(扩容失败\n); exit(1); } for (int i 0; i L-length; i) { newData[i] L-data[i]; } free(L-data); L-data newData; L-capacity newCapacity; printf(已扩容至 %d\n, newCapacity); }容量为什么要翻倍而不是每次加1这是数据结构里一个经典的“均摊复杂度”问题。假设从容量1开始每次加1地扩容那么插入n个元素扩容时总共要拷贝 123...(n-1) 次大约是 O(n²) 的拷贝量。而每次翻倍拷贝次数是 1248...n ≈ 2n均摊到每个插入操作上只有O(1)。换句话说翻倍扩容让“偶尔一次昂贵操作”被分摊到了每一次插入上整体性能不会因为扩容次数太多而劣化。这个思想在 vector、ArrayList 等高级数据结构里也是核心设计你在这里掌握它等于提前看透了现代编程语言的容器实现。还有个小细节为什么用 malloc 申请新空间再用循环拷贝而不是直接用 reallocrealloc 在堆空间足够时可能原地扩展省一趟拷贝但无法保证一定成功用 malloc 手动拷贝 free 的流程逻辑最清晰也最不容易出错。初学阶段我不建议用 realloc等你能熟练处理指针再优化不迟。3. 通讯录项目的完整落地3.1 项目功能清单与整体架构有了顺序表这个“发动机”通讯录就只是由它驱动的业务层。整个项目我分成三层存储层顺序表的全部操作InitList、InsertContact、DeleteContact、FindByName、ExpandList、PrintList、DestroyList业务层通讯录的具体功能添加、删除、查找、修改、显示、清空交互层main函数里的菜单循环源码组织方式用一个单文件也能写完但我建议拆成三个文件seqlist.h放头文件和声明seqlist.c放顺序表实现main.c放主函数和菜单。这和真实项目的目录结构习惯一致也方便你以后扩展。3.2 完整源码头文件与顺序表实现先看头文件把类型和函数声明统一集中// seqlist.h #ifndef SEQLIST_H #define SEQLIST_H #include stdio.h #include stdlib.h #include string.h typedef struct { char name[20]; char phone[15]; char group[10]; } Contact; typedef struct { Contact *data; int length; int capacity; } SeqList; void InitList(SeqList *L, int initCapacity); void ExpandList(SeqList *L); int InsertContact(SeqList *L, int pos, Contact c); int DeleteContact(SeqList *L, int pos); int FindByName(SeqList *L, char *name); void ModifyContact(SeqList *L, int pos, Contact newContact); void PrintList(SeqList *L); void DestroyList(SeqList *L); #endif头文件里的#ifndef是防止重复包含的经典写法多个 .c 文件互相 include 时能避免重复定义报错。这个习惯你从一开始就要养成。顺序表实现文件seqlist.c就是把上一节讨论的每个函数写完整。这里特别说一下修改函数它和插入删除配合起来是这样的void ModifyContact(SeqList *L, int pos, Contact newContact) { if (pos 1 || pos L-length) { printf(位置非法修改失败\n); return; } L-data[pos - 1] newContact; }这个函数业务层用得不多因为用户很少会一次性把联系人所有字段重新输入一遍。实际菜单里的“修改”是先按名字找到下标然后提示“留空表示不修改”只更新用户填写的那几个字段。这是我在做项目时自己加的交互优化源码里保留了这个版本供你体验更好的操作手感。3.3 主程序与菜单循环如何设计一个不怕用户乱输入的交互main.c 是整个项目面向用户的门面。通讯录不是库用户是活生生的人所以交互逻辑比存储层更细致。我设计的菜单循环长这样int main() { SeqList book; InitList(book, 10); int choice; char keyword[20]; Contact temp; int pos; while (1) { printf(\n 通讯录管理系统 \n); printf(1. 添加联系人\n); printf(2. 删除联系人\n); printf(3. 查找联系人\n); printf(4. 修改联系人\n); printf(5. 显示所有联系人\n); printf(6. 清空通讯录\n); printf(0. 退出系统\n); printf(\n); printf(请输入功能序号); scanf(%d, choice); getchar(); // 吸收回车防止影响后续字符串输入 switch (choice) { case 1: printf(请输入姓名); fgets(temp.name, 20, stdin); temp.name[strcspn(temp.name, \n)] \0; printf(请输入电话); fgets(temp.phone, 15, stdin); temp.phone[strcspn(temp.phone, \n)] \0; printf(请输入分组); fgets(temp.group, 10, stdin); temp.group[strcspn(temp.group, \n)] \0; if (InsertContact(book, book.length 1, temp)) { printf(添加成功\n); } break; case 2: printf(请输入要删除的姓名); fgets(keyword, 20, stdin); keyword[strcspn(keyword, \n)] \0; pos FindByName(book, keyword); if (pos -1) { printf(查无此人\n); } else if (DeleteContact(book, pos 1)) { printf(删除成功\n); } break; case 3: printf(请输入要查找的姓名); fgets(keyword, 20, stdin); keyword[strcspn(keyword, \n)] \0; pos FindByName(book, keyword); if (pos -1) { printf(查无此人\n); } else { printf(姓名%s 电话%s 分组%s\n, book.data[pos].name, book.data[pos].phone, book.data[pos].group); } break; case 4: // 修改逻辑 printf(请输入要修改的姓名); fgets(keyword, 20, stdin); keyword[strcspn(keyword, \n)] \0; pos FindByName(book, keyword); if (pos -1) { printf(查无此人\n); } else { printf(请输入新的姓名); fgets(temp.name, 20, stdin); temp.name[strcspn(temp.name, \n)] \0; printf(请输入新的电话); fgets(temp.phone, 15, stdin); temp.phone[strcspn(temp.phone, \n)] \0; printf(请输入新的分组); fgets(temp.group, 10, stdin); temp.group[strcspn(temp.group, \n)] \0; book.data[pos] temp; printf(修改成功\n); } break; case 5: PrintList(book); break; case 6: book.length 0; printf(通讯录已清空\n); break; case 0: DestroyList(book); printf(已退出系统内存已释放。\n); return 0; default: printf(无效输入请重新选择\n); break; } } return 0; }这段代码里有一行经常被忽略但极重要getchar();在scanf(%d, choice);之后。因为 scanf 读完整数后回车键产生的换行符还留在输入缓冲区里如果不清掉紧接着的 fgets 会把换行符当空字符串读走导致姓名输入直接被跳过。这是C语言控制台程序里最经典的坑我见过无数人在这里卡一晚上。另外字符串输入我用的是fgets而不是scanf(%s)因为 scanf 遇到空格会终止读取输“张 三”这种带空格的名字就会截断fgets 则会读满整行再通过strcspn(temp.name, \n)把末尾的换行符替换成字符串结束符干净利落。3.4 显示联系人打印函数暴露的数据布局打印整个通讯录是顺序表遍历操作的直观展示void PrintList(SeqList *L) { if (L-length 0) { printf(通讯录为空\n); return; } printf(%-4s %-12s %-8s\n, 序号, 姓名, 电话, 分组); for (int i 0; i L-length; i) { printf(%-4d %-12s %-8s %-8s\n, i 1, L-data[i].name, L-data[i].phone, L-data[i].group); } printf(共 %d 位联系人\n, L-length); }你看遍历的顺序表和访问数组没区别直接按下标取。这也是为什么实现“显示所有联系人”时一行循环就搞定。如果你的通讯录数据量是几万条顺序表的内存连续性反而会让CPU缓存命中率高比散落各处的链表节点更快。这一点在真实系统里是顺序表的重要优势顺带一提。3.5 销毁表凡是 malloc 出来的都记得 free程序退出前调用销毁函数把堆内存还回去void DestroyList(SeqList *L) { free(L-data); L-data NULL; L-length 0; L-capacity 0; }这是很多课程设计里最容易被忽略的一步。程序退出时操作系统会自动回收内存但在长期运行的程序里如果每次都 leak 一点迟早会把内存耗尽。好习惯从第一个C项目开始养成谁的地址由 malloc 分配谁就有义务负责 free。注意这里的函数参数是SeqList *L在函数内部把 L 的 data 成员置为 NULL并没有改变 L 这个指针本身。所以不需要二级指针。什么情况下需要二级指针当你在函数里要给整个顺序表重新 malloc 分配而调用者的 L 变量本身要被更新时就需要传SeqList **L。初始化和销毁都不涉及这个场景一级指针就够了。4. 实际开发中遇到的坑与排查记录4.1 插入位置混乱位序和下标的关系必须死记我自己的项目初期版本里插入联系人时直接用了InsertContact(book, book.length, temp)。看起来对但仔细推敲length 是5时当前位置5并不存在有效元素下标是0到4。其实这个写法最终也能插入成功因为 pos5 恰好落在最后一个元素的后面是为尾插。但如果你写的是 pos length 1 呢那就直接越界了会写入 data[5]length 还是5下一次打印“共5位联系人”时第6个位置的数据就丢了。怎么避免在 InsertContact 内部已经做了位置合法性检查pos 1 || pos length 1就返回失败。所以业务层即使传错最多是插错位置不会破坏内存。但作为开发者你必须明确传入的pos是用户视角的“第几个”函数内部自动转换到下标。实在记不住的可以在函数入口打印一行printf(即将在位置%d插入\n, pos)调试完再删掉。4.2 字符串输入被跳过scanf和fgets混用的缓冲区问题这个问题在3.3节已经提到这里展开记录一下我调试时的经历。第一次写完菜单输入“1”添加联系人结果程序直接输出了“请输入姓名请输入电话”两行连输入的机会都不给。原因就是 scanf(%d) 后残留的换行符被 fgets 吃掉了。排查方法在 scanf 后加一行 getchar() 或者while (getchar() ! \n);把缓冲区清干净。注意后者更严谨因为如果用户输入了非数字字符缓冲区里可能残留多个字符。后来我干脆规定项目里统一用 fgets 读所有输入数字也先读成字符串再 atoi 转绕开混用带来的问题。这是我在实际项目里沉淀的经验别图省事用 scanfC语言里处理带空格的字符串输入fgets 永远比 scanf 稳。4.3 内存泄漏与野指针malloc、free、置空三兄弟的纪律项目跑到第15次添加联系人后程序突然卡死。排查发现是扩容函数里 free 了旧内存但结构体数组里存的是结构体而不是指针所以没有浅拷贝问题free 之后旧内存里的数据本来就不需要了。问题出在另一个地方主程序里我定义了一个Contact *p试图保存 FindByName 返回的地址删除联系人之后又用这个 p 打印信息。删除时数据搬移覆盖了那块内存p 变成了悬垂指针打印出来就是乱码。这件事教育了我顺序表中的“位置”和“指针”都不是永久契约任何插入删除都可能让旧指针失效。想长期保有一个联系人正确的做法是保存它的下标或者姓名关键字用完临时取地址不要提前存指针。4.4 常见问题速查表现象可能原因排查与解决插入后打印多一位乱码pos传成了length2检查业务层pos传参边界必须≤length1输入姓名时跳过不等待scanf残留换行符在scanf后清空输入缓冲区统一改用fgets删除联系人后功能错乱删除后未处理length检查DeleteContact末尾是否有length--程序运行几次后崩溃扩容失败或越界检查ExpandList返回值与边界条件打印联系人出现字符串后面带乱码字符串没加结束符fgets读取后一定手动补\0退出后内存泄漏未调用DestroyListmain函数退出分支调用free查询中文姓名失败编码或fgets截断问题用相同编码编译运行确认输入长度不超数组菜单循环死循环choice读取失败检查是否清空缓冲区输入非数字后状态是否恢复这张表里的每一条都是我从调试记录里捞出来的真实案例写作业或者做课设时遇到同样现象直接把对应行抄走就行。5. 扩展思路从通讯录项目还能继续走向哪里做到这里你已经有了一个能跑、能存、能查、能改的通讯录顺序表的主要知识点也覆盖到位了。但要是就此打住项目的含金量还差一口气。我建议你沿着下面三个方向再推进一步哪怕只做一个你对数据结构的理解都会明显加深。方向一是按字段查找。现在的查找只支持姓名一个通讯录里完全可能有多个“张三”。你可以扩展成输入姓名循环遍历把所有叫张三的人全部打印出来附上分组和电话让用户选一个操作。这个需求推广开来就是“按条件筛选”需要你理解顺序表的遍历其实可以叠加任意业务逻辑。方向二是文件持久化。现在程序一退出数据全没了。加上“保存到文件”和“从文件加载”两个功能保存就是遍历顺序表把每条记录按行写入文本文件加载就是逐行读取依次调用 InsertContact 插到表里。文件里一行一个字段用逗号分隔比如张三,13812345678,家人。这个功能做完你的项目就从“课程作业”升级成了“真正可用的工具”同时你会顺便掌握C语言文件操作的基本套路。方向三是性能测量。随便造50000条联系人分别统计用顺序表和单链表做“尾部插入”的时间差异。你会发现顺序表因为内存连续、局部性好插入速度反而可能比链表更快而中间插入时顺序表明显吃亏。这个实验做完你以后选数据结构就不是靠背结论而是靠实测数据了。这三个方向里我身边的学生普遍反馈文件持久化最有成就感——自己写的通讯录终于能在第二天打开时还记得昨晚存的名字了。建议你先做这个。6. 最后的实操建议为了帮你避开不必要的弯路我把这个项目中个人体会最深的几个经验集中在这里再强调一遍。第一不要急着写功能代码先画图。拿纸笔画一个带有10个格子的长条标上编号0到9然后模拟插入“张三”到第3个位置、删除第5个位置这两个操作把元素移动的方向用箭头画出来。图一画完代码就是照着图誊写的事。第二每个函数只做一件事函数命名要动词开头。InsertContact 就是插入FindByName 就是查找PrintList 就是打印。不要写一个300行的 main 把所有逻辑塞进去否则出了 bug 你会连定位都是难题。分层设计是 C 语言项目的基本修养。第三测试要有“边界意识”。不要只测正常操作刻意去测连续插入到容量上限再插一条删除第一个和最后一个元素查找不存在的名字空表显示。这些边界情况才是隐藏 bug 最多的角落也是期末机考的扣分点。第四代码里每个指针都要问自己这个指针是谁分配的谁负责释放如果答案是”不知道”那你必然在内存管理上出错。顺序表和通讯录的组合是一个性价比极高的练手项目。原理上它串起了结构体、指针、动态内存、数组操作、字符串处理、文件读写应用上它又是一个人人用过、需求明确的真实系统。把这个项目吃透你后续学链表、栈、队列、二叉树时会发现很多思维都是相通的——无非是“数据怎么存”和“操作怎么做”这两个问题在不同结构下的不同答案。源码里所有函数我都给了注释你可以直接编译运行也可以在此基础上加字段、加排序、加文件存储把它变成属于你自己的作品。动手改第一行代码的时候项目才开始真正属于你。
返回列表