简介:这是一份面向高校计算机/物联网专业学生的数据结构课程设计报告,围绕图书管理信息系统的设计与实现展开,覆盖图书采编、编目、查询以及借还书等核心业务流程。报告从问题描述与基本要求出发,给出了以书号为主关键字、以书名/作者/出版社为次关键字的索引文件设计,以及基于链表和结构体的图书、读者信息组织方式,并设计了buy、SearchByNum、SearchByName、borrow、return等处理函数。资源包为1个Word文档,大小119KB,整体结构完整,包含题设、概要设计、模块分析与关键代码示意,能够满足课程设计报告撰写或系统功能参考的需要。内容预览中还呈现了折半查找、单链表、外部变量返回查找位置等具体实现细节,便于读者对照理解数据结构在信息管理系统中的实际应用。目前已有1381人学习浏览,适合正在完成同类课程设计或需要借鉴图书管理功能模块设计的同学参考使用。
1. 图书管理信息系统:这份数据结构课设报告到底值不值得照着做
如果你是正在准备数据结构课程设计的人,大概率见过这类题目:做个图书管理信息系统,用C语言实现采编、查询、借还书。听起来简单,但真动手写起来,很多人卡在链表怎么挂、折半查找怎么跟插入配合、借还书时怎么删结点不丢数据。这份《图书管理信息系统的设计与实现》是湖北师范学院2012年的课程设计报告,代码是纯C语言写的,没有数据库、没有图形界面,全部用数组和链表在内存里完成管理。它能解决的核心问题非常明确:用最朴素的数据结构——线性表、链表、折半查找——把一套完整的业务流程跑通。适合三类人:正在做类似课设想找参考的学生、想复习C语言链表操作的人、需要梳理课设答辩思路的初学者。这篇笔记我会把它的设计思路、每个核心函数的实现逻辑和参数细节、以及我自己复现时踩过的坑全部拆开讲。
2. 数据结构选型:数组、链表和结构体怎么组织一本书
2.1 整体架构:为什么图书用数组、借阅者用链表
先看这份报告的总体设计。系统里有两类核心数据:图书信息和借阅者信息。对于图书,报告选择了一个结构体数组来存放,定义是ook boo[MAXSIZE],MAXSIZE被定义为 100,也就是系统最多支持 100 种书。每种书的结构体包含书号num、书名name、作者auth、出版社pub、总库存TotNum、现库存NowNum,以及一个指向借了这本书的读者链表的头指针next。
#define MAXSIZE 100 #define LIST_INIT_SIZE 100 typedef struct LNode { char CardNum[20]; // 图书证号 struct LNode *next; // 指向下一个借阅者 } LinkList; typedef struct book { char num[20]; // 书号 char name[20]; // 书名 char auth[20]; // 作者 char pub[20]; // 出版社 int TotNum; // 总库存 int NowNum; // 现库存 LinkList *next; // 借了该书的人,链表头 } ook;图书用数组而不是链表,这个选择很关键。因为报告里的核心查找函数BinarySearch()用的是折半查找,而折半查找的前提是数据存储在连续内存里、按下标随机访问。数组天然满足这个条件,时间复杂度是 O(log n)。如果你把图书也换成链表,折半查找就没法用了,只能顺序遍历,查找效率会掉到 O(n)。所以这种选型的逻辑是:查询频繁的数据用数组,数量动态变化、增删频繁的借阅记录用链表。
借阅者信息用了一个叫lend的数组,但每个借阅者又挂了一个Bor链表记录他借了哪些书。Bor结构体里存书号BNum、借书日期BorDate、归还日期RetDate,next指向下一条借书记录:
typedef struct Boro { char BNum[20]; // 所借书的书号 char BorDate[8]; // 借书日期 char RetDate[8]; // 归还日期 struct Boro *next; // 下一条借书记录 } Bor; typedef struct LinkBook { Bor *next; // 该图书证的借书记录链表头 char CNum[20]; // 图书证号 int Total; // 借书的数量 } lend[LIST_INIT_SIZE];这里有个容易看混的点:lend不是结构体类型名,是一个数组类型名,lend Lin声明了一个长度为LIST_INIT_SIZE的结构体数组。Retotal这个全局变量记录当前注册过的借阅证数量,新增读者时Retotal++,删除时Retotal--。
2.2 全局变量设计:total 和 Retotal 的边界作用
报告里用了两个关键全局变量:total表示图书种类数,Retotal表示借阅者证数。这两个变量贯穿所有函数,比如BinarySearch()的查找范围就是low=0, high=total-1。我在复现的时候发现一个隐患:如果total初始化为 0,而你在 main 函数里先调用了Borrow()而不是先Buy()录入图书,那么二分查找的范围就是空区间,虽然代码里有用total==0来判断“书库没书”,但不同函数对这种情况的处理并不一致,有的直接返回 false,有的会继续走后续逻辑。所以建议在main()里先把图书采编入库函数跑一遍,保证total>0再开放借书功能。
LIST_INIT_SIZE和MAXSIZE都是 100,但代表的意义完全不同。前者限制借阅者数量,后者限制图书种类数。如果你的测试数据超过 100 种书或者 100 个读者,数组会越界,程序可能直接崩溃。报告里没做动态扩容,这是它的一个明显边界,后面避坑章节会细说。
2.3 三个索引链头文件:主索引、书名索引、作者索引的对应关系
这份报告的题目描述里给了三个索引链头文件——书名索引、作者索引、出版社索引,每个索引格式是“关键字 → 链头地址 → 长度”。比如书名“数据库”对应链头地址 6、长度 2,代表书号为 11021 和 62201 的两本书书名都是“数据库”。
但注意:正文的完整代码里,这个索引文件的设计并没有真正实现。代码里用的是直接在结构体数组上二分查找书号、顺序遍历书名,并没有建独立的多级索引文件。也就是说,题目描述里的索引设计是“要求”,代码实现用的是更朴素的折半查找方案。这一点在答辩时很容易被老师追问:你题目里写了索引链头文件,代码里为什么没有?我的建议是,如果你要交这份报告,要么在文字说明里承认“实际实现采用折半查找替代多级索引”,要么自己补一段索引文件的定义。后面避坑章我会给一个补全方案。
3. 查找与插入:折半查找和有序插入怎么配合才不出错
3.1 BinarySearch 折半查找的实现细节和返回值陷阱
查找是这份报告的灵魂。BinarySearch()的代码我完整贴出来,并标注每一处的逻辑:
int mid = 0; // 外部变量 mid,用来返回查找到的位置 int BinarySearch(ook boo[], char SearchNum[]) { int low = 0, high = total - 1; int found = 0; while (low <= high) { mid = (low + high) / 2; // 取中间下标 if (strcmp(boo[mid].num, SearchNum) == 0) { // 书号相等 found = 1; return 1; // 查找成功 } if (strcmp(boo[mid].num, SearchNum) > 0) high = mid - 1; // 目标在左半区 else low = mid + 1; // 目标在右半区 } if (found == 0) return 0; // 查找失败 }这里有一个非常典型的 C 语言写法:函数只有一个返回值,但调用方往往既想知道“有没有找到”,又想知道“找到的位置”。报告用了一个外部全局变量mid来解决——查找成功后mid保存的就是目标书号在数组中的下标。这个做法在课设里很常见,但它的缺陷也很明显:如果两个线程同时调用这个函数,mid会被互相覆盖。好在这个系统本来就是单线程控制台程序,不会出并发问题,答辩时可以解释为“为了简化接口设计,用外部变量传递查找位置”。
对比strcmp的返回值和二分调整方向,逻辑是对的:strcmp返回负值表示第一个参数比第二个小,正值表示大。但注意原代码里有个笔误,第一版实现写的是if(strcmp(boo[mid].num,SearchNum)!=0) high=mid-1;,这会让所有不相等的情况都往左半区找,导致查找失败。后面修正成了大于 0 才往左、否则往右。这个细节非常值得在答辩时主动提出来,说明你理解了二分法的边界调整逻辑。
另外一个细节:mid在查找失败返回 0 的时候,保留的是最后一次计算的位置。Buy()里插入新书时,就是用这个mid作为插入点,把后面的元素整体后移。所以你会发现mid这个全局变量在查找失败时反而承载了“插入位置”的语义——这是这份代码设计里最巧妙也最容易被忽略的点。
3.2 Buy 采编入库:插入点计算和后移的边界问题
Buy()函数是报告里最长的函数之一,它的核心逻辑分两段:如果二分查找找到了书,直接把总库存和现库存 +1;如果没找到,就把新书插到mid指向的位置后面,并保持数组有序。
void Buy(ook boo[], char BuyNum[]) { if (BinarySearch(boo, BuyNum)) { boo[mid].TotNum++; boo[mid].NowNum++; printf("入库成功。已更改书库中该书的信息。\n"); } if (!BinarySearch(boo, BuyNum)) { int i; for (i = total; i > mid && total; i--) boo[i] = boo[i - 1]; // 从后往前移动,空出 mid+1 位置 strcpy(boo[i].num, BuyNum); // 注意:这里 i 是循环结束后 mid+1 printf("该书在书库中不存在。设立新书目,请补全书的详细信息。\n"); printf("该书购入的数量是:"); scanf("%d", &boo[i].NowNum); boo[i].TotNum = boo[i].NowNum; printf("该书的名字是:"); scanf("%s", boo[i].name); printf("该书的作者是:"); scanf("%s", boo[i].auth); printf("该书的出版社是:"); scanf("%s", boo[i].pub); boo[i].next = NULL; total++; printf("已增加该书的信息。入库成功。\n"); } }这里有几个值得注意的细节。第一,for循环里的条件i > mid && total,total在这里作为布尔值用,如果total==0就不移动任何元素,此时mid还是 0,新书插到下标 0 的位置。第二,循环结束后i的值是mid+1,所以strcpy(boo[i].num, BuyNum)实际写入的是mid+1位置。第三,移动是从total开始、到mid+1结束,也就是把mid+1到total-1的元素整体后移一位,腾出mid+1这个空位。整个逻辑在total不为 0 的时候是对的。
但这里有个隐蔽的翻车点:如果mid在查找失败时指向数组末尾附近,比如total=99、mid=98,新书要插到下标 99,但数组最大容量是MAXSIZE=100,所以最多只能插到下标 99,再插入第 101 种书就会越界。报告里没有任何数组满的判断。我复现时测试到第 100 种书就出问题了。建议补一段判断:
if (total >= MAXSIZE) { printf("书库已满,无法录入新书。\n"); return; }这段代码放在Buy()函数开头,两行就能避免最危险的越界错误。
3.3 SearchByNum 和 SearchByName:两种查询方式各自的适用场景
按书号查用二分查找,按书名查用线性遍历。这个选择很合理,因为数组按书号有序,二分查找才能成立;而书名没有排序,只能遍历。SearchByNum()里有一个细节:如果查到书而且这本书被借出去了,会遍历boo[mid].next链表,把所有借阅者的图书证号打印出来。
void SearchByNum(ook boo[], char SeaNum[]) { LinkList *p; if (!BinarySearch(boo, SeaNum)) printf("对不起,未找到您想查找的书。\n"); else { p = boo[mid].next; printf("书号:%s 书名:%s 作者:%s 出版社:%s 现库存:%d 总库存:%d\n", boo[mid].num, boo[mid].name, boo[mid].auth, boo[mid].pub, boo[mid].NowNum, boo[mid].TotNum); if (p != NULL) { printf("已借该书的图书证号:\n"); while (p) { printf("%s ", p->CardNum); p = p->next; } } printf("\n"); } }按照书名查找的函数实现更简单,就是遍历所有图书,遇到书名相同的就全部打印出来。这个函数有个小问题:它没有处理“没找到任何书”的情况,如果遍历一遍没有匹配项,屏幕上只会显示一行“找到符合该书名的书的详细信息如下:”,然后什么都没有。建议加一个计数变量,遍历结束如果是 0 就提示“抱歉,未找到该书名对应的书。”
4. 借还书核心流程:链表结点的增删有哪些边界要处理
4.1 Borrow 借书:两条链表同时更新的顺序和条件
借书是整个系统里最复杂的操作,因为它要同时维护两条链表:图书结构体里的借阅者链表boo[mid].next,和借阅者结构体里的借书记录链表Lin[i].next。两条链表的更新顺序不能颠倒,如果先更新了图书这边的链表,再在借阅者表里查找证号,万一找不到证号,还得回滚图书链表的修改——代码里没有回滚逻辑,所以必须把不可逆的修改放在最后做。
void Borrow(ook boo[], lend Lin, char BorrowNum[], char CaNum[]) { Bor *p, *q; LinkList *m, *n; int i; if (!BinarySearch(boo, BorrowNum) || total == 0) { printf("书库里没这书。\n"); return; } if (boo[mid].NowNum <= 0) { printf("借阅失败。该书现在库存为0。\n"); return; } boo[mid].NowNum--; // 现库存减1 // —— 第一步:往图书的借阅者链表中追加读者证号 —— if (boo[mid].next == NULL) { m = (LinkList *)malloc(sizeof(LNode)); boo[mid].next = m; strcpy(m->CardNum, CaNum); m->next = NULL; } else { m = boo[mid].next; while (m->next) m = m->next; n = (LinkList *)malloc(sizeof(LNode)); m->next = n; strcpy(n->CardNum, CaNum); n->next = NULL; } // —— 第二步:往借阅者的借书链表中追加书目 —— for (i = 0; i < Retotal; i++) { if (strcmp(Lin[i].CNum, CaNum) == 0) { // 已有该证信息 p = Lin[i].next; while (p->next) p = p->next; q = (Bor *)malloc(sizeof(Boro)); p->next = q; strcpy(q->BNum, BorrowNum); printf("输入归还日期:"); scanf("%s", q->RetDate); q->next = NULL; printf("借阅成功。\n"); break; } } if (i == Retotal) { // 没有该证信息,新建 strcpy(Lin[i].CNum, CaNum); p = (Bor *)malloc(sizeof(Boro)); Lin[i].next = p; strcpy(p->BNum, BorrowNum); printf("输入归还日期:"); scanf("%s", p->RetDate); p->next = NULL; Retotal++; printf("借阅成功。\n"); } }这段代码里有几个值得展开讲解的边界场景。
第一,Lin[i].Next可能为空吗?在正常流程里不会,因为借书时如果读者没有任何历史记录,函数会创建一个Bor结点挂上去。但如果你在 main 函数里直接构造了一个只有证号、没有借书记录的读者,那p=Lin[i].next就是NULL,while(p->next)会直接段错误。这个函数的假设前提是:每个注册读者至少借过一本书。如果要在代码里做防御,得在取p之前判断Lin[i].next==NULL的情况。
第二,scanf("%s", q->RetDate)输入归还日期,但前面Borrow()根本没让用户输入借书日期,BorDate字段是空的。报告里结构体定义了BorDate,但代码里从来没有给它赋值。这是我复现时发现的比较明显的逻辑缺口,你自己补代码的时候要决定:要么录入当前系统日期作为借书日期,要么在借书流程里让用户输入。我建议用time()函数自动生成日期,避免用户多输一步。
第三,借书环节在图书链表中只是记录了证号,没有记录借书日期和应还日期——所有日期信息都存在借阅者链表的Bor结点里。这意味着如果想要按书查“这本书被谁借走了、什么时候该还”,是查不到的,只能知道证号。这是这个课设系统的一个功能边界,答辩时如果被问到,可以如实说是按报告设计做的简化。
4.2 Return 还书:删除链表结点的三种情况和 free 的位置
还书函数Return()的复杂度比借书更高,因为链表删除要区分“删除的是第一个结点”和“删除的是中间结点”两种情况,每种情况的指针操作都不一样。
void Return(ook boo[], lend Lin, char ReturnNum[], char BorrowerNum[]) { Bor *p, *q; LinkList *m, *n; int flag = 0; if (!BinarySearch(boo, ReturnNum) || !total) { printf("书库中无此书。\n"); return; } // —— 第一部分:从图书的借阅者链表中删除该读者 —— m = boo[mid].next; if (m == NULL) { printf("这本书没有借出记录。\n"); return; } if (strcmp(m->CardNum, BorrowerNum) == 0) { // 情况1:要还书的人正好是链表的第一个结点 boo[mid].NowNum++; boo[mid].next = m->next; free(m); } else { // 情况2:遍历找到目标结点 while (m->next) { if (strcmp(m->next->CardNum, BorrowerNum) == 0) { n = m->next; m->next = n->next; free(n); boo[mid].NowNum++; break; } m = m->next; } } // —— 第二部分:从借阅者的借书链表中删除该书的记录 —— for (int i = 0; i < Retotal; i++) { if (strcmp(Lin[i].CNum, BorrowerNum) == 0) { p = Lin[i].next; if (p == NULL) break; if (strcmp(p->BNum, ReturnNum) == 0) { // 还的是借的第一本书 Lin[i].next = p->next; free(p); printf("成功归还该书。\n"); flag = 1; break; } else { // 遍历找对应书号的结点 while (p->next) { if (strcmp(p->next->BNum, ReturnNum) == 0) { q = p->next; p->next = q->next; free(q); printf("成功归还该书。\n"); flag = 1; break; } p = p->next; } } } } // —— 第三部分:清理没有借书记录的借阅证 —— for (int k = 0; k < Retotal; k++) { if (Lin[k].next == NULL) { int j; for (j = k; j < Retotal; j++) Lin[j] = Lin[j + 1]; // 数组整体前移 strcpy(Lin[j].CNum, " "); Retotal--; break; // 注意:break 只删一个空证 } } if (flag == 0) printf("无该证信息。\n"); }这里第三部分的逻辑需要特别说明:它遍历Lin数组,找到第一个next == NULL的借阅证,然后把它后面的所有元素前移一位,相当于删掉了这个空证。但问题在于for循环里找到空证后只执行一次删除,然后break退出。如果同时有多个空证,只清理了第一个。这是个功能缺陷,但不影响主要流程——因为一个证只有在它还完所有书之后才会变成空证,而这种清理只是为了防止数组里堆积垃圾数据。
free()的位置也是个细节。报告里释放借阅者链表的结点用的是free(n)和free(m),释放借书链表的结点用的是free(p)和free(q)。注意在释放之前,指针已经指向了下一个结点,所以释放的是孤立出来的结点,不会影响链表结构。这个删除写法是课设里必考的点,如果在答辩时被问“为什么删除后链表不断开”,你可以直接画图说明:先让前一个结点的next指向目标结点的next,再free目标结点。
5. 避坑指南:复现这份课设报告时的六个翻车点
5.1 数组越界:MAXSIZE 100 的限制和录入第 101 本书
现象:连续录入新书到第 100 种时程序还能正常跑,录入第 101 种时要么打印乱码,要么直接崩溃退出。
原因:Buy()里插入新书用的是for(i=total; i>mid && total; i--) boo[i]=boo[i-1];,当total=100时,移动后写入的boo[100]已经超出数组boo[MAXSIZE]的合法下标 0~99,属于典型的数组越界写。
解决:在Buy()函数开头加判断,total >= MAXSIZE时直接返回并提示书库已满。同时把MAXSIZE改成更大值,比如 200 或 1000,按你的测试数据规模来。我一般会顺手把MAXSIZE和LIST_INIT_SIZE都改成 1024,省得测试数据稍微多一点就踩边界。
5.2 scanf 缓冲区残留:输入数字后再输入字符串导致读取失败
现象:在Buy()里先scanf("%d", &boo[i].NowNum)输入数量,紧接着scanf("%s", boo[i].name)输入书名,结果书名没等到输入就被跳过了,程序直接往后执行。
原因:scanf("%d")读取完数字后,缓冲区内残留了一个换行符\n,下一个scanf("%s")读到这个换行符视为输入结束,直接把空字符串赋给了name。
解决:在每次scanf之后用getchar()吃掉残留的换行符,或者在scanf的格式串里加空格,比如scanf(" %s", boo[i].name)。注意格式串里加空格能跳过任意空白字符,比getchar()更稳妥。报告中多处scanf都加了前导空格,比如scanf(" %d",&boo[i].NowNum),这个细节是原作者刻意处理的,复现时不要自作主张删掉空格。
5.3 BinarySearch 的 mid 全局变量在多处调用时被覆盖
现象:Buy()里先调用了BinarySearch判断书是否存在,然后又调用一次BinarySearch获取mid位置,结果两次查找的结果不一致,第二次返回的mid不是同一个值。
原因:mid是全局变量,每次调用BinarySearch都会重新计算并覆盖它。在Buy()里第一次调用if(BinarySearch(boo, BuyNum))后mid保存的是查找成功的位置;但如果查找失败,mid保存的是最后比较的位置。第二次调用if(!BinarySearch(boo, BuyNum))时mid可能已经变化。
解决:不要连续调用两次BinarySearch,而是用一个局部变量保存结果:
int found; found = BinarySearch(boo, BuyNum); if (found) { boo[mid].TotNum++; ... } else { // 此时 mid 仍然可用,作为插入位置 ... }这一处是报告原代码里最大的效率隐患——每次Buy()都做了两次二分查找,数据量小看不出来,数据量到几千条时能明显感到卡顿。改成一次查找后逻辑也更清晰。
5.4 Return 里的空链表访问:借书人链表的 head 为 NULL 时直接崩溃
现象:运行Return()还一本从没被借出去的书,程序在m=boo[mid].next;之后直接访问m->CardNum,然后段错误。
原因:如果一本书的现存量和总量相同,说明没人借过它,boo[mid].next为NULL。但Return()的第一部分没有判断m==NULL就直接strcmp(m->CardNum, BorrowerNum),空指针解引用必然崩溃。
解决:在m = boo[mid].next;后面加一行判断:
if (m == NULL) { printf("这本书没有借出记录,无法归还。\n"); return; }同样的场景也会出现在借阅者链表里:如果一个读者没有任何借书记录,Lin[i].next是NULL,Return()里p=Lin[i].next; strcmp(p->BNum, ReturnNum)同样会空指针。需要加if (p == NULL) break;防御。
5.5 借书日期缺失:结构体里有 BorDate 但从来没赋值
现象:运行程序时借书功能可以正常完成,但查看借阅者的借书记录时发现BorDate字段是空的,只有归还日期被录入了。
原因:Borrow()里创建Bor结点时只strcpy了BNum和RetDate,跳过了BorDate。结构体定义里有这个字段,但代码里没有对应的输入或赋值逻辑。
解决:在借书时用 C 标准库的time()生成当前日期自动填充:
#include <time.h> time_t now; struct tm *tm_now; char dateStr[9]; time(&now); tm_now = localtime(&now); strftime(dateStr, sizeof(dateStr), "%Y%m%d", tm_now); strcpy(q->BorDate, dateStr);这样借书日期就是系统当天日期,不用用户手动输入,也更贴近真实场景。
5.6 多个空借阅证只清理第一个:Return 里的 break 导致数组留下空洞
现象:连续让两个读者还完所有书后,检查Retotal发现只减少了 1,另一个空证还在数组里,后续新注册的读者可能覆盖掉它、也可能不覆盖。
原因:清理空证的for循环里,找到第一个Lin[k].next == NULL的元素后执行前移和Retotal--,然后立刻break,后续的空证没有被处理。
解决:把break去掉,让for循环继续遍历。但要注意,前移后数组长度变了,下标需要重新调整,所以最简单的做法是重新遍历:删掉一个空证后,k回退一步。或者用双循环,先统计空证数量,再一次性前移。课设级别的代码,我推荐一个最直观的写法:
int k = 0; while (k < Retotal) { if (Lin[k].next == NULL) { for (int j = k; j < Retotal; j++) Lin[j] = Lin[j + 1]; Retotal--; } else { k++; } }这个while循环处理完所有空证,且不会因为前移跳过元素。这是我在复现时踩过坑之后改出来的,比原报告里的for+break方案稳得多。
6. 验证方法与进阶改造:怎么证明这套系统能跑、能答辩
拿到这份报告和代码之后,第一步不是急着改功能,而是先完整跑一遍核心流程,确认原始代码在你的编译器环境下能编译、能运行。我写了一个标准验收流程,照着走一遍基本能覆盖所有功能分支:
# 假设代码文件是 book.c,用 gcc 编译(如果是 Windows 用 Dev-C++ 或 VS 的 C 语言环境也行) gcc book.c -o book.exe # 运行程序 ./book.exe启动后按这个顺序操作:先录入 3~5 本书,其中两本故意用相同书名,方便测试按书名查找的多条结果展示;然后按书号查找一本录入过的书、一本没录入过的书;接着借书——第一次借一本书,第二次借同一本书给另一个读者,第三次再借一本库存已清零的书验证“借阅失败”;最后还书——先还第一本书,再还第二本书,还一本没借出去的书看提示。每一步都记录输出结果。如果在这个过程中某个环节崩了,对照上一章的避坑表逐条排查。
验证通过后,如果想让这份课设看起来“更有东西”,有两个性价比很高的改造方向。第一个是给Buy()补充库存上限和数组满检查,因为原代码的越界问题是硬伤;第二个是给借书流程自动生成借书日期,并且把归还日期从“手动输入”改成“借书日期 + 30 天”自动计算——这两个改动加起来不超过 20 行代码,但能让答辩时的功能完整度明显提升。
如果你还想再进一步,可以把boo数组改成链表结构、把BinarySearch改成按书名建立的哈希索引——但我不建议你这么做。因为这份报告的核心价值在于它是一份“刚好够用”的课设代码:每种数据结构都有明确的出场理由,折半查找配合有序数组、链表用于动态增删的借阅记录,逻辑闭环清晰,评委老师不会追问太难的问题。你大改数据结构之后,反而要解释为什么不用折半查找了、哈希冲突怎么处理,给自己挖坑。
我自己复现这个项目的体验是:它最大的优点不是代码写得多么优雅,而是把所有课设需要讲清楚的概念都集中在了几个关键函数里——二分查找、链表增删、结构体数组、全局变量传参,每一个都能在答辩时展开讲三分钟。从那以后我每次做课设复现,都强制自己先在纸上画出链表结构图、标清楚每次增删的指针变化,再去改代码。这个方法让我少踩了一半的指针坑,希望帮到你。
本文还有配套的精品资源,点击获取