简介:这是一套基于C语言链表实现的完整图书管理系统课程设计级项目,面向计算机专业初学者与数据结构实践者,解决传统数组式管理在动态增删改查场景下的性能与扩展性瓶颈问题。资源包共35个文件,包含9个核心C源码(如main.c、bookinfo.c、userRegisterLogin.c)、2个关键数据文件(books.txt和users.txt用于持久化存储)、1个头文件head.h、2个布局文件及可直接运行的图书管理系统.exe,辅以程序设计说明书(docx/pdf)和Makefile.win等构建支持文件,总大小2.96MB。已有5547人学习下载,体现了其在教学实践中的广泛认可度。读者可获得模块清晰、职责分离的工程化代码结构——管理员与读者双角色功能完备,支持导入导出、组合查询、多字段排序与借阅统计,并具备良好的可读性与调试基础,是理解链表应用、文件I/O与小型系统架构的优质范例。 到期末周了,又到了一年一度C语言大作业冲刺的时候。图书管理系统,这个题目我估计每个学校都在用,链表存储、文件保存、控制台交互,三件套齐活。但说实话,很多同学交上来的代码,老师一眼扫过去就知道是拼出来的还是自己写的——不是代码跑不通,而是整体的设计思路、函数划分、错误处理太“散”了。今天我就把这个项目从数据结构设计、函数拆解到文件格式约定、边界处理,完整过一遍。你把这篇文章吃透,不仅能把作业写完,还能在验收的时候从容回答老师的追问。

1. 这个项目到底要做什么:需求拆解与模块划分

图书管理系统的核心价值其实就一句话: 用C语言模拟一个最简化的图书借阅流程 。它跟真实的图书馆管理系统没法比,但在数据结构课程设计里,它恰好能把链表操作、文件持久化、结构体嵌套、字符串处理这些C语言的核心知识点全部串起来。

1.1 从标题能提炼出的隐含需求

你仔细看标题——“C语言图书管理系统 链表实现 功能齐全(书籍信息以及用户信息保存在附带的txt文件中)”,里面有三个关键信息:

  • 链表实现 :说明数据的动态存储用链表,不是定长数组。这要求你处理节点的动态创建、插入、删除、查找、排序、释放。
  • 功能齐全 :这词儿很微妙,不同老师对“齐全”的定义不一样。但最基本的底线是——图书的增删改查、用户的注册登录、借书还书,这六件事不能少。往上还可以加:按书名/作者/ISBN搜索、按出版社统计、按借阅次数排序、逾期罚款计算、图书数量超限校验。
  • txt文件保存 :程序退出后数据不能丢,重启后要能从txt里加载回来。这是文件读写操作的考点,后面我会重点讲。

1.2 模块划分:先画清楚分工再做代码

我见过太多同学一上来就写main函数,写完两百行发现逻辑乱套。正确的做法是先分模块。以这个项目为例,合理的模块划分应该是:

模块 职责 涉及的知识点
数据层 图书/用户结构体定义、链表头节点管理 结构体、指针
持久层 从txt加载数据、保存数据到txt 文件读写、字符串解析
业务层 增删改查、借书还书、排序统计 链表操作、排序算法
交互层 菜单循环、接收用户输入、展示结果 printf/scanf、输入校验
入口层 程序初始化、主循环、退出清理 main函数设计

建议你写代码的时候,把同层代码放同一个文件里拆成函数,不要全部堆在main里。哪怕你最终只提交一个.c文件,也 写在文件里用注释分区分好 ,老师看到有层次感的代码,印象分会高很多。

2. 链表数据结构怎么定:节点设计决定后面的路好不好走

2.1 图书节点和用户节点要分开,不要图省事共用结构体

有些同学为了省事,用一个Book结构体把书名、作者、借书人全塞进去。这种做法在应付极简演示时勉强能跑,但一旦涉及到“一个用户同时借多本书”或者“同一本书被多个用户借过”这种逻辑,就崩了。

正确的设计是 图书链表和用户链表分开维护 ,它们之间靠“借书信息”建立关联。我推荐的做法是:

// 图书信息
typedef struct Book {
    char isbn[20];          // ISBN号,唯一标识
    char title[100];        // 书名
    char author[50];        // 作者
    char publisher[50];     // 出版社
    int total_count;        // 馆藏总数
    int available_count;    // 可借数量
    int borrow_count;       // 累计借出次数(用于排序)
    struct Book *next;      // 链表指针
} Book;

// 用户信息
typedef struct User {
    char user_id[20];       // 学号/工号,唯一标识
    char name[50];          // 姓名
    char password[20];      // 密码
    char borrowed_isbn[10][20]; // 当前借阅的ISBN列表,最多10本
    int borrowed_num;       // 当前借了几本
    float fine_due;         // 待缴罚款
    struct User *next;      // 链表指针
} User;

你可能会问:为什么用户节点里不直接放一个“借阅记录链表”,而用一个固定数组存ISBN?因为对于大作业这个场景,固定数组更简单、更直观,而且方便保存到txt文件——你遍历文件时按固定格式读回来就行,不用频繁malloc。如果你想把项目做成进阶版,可以把 borrowed_isbn 换成指向 BorrowRecord 结构体的链表,但那样文件格式和加载逻辑都会复杂一倍,得分不是特别追求优秀的话,固定数组够用了。

2.2 链表头节点:用不存数据的头节点还是直接指针?

这是很多新手容易纠结的问题。两种做法我都用过,给你说一下实测感受:

  • 直接用指针指向第一个有效节点 :实现简单,但删除第一个节点时要特殊处理“头指针指向第二个节点”的情况,代码里全是 if (prev == NULL) 这类分支,容易漏判断。
  • 用带数据的头节点 :删除和新增逻辑统一,不用考虑头指针更新的特殊情况,写起来更省心,但如果你遍历链表打印全部节点时不小心把头节点也打印出来,会多一行脏数据。

我个人的建议是: 链表头节点用于存储“总数”这类聚合信息,不存储实际业务数据 。比如头节点的 total_count 记录馆藏图书数量,方便查询。这样既统一了操作逻辑,又多了一个统计信息的存储位置。代价是你需要用 head->next 表示第一个真正的图书节点,遍历时从 head->next 开始。

// 创建带头节点的空链表
Book* create_book_list(void) {
    Book *head = (Book*)malloc(sizeof(Book));
    if (!head) {
        printf("内存分配失败!\n");
        exit(1);
    }
    // 头节点不存有效数据,total_count记录链表长度
    strcpy(head->isbn, "");
    head->next = NULL;
    head->total_count = 0;
    return head;
}

2.3 为什么这里选链表而不是数组

既然图书信息已经存在txt文件里了,为什么不一次性读出全部数据放到结构体数组里?原因主要有三点:

  1. 增删操作更自然 :数组中间删除一个元素,后面所有元素都得前移,时间复杂度O(n)。链表删除节点,改一下前后指针就行。
  2. 内存按需分配 :文件里有多少条记录,就malloc多少个节点。不会出现“数组开小了装不下,开大了浪费内存”的问题。
  3. 课程考核要求 :这是最现实的原因。链表操作本来就是C语言课程的重点,老师明确要求“链表实现”,你交个数组方案上去,可能连验收都过不了。

3. 文件存储格式与读写:txt文件不是简单地读一行写一行

文件读写是这个项目里最“阴”的部分。很多同学在内存里操作链表没毛病,一涉及保存/加载就各种问题。核心原因就是 没有设计好文件格式 。

3.1 文件格式设计:定好分隔符和字段顺序

我推荐这样一个格式(books.txt):

#ISBN#书名#作者#出版社#总数量-可借数量#借出次数
978-7-111-54778-3#C程序设计#谭浩强#清华大学出版社#5-3#12
978-7-302-45678-9#数据结构#严蔚敏#清华大学出版社#3-2#8
  • 用 # 做字段分隔符,因为书名和作者里不太可能出现 # ,比用空格分隔更安全。
  • 总数量和可借数量之间用 - 连起来,一个是 5-3 表示总馆藏5本,可借3本。这样一行一个字段的逻辑清楚,解析也方便。
  • borrow_count 记录累计借出次数,为“热门图书排序”功能做准备。

users.txt 的格式类似:

#用户ID#姓名#密码#已借数量#待缴罚款#已借ISBN列表
20210001#张三#123456#2#0.0#978-7-111-54778-3,978-7-302-45678-9

已借ISBN列表用逗号分隔。这里要注意,设计这种“一行一个完整记录”的格式,就是为了让 fgets 读取每一行、再用 sscanf 解析时,逻辑简单、不容易出错。

3.2 加载txt:从文件到链表

加载的核心逻辑就是 逐行读取、按分隔符切割、malloc节点、尾插法挂到链表上 。尾插法意味着新节点总是追加到链表末尾,这样可以保持和文件里数据的顺序一致。

void load_books(Book *head, const char *filename) {
    FILE *fp = fopen(filename, "r");
    if (!fp) {
        printf("警告:书籍数据文件 %s 不存在,已创建空链表。\n", filename);
        return;
    }

    char line[1024];
    int load_count = 0;

    while (fgets(line, sizeof(line), fp)) {
        // 跳过空行和注释行
        if (strlen(line) < 2 || line[0] == '#') continue;

        Book *new_node = (Book*)malloc(sizeof(Book));
        if (!new_node) {
            printf("内存分配失败,加载中断!\n");
            fclose(fp);
            return;
        }
        memset(new_node, 0, sizeof(Book));
        new_node->next = NULL;

        char count_text[50];
        // 解析一行:从line中按#分隔提取字段
        // 注意:sscanf需要严格匹配格式
        int parsed = sscanf(line, "#%[^#]#%[^#]#%[^#]#%[^#]#%[^#]#%d",
            new_node->isbn, new_node->title, new_node->author,
            new_node->publisher, count_text, &new_node->borrow_count);

        if (parsed != 6) {
            printf("行解析失败,跳过: %s\n", line);
            free(new_node);
            continue;
        }

        // 解析"总数量-可借数量"
        int total, available;
        if (sscanf(count_text, "%d-%d", &total, &available) != 2) {
            free(new_node);
            continue;
        }
        new_node->total_count = total;
        new_node->available_count = available;

        // 尾插法挂到链表末尾
        Book *tail = head;
        while (tail->next) tail = tail->next;
        tail->next = new_node;
        head->total_count++;
        load_count++;
    }

    fclose(fp);
    printf("成功加载 %d 条图书记录。\n", load_count);
}

这里有几个细节要特别提一下:

  • %[^#] 是sscanf的字符集匹配格式,读取“直到遇到#为止”的所有字符,这是解析这种分隔符文件的利器。
  • memset(new_node, 0, sizeof(Book)) 把节点所有字节清零,防止字符串数组里有垃圾值,后面打印时出现乱码。
  • 解析失败时跳过这一行,而不是直接崩溃。这种容错虽然简单,但能让你在调试时少很多无头绪的崩溃。

3.3 保存txt:从链表到文件

保存逻辑是加载的逆过程,但有个坑要特别注意: 保存时必须用临时文件,保存成功后才替换原文件 。如果直接打开原文件写,写到一半程序崩溃,原文件的数据就全废了。用“先写临时文件,再重命名替换”的方式,就算崩溃,原文件还在,最多丢一次未保存的数据。

int save_books(Book *head, const char *filename) {
    char temp_file[256];
    snprintf(temp_file, sizeof(temp_file), "%s.tmp", filename);

    FILE *fp = fopen(temp_file, "w");
    if (!fp) {
        printf("保存失败:无法创建临时文件!\n");
        return 0;
    }

    fprintf(fp, "#ISBN#书名#作者#出版社#总数量-可借数量#借出次数\n");

    Book *p = head->next;
    for (; p != NULL; p = p->next) {
        fprintf(fp, "#%s#%s#%s#%s#%d-%d#%d\n",
            p->isbn, p->title, p->author, p->publisher,
            p->total_count, p->available_count, p->borrow_count);
    }

    fclose(fp);

    // 用临时文件替换原文件
    if (remove(filename) != 0 && access(filename, F_OK) == 0) {
        printf("原文件删除失败,请检查权限!\n");
        return 0;
    }
    if (rename(temp_file, filename) != 0) {
        printf("文件替换失败,数据未完全保存!\n");
        return 0;
    }

    printf("数据已保存到 %s。\n", filename);
    return 1;
}

3.4 系统退出时统一保存,操作时不要频繁写磁盘

别在每次借书后都调save_books,频繁写磁盘不仅慢,而且容易把硬盘写坏(虽然现代硬盘读写寿命都很长,但没这个必要)。正确做法是 每次增删改操作时修改内存中的链表,退出前统一保存一次 。如果你想保险,可以加一个“修改标志位”,只有发生过修改才在退出时保存。这算是真实项目管理里“脏标记”的简单版。

4. 核心功能逐个拆解:增删改查与借还书的完整链路

4.1 图书管理:插入、查找、删除、修改

插入 分了两种情况:新书入库(isbn不存在,直接尾插)和已有图书增加馆藏(isbn已存在,total_count和available_count加1)。插入前先遍历链表查一次isbn,避免重复录入。

// 新增图书:如果ISBN已存在则增加馆藏数量
int add_book(Book *head) {
    Book new_data;
    memset(&new_data, 0, sizeof(new_data));
    printf("请输入ISBN号:");
    scanf("%s", new_data.isbn);
    printf("请输入书名:");
    scanf("%s", new_data.title);
    printf("请输入作者:");
    scanf("%s", new_data.author);
    printf("请输入出版社:");
    scanf("%s", new_data.publisher);
    printf("请输入馆藏数量:");
    scanf("%d", &new_data.total_count);

    // 检查ISBN是否已存在
    for (Book *p = head->next; p != NULL; p = p->next) {
        if (strcmp(p->isbn, new_data.isbn) == 0) {
            p->total_count += new_data.total_count;
            p->available_count += new_data.total_count;
            printf("该图书已存在,馆藏数量增加 %d,当前总数 %d。\n",
                   new_data.total_count, p->total_count);
            return 1;
        }
    }

    // 新书,尾插
    Book *node = (Book*)malloc(sizeof(Book));
    if (!node) return 0;
    *node = new_data;
    node->available_count = new_data.total_count;
    node->borrow_count = 0;
    node->next = NULL;

    Book *tail = head;
    while (tail->next) tail = tail->next;
    tail->next = node;
    head->total_count++;
    printf("图书入库成功,当前图书总数 %d。\n", head->total_count);
    return 1;
}

这里我用了 *node = new_data; 把栈上结构体的值直接拷到堆节点里,简单高效,不用逐个字段赋值。但前提是结构体里没有指针字段,否则就是浅拷贝了。本项目中结构体都是字符串数组,所以没问题。

查找 支持按ISBN精确匹配、按书名模糊匹配( strstr ),还可以拓展为按作者、按出版社。这里的关键是不要把查找逻辑写在菜单函数里,而是抽成一个独立的函数。这样处理借书还书时也能复用。

// 按ISBN精确查找
Book* find_book_by_isbn(Book *head, const char *isbn) {
    for (Book *p = head->next; p != NULL; p = p->next) {
        if (strcmp(p->isbn, isbn) == 0) return p;
    }
    return NULL;
}

// 按书名模糊查找,返回找到了几本
int find_books_by_title(Book *head, const char *title) {
    int count = 0;
    for (Book *p = head->next; p != NULL; p = p->next) {
        if (strstr(p->title, title)) {
            printf("%s | %s | %s | 可借 %d/%d\n",
                   p->isbn, p->title, p->author,
                   p->available_count, p->total_count);
            count++;
        }
    }
    return count;
}

删除 的经典操作是“双指针遍历”,一个前置指针prev,一个当前指针cur。因为要断开链表,必须拿到被删除节点的前一个节点。这里如果你用了带头节点的链表,删除第一个有效节点时prev就是头节点,逻辑统一,不用特判。

int delete_book(Book *head, const char *isbn) {
    Book *prev = head;
    Book *cur = head->next;

    while (cur != NULL) {
        if (strcmp(cur->isbn, isbn) == 0) {
            prev->next = cur->next;
            free(cur);
            head->total_count--;
            printf("删除成功!当前图书总数 %d。\n", head->total_count);
            return 1;
        }
        prev = cur;
        cur = cur->next;
    }
    printf("未找到该ISBN对应的图书。\n");
    return 0;
}

删除前最好再确认一下:这本图书当前有没有被借出去?如果 available_count != total_count ,说明有用户手里还借着这本书,强行删除会导致用户借阅记录里的ISBN变成“幽灵书”。稳妥的处理是:提示“该图书尚有X本未归还,无法删除”,或者联动把所有借了该书的用户的借阅记录也清掉。推荐用前者,简单安全。

4.2 用户注册与登录:链表+文件落盘

用户管理的核心是注册、登录、展示个人信息(含借阅列表)。

  • 注册 :要求用户ID唯一,输入两次密码一致才创建用户节点,尾插到用户链表,更新users.txt。
  • 登录 :输入ID和密码,跟用户链表比对。密码建议不要明文存,但大作业阶段不强制。你可以做个简单的异或加密或哈希,提一下是加分项。

4.3 借书流程:一个完整的增删改组合操作

借书的业务逻辑看起来只有“把书借给用户”,实际上牵扯到四个数据结构的变化:

  1. 图书链表里对应节点: available_count - 1 , borrow_count + 1
  2. 用户链表里对应用户节点: borrowed_num + 1 ,在 borrowed_isbn 数组里写入ISBN
  3. 判断:读者是否已借满10本?书是否还有可借数量?ISBN是否存在?用户是否登录?
  4. 操作完成后,设置“数据已修改”标志,方便退出时统一保存。

借书前做判断的顺序很重要,错误提示要明确,不能笼统地报“借书失败”。我按顺序列一下校验步骤:

int borrow_book(Book *bhead, User *uhead, const char *uid, const char *isbn) {
    User *user = find_user_by_id(uhead, uid);
    if (!user) {
        printf("用户不存在,请先注册。\n");
        return 0;
    }
    Book *book = find_book_by_isbn(bhead, isbn);
    if (!book) {
        printf("图书不存在。\n");
        return 0;
    }
    if (book->available_count <= 0) {
        printf("该图书已全部借出,不可借。\n");
        return 0;
    }
    if (user->borrowed_num >= MAX_BORROW_COUNT) {
        printf("您已借满 %d 本,请先归还部分图书。\n", MAX_BORROW_COUNT);
        return 0;
    }
    // 检查是否已经借过这本书(防止重复借同一本)
    for (int i = 0; i < user->borrowed_num; i++) {
        if (strcmp(user->borrowed_isbn[i], isbn) == 0) {
            printf("您已借过这本书,不能重复借阅。\n");
            return 0;
        }
    }

    // 更新图书信息
    book->available_count--;
    book->borrow_count++;

    // 更新用户信息
    strcpy(user->borrowed_isbn[user->borrowed_num], isbn);
    user->borrowed_num++;

    printf("借书成功!%s 已借出,剩余可借 %d 本。\n", book->title, book->available_count);
    return 1;
}

这里我加了“不能重复借同一本”的校验,这个是你答辩时能说的亮点:说明你考虑到了用户和图书之间的唯一性约束。

4.4 还书流程:链路对称,别忘了罚款业务

还书跟借书是对称的:

  1. 判断用户是否借了这本书(遍历用户的borrowed_isbn数组)
  2. 图书节点: available_count + 1
  3. 用户节点:把该ISBN从数组里移除,数组后面的元素往前移
  4. 可选功能:如果还书日期晚于应还日期,计算罚款金额并累加到用户节点的 fine_due

罚款这块,如果做了就要设计好日期判断。简单方案是:借书时记录时间戳,还书时判断是否超过30天。但大作业阶段如果没做日期判断,直接用“读者是否自己报超期天数”的方式,也说得过去。我个人建议做到“计算超期天数”这一层,能体现你对真实业务的理解。

4.5 排行榜与统计:遍历链表的典型应用

“功能齐全”的加分项里,我强烈推荐加一个“热门图书排行榜”:按 borrow_count 降序排列,输出TOP10。这个功能的技术点在于排序。

排序有两种做法:

  • 直接对链表节点进行插入排序 :每次从未排序部分摘下最大节点,尾插到已排序链表中。代码稍长,但在考查链表操作上有说服力。
  • 借助辅助数组 :把链表节点指针提取到数组里,用 qsort 排序,再按序输出。代码短,易实现,但严格来说是“用数组思维解链表题”,答辩时容易被问住。

我给你展示一种基于“选择排序”思想的实现,直接在链表上比较但不交换节点,只交换节点里的数据字段。思路跟冒泡排序一样容易理解,但更好写:

void sort_books_by_borrow_count(Book *head) {
    for (Book *p = head->next; p != NULL; p = p->next) {
        Book *max = p;
        for (Book *q = p->next; q != NULL; q = q->next) {
            if (q->borrow_count > max->borrow_count) {
                max = q;
            }
        }
        if (max != p) {
            // 交换节点数据(不含next指针)
            Book temp;
            strcpy(temp.isbn, p->isbn);       strcpy(p->isbn, max->isbn);       strcpy(max->isbn, temp.isbn);
            strcpy(temp.title, p->title);     strcpy(p->title, max->title);     strcpy(max->title, temp.title);
            strcpy(temp.author, p->author);   strcpy(p->author, max->author);   strcpy(max->author, temp.author);
            strcpy(temp.publisher, p->publisher); strcpy(p->publisher, max->publisher); strcpy(max->publisher, temp.publisher);
            p->total_count ^= max->total_count; max->total_count ^= p->total_count; p->total_count ^= max->total_count;
            p->available_count ^= max->available_count; max->available_count ^= p->available_count; p->available_count ^= max->available_count;
            p->borrow_count ^= max->borrow_count; max->borrow_count ^= p->borrow_count; p->borrow_count ^= max->borrow_count;
        }
    }
}

等下,这里的异或交换整型字段虽然能用,但如果字段是浮点数或者是数组就不行。而且可读性差。在正式项目里不推荐,但作为C语言学习的技巧演示,可以提一嘴。更稳妥的交换方式是:写一个 swap_book_data 函数,把借出次数这个整型字段直接用一个临时变量交换即可。比如:

if (max != p) {
    char temp_str[100];
    strcpy(temp_str, p->title);   strcpy(p->title, max->title);   strcpy(max->title, temp_str);
    strcpy(temp_str, p->author);  strcpy(p->author, max->author); strcpy(max->author, temp_str);
    strcpy(temp_str, p->publisher); strcpy(p->publisher, max->publisher); strcpy(max->publisher, temp_str);
    strcpy(temp_str, p->isbn);    strcpy(p->isbn, max->isbn);     strcpy(max->isbn, temp_str);
    int temp = p->total_count;    p->total_count = max->total_count; max->total_count = temp;
    temp = p->available_count;    p->available_count = max->available_count; max->available_count = temp;
    temp = p->borrow_count;       p->borrow_count = max->borrow_count; max->borrow_count = temp;
}

这样写虽然代码长,但清晰、不容易错。注意不要在交换时动 next 指针,否则链表会断掉。

5. 文件方案踩过的坑:编码、路径、乱码、崩溃

文件这块儿有一堆只有写多了才会发现的坑,我挑几个典型的说。

5.1 中文乱码的根源不在于C代码,而在于文件编码

如果你用VS或Code::Blocks在Windows上跑,用 fprintf 写中文,再用记事本打开,默认可能是GBK编码;但如果你用VSCode写代码,源文件是UTF-8编码,编译器把字符串常量按UTF-8存进二进制,写进文件的就是UTF-8。这两者混在一起,就会出现“自己写的文件自己都读不回来”的乱码。

解决方案没有一个绝对标准,但核心思路是: 保证控制台输入、源代码里的字符串常量、txt文件三方的编码一致 。我的建议是,在Windows上用系统自带的记事本/GBK环境跑,就把源文件编码也设置成GBK;在Linux或macOS上跑,就用UTF-8。不要跨平台交叉打开文件。如果实在无法避免,就在打开文件时用 setlocale 设置本地化环境:

#include <locale.h>
setlocale(LC_ALL, "");

注意,这行代码要在 main 函数开头调用,能解决一部分控制台中文输出乱码的问题,但对txt文件编码本身无效。文件编码问题最本质的解法还是“统一编码”。

5.2 scanf输入后残留换行符,导致fgets读到空行

这是一个非常经典的问题。当你在菜单里用 scanf("%d", &choice) 选择功能后,回车产生的换行符会残留在输入缓冲区中。紧接着如果你用 fgets 读取书名,它会直接把这个残留的换行符读走,导致你的书名变成空字符串。

解决方案有几种:

  1. 用完 scanf 后,调用 while(getchar() != '\n'); 清空缓冲区。
  2. 用 scanf(" %d", &choice) ——在 %d 前加一个空格,跳过空白字符。
  3. 统一用 fgets 读字符串再 sscanf 解析。

我推荐方案1,因为它最直观、最好解释。但如果你的程序里大量需要读字符串,方案3才是治本的方案。写一个简单的 read_string 函数包一层,会舒服很多。

5.3 打开文件失败时的应对:别用exit(1)直接退出

很多同学的代码在 fopen 失败后直接 exit(1) 退出。但如果文件不存在是因为第一次运行(还没有生成txt文件),你这样退出就很蠢。更友好的方式是:文件不存在时提示“将创建空链表”,然后以空的链表继续运行,等退出时再创建并写入文件。只在你明确要求读文件、但文件里的格式完全无法解析时才提示“文件损坏”,这时再考虑是否退出。

6. 项目评估与扩展:从六级代码到更完整的系统

写到这里,这个系统已经具备了:图书管理(增删改查、排序)、用户管理(注册、登录、借阅记录)、文件持久化(读写txt、退出保存)三大能力。从课程设计的评分维度看,已经能达到良或优的水平。

如果再往上走,还有几个扩展方向值得尝试,你可以根据自己的时间选一两个:

  1. 日期模块 :借书、还书带时间戳,自动计算逾期天数与罚款。
  2. 管理员/普通用户分权 :管理员能新增和删除图书,普通用户只能查书和借还书。
  3. 数据校验增强 :ISBN的格式校验(比如判断长度和分隔符)、用户ID的纯数字校验、密码强度检查。
  4. 操作日志 :每次借书/还书/入库都写一行日志到log.txt,这个能当答辩亮点。
  5. 链表排序的稳定性优化 :把借阅排序做成真正的链表排序(归并排序等),而不是简单交换数据。

最后给你一个排版建议:交代码前,在.c文件顶部注释块里写清楚“程序名称、作者、日期、功能介绍、运行环境”,在关键函数前写上功能注释。老师看的第一眼通常就是这些,这决定了你代码的“第一印象”。

我个人做这类课设项目的习惯是:先写“能跑通基本流程”的版本,跑通后再回头看代码,把重复代码抽成函数,把边界判断补齐。这一步往往花不了太长时间,但代码质量完全是两个等级。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐