news 2026/10/13 7:33:42

C语言图书管理系统课设:SQLite数据库实现增删改查与借还书

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言图书管理系统课设:SQLite数据库实现增删改查与借还书

简介:这份资源是面向高校计算机相关专业学生的数据库课程设计完整方案,以C语言实现图书管理系统,适合正在准备期末大作业、课程设计或需要数据库与C语言综合实践的新手参考。压缩包共10个文件,约4KB,包含3个c源文件、3个h头文件、1个sql建库脚本、1个makefile构建文件、1个md说明文档及gitignore配置,覆盖主程序、功能模块、语法解析与数据库连接配置等核心部分,代码附有注释,便于理解整体架构与模块划分。目前已有381人学习下载。项目按图书管理业务组织目录,提供建库脚本与编译入口,下载后简单部署即可运行,可帮助读者快速掌握C语言与数据库结合的实现思路、表结构设计及功能函数拆分方式,适合作为课程设计参考或二次开发基础。

1. 从课程设计到能跑起来的图书管理系统:C 语言 + 数据库到底怎么落地

很多人做 C 语言课设,第一反应是写个控制台菜单,用数组存几本书,增删改查一跑,截图交差。问题是这种版本连自己都不想打开第二次——数据一关程序就没了,查询靠遍历,借阅记录和图书库存对不上。图书管理系统这个题目之所以年年被选,恰恰因为它天然带着「数据持久化 + 多表关联 + 事务一致性」的真实需求,而这些需求用纯数组是糊弄不过去的。

这篇笔记拆的是「基于 C 语言实现图书管理系统源码 + 数据库」这条路线:用 C 写业务逻辑和交互层,用关系型数据库(常见做法是 SQLite,零配置、单文件、适合课设)存图书、读者、借阅三张核心表,通过 C 的数据库 API 完成增删改查。适合正在做数据库课设、C 语言课设,或者想找一个能把「C 语言基础」和「数据库增删改查」串起来的练手项目的同学。读完你应该能自己搭出一套能编译、能持久化、能演示借还书流程的版本,而不是交完就删。

2. 选型先定死:为什么是 C + SQLite 而不是 C + MySQL

2.1 课设场景下数据库的三条硬指标

选数据库不是选最火的,是选最匹配交付场景的。课设的交付场景有三个硬指标:第一,助教或老师要在自己电脑上编译运行,不能要求对方先装一个数据库服务;第二,数据要能持久化,关掉程序再打开,之前录的书还在;第三,代码量要可控,你是在写课设不是写生产系统,接口越简单越好。

按这三条筛,MySQL 第一个出局——它需要独立安装服务、配置账号密码、启动后台进程,换台机器就可能连不上。常见做法是课设用 SQLite:整个数据库就是一个.db文件,跟你的源码放一起,拷走就能跑。它的 C 接口叫sqlite3,官方提供一个sqlite3.c和sqlite3.h,直接编进你的工程,连动态库都不用单独装。

提示:如果你的课设明确要求用 MySQL 或要求体现「客户端-服务端」架构,那另说;但纯 C 课设里,SQLite 是翻车率最低的选择。

2.2 三张表撑起整个系统

图书管理系统的数据模型不用设计得很复杂,三张表足够覆盖 90% 的课设要求:

表名作用关键字段
books图书库存id、isbn、title、author、total、available
readers读者信息id、name、student_no、borrowed_count
borrow_records借阅流水id、book_id、reader_id、borrow_date、return_date

books里的total是总库存,available是可借数量,这两个字段分开存是血泪经验——很多同学只存一个「是否借出」,结果同一本书有多本库存时就崩了。borrow_records的return_date为空表示还没还,这样查「当前在借」就是WHERE return_date IS NULL,比额外加状态字段干净。

2.3 建表 SQL 与初始化

数据库文件第一次运行时创建,建表语句直接写进 C 代码里执行:

-- schema.sql 逻辑,实际会以字符串形式嵌入 C 代码 CREATE TABLE IF NOT EXISTS books ( id INTEGER PRIMARY KEY AUTOINCREMENT, isbn TEXT NOT NULL UNIQUE, title TEXT NOT NULL, author TEXT, total INTEGER NOT NULL DEFAULT 1, available INTEGER NOT NULL DEFAULT 1 ); CREATE TABLE IF NOT EXISTS readers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, student_no TEXT NOT NULL UNIQUE, borrowed_count INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS borrow_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, reader_id INTEGER NOT NULL, borrow_date TEXT NOT NULL, return_date TEXT, FOREIGN KEY (book_id) REFERENCES books(id), FOREIGN KEY (reader_id) REFERENCES readers(id) );

逻辑说明:IF NOT EXISTS保证程序反复启动不会因为表已存在而报错,这是课设里最容易被忽略的一点——第一次跑成功,第二次跑就崩,八成是建表语句没加这个。AUTOINCREMENT让主键自增,省得你自己维护 ID。UNIQUE约束放在isbn和student_no上,从数据库层面挡住重复录入,比在 C 代码里写一堆判断可靠得多。

参数说明:total和available默认值设 1,适合单本录入;如果一次录入多本,插入时显式指定。borrow_date和return_date用 TEXT 存YYYY-MM-DD格式,SQLite 没有原生日期类型,用文本存反而方便比较和显示。

3. 把 C 和 SQLite 接起来:编译、连接、封装三步走

3.1 编译环境怎么配才不翻车

SQLite 的 C 接口有两种用法:一是系统装了libsqlite3-dev后直接-lsqlite3链接;二是把sqlite3.c和sqlite3.h下载下来放进工程一起编译。课设推荐第二种,因为不依赖目标机器的库版本,拷到哪都能编。

# 方式一:系统已安装开发库(Linux / macOS) gcc main.c db.c -lsqlite3 -o library # 方式二:把 sqlite3.c 放进工程一起编译(推荐,跨机器最稳) gcc main.c db.c sqlite3.c -o library

逻辑说明:方式二把 SQLite 源码当普通 C 文件编译,生成的可执行文件自带数据库引擎,换台机器只要有 gcc 就能跑。方式一依赖系统库,助教机器上没装libsqlite3-dev就链接失败。

参数说明:-lsqlite3必须放在源文件之后,gcc 的链接顺序有讲究,放前面会报 undefined reference。这是新手最常见的编译报错,记住「库放最后」。

3.2 打开数据库与错误处理封装

直接调sqlite3_open每次都写错误判断太啰嗦,封装一层:

#include <stdio.h> #include <sqlite3.h> static sqlite3 *g_db = NULL; /* 打开数据库,失败返回 0,成功返回 1 */ int db_open(const char *path) { int rc = sqlite3_open(path, &g_db); if (rc != SQLITE_OK) { fprintf(stderr, "打开数据库失败: %s\n", sqlite3_errmsg(g_db)); sqlite3_close(g_db); g_db = NULL; return 0; } return 1; } /* 执行无返回值的 SQL(建表、插入、更新、删除) */ int db_exec(const char *sql) { char *errmsg = NULL; int rc = sqlite3_exec(g_db, sql, NULL, NULL, &errmsg); if (rc != SQLITE_OK) { fprintf(stderr, "SQL 执行失败: %s\n", errmsg); sqlite3_free(errmsg); return 0; } return 1; } void db_close(void) { if (g_db) sqlite3_close(g_db); }

逻辑说明:g_db用全局变量持有连接句柄,课设是单线程程序,这样最省事。db_exec把sqlite3_exec包一层,统一打印错误信息——errmsg是 SQLite 分配的内存,必须用sqlite3_free释放,忘了释放就是内存泄漏,虽然课设跑不出问题,但这是习惯问题。

参数说明:sqlite3_open的 path 传"library.db"就在当前目录生成文件;传":memory:"则建内存库,程序退出数据全丢,调试时可以用,正式演示千万别用。

3.3 查询为什么要用回调而不是 exec

sqlite3_exec也能查数据,但它把结果以字符串数组形式丢给回调,类型全丢了,处理起来很别扭。查图书列表这种需要拿到每一列的场景,用sqlite3_prepare_v2+sqlite3_step+sqlite3_column_*更清晰:

/* 查询所有图书并打印 */ void list_books(void) { const char *sql = "SELECT id, isbn, title, author, available FROM books;"; sqlite3_stmt *stmt; if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) { fprintf(stderr, "准备语句失败: %s\n", sqlite3_errmsg(g_db)); return; } printf("%-4s %-16s %-24s %-12s %s\n", "ID", "ISBN", "书名", "作者", "可借"); while (sqlite3_step(stmt) == SQLITE_ROW) { int id = sqlite3_column_int(stmt, 0); const char *isbn = (const char *)sqlite3_column_text(stmt, 1); const char *title = (const char *)sqlite3_column_text(stmt, 2); const char *author= (const char *)sqlite3_column_text(stmt, 3); int avail = sqlite3_column_int(stmt, 4); printf("%-4d %-16s %-24s %-12s %d\n", id, isbn, title, author, avail); } sqlite3_finalize(stmt); }

逻辑说明:sqlite3_prepare_v2把 SQL 编译成语句对象,sqlite3_step每调一次前进一行,返回SQLITE_ROW表示有数据,返回SQLITE_DONE表示结束。sqlite3_column_int和sqlite3_column_text按列索引取值,索引从 0 开始,顺序和 SELECT 里的字段一致。

参数说明:sqlite3_column_text返回的指针由语句对象管理,sqlite3_finalize之后就失效,如果要长期保存必须自己strdup一份。sqlite3_finalize必须调用,否则语句对象泄漏,循环查询几百次就可能出问题。

4. 借书还书这两个动作,才是课设的评分点

4.1 借书:库存判断和事务不能省

借书不是简单插一条记录,它要同时做三件事:检查可借数量、扣减库存、写借阅流水。这三步必须一起成功或一起失败,否则会出现「记录写了但库存没扣」的脏数据。SQLite 支持事务,用BEGIN和COMMIT包起来:

/* 借书:成功返回 1,失败返回 0 并打印原因 */ int borrow_book(int book_id, int reader_id) { char sql[512]; sqlite3_stmt *stmt; db_exec("BEGIN;"); /* 1. 检查可借数量 */ snprintf(sql, sizeof(sql), "SELECT available FROM books WHERE id = %d;", book_id); sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); db_exec("ROLLBACK;"); printf("图书不存在\n"); return 0; } int avail = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); if (avail <= 0) { db_exec("ROLLBACK;"); printf("该书已全部借出\n"); return 0; } /* 2. 扣减库存 */ snprintf(sql, sizeof(sql), "UPDATE books SET available = available - 1 WHERE id = %d;", book_id); if (!db_exec(sql)) { db_exec("ROLLBACK;"); return 0; } /* 3. 写借阅记录,日期用 SQLite 的 date 函数 */ snprintf(sql, sizeof(sql), "INSERT INTO borrow_records(book_id, reader_id, borrow_date) " "VALUES(%d, %d, date('now'));", book_id, reader_id); if (!db_exec(sql)) { db_exec("ROLLBACK;"); return 0; } /* 4. 更新读者借阅计数 */ snprintf(sql, sizeof(sql), "UPDATE readers SET borrowed_count = borrowed_count + 1 WHERE id = %d;", reader_id); db_exec(sql); db_exec("COMMIT;"); printf("借书成功\n"); return 1; }

逻辑说明:任何一步失败都ROLLBACK,保证数据一致。date('now')是 SQLite 内置函数,直接生成当天日期,省得在 C 里调time再格式化。读者借阅计数更新失败没有回滚,因为它是辅助字段,不影响核心一致性——这是取舍,不是疏忽。

参数说明:snprintf的缓冲区给 512 字节,SQL 拼接不会超。这里用字符串拼接而不是参数绑定,是为了课设可读性;生产代码应该用sqlite3_bind_int防注入,但课设输入都是自己人敲的,拼接够用。如果你想让代码更规范,把%d换成?再用sqlite3_bind_int绑定即可。

4.2 还书:先查再改,别直接 UPDATE

还书要找到那条未归还的记录,把return_date填上,同时把库存加回去:

int return_book(int book_id, int reader_id) { char sql[512]; sqlite3_stmt *stmt; db_exec("BEGIN;"); /* 找未归还记录 */ snprintf(sql, sizeof(sql), "SELECT id FROM borrow_records " "WHERE book_id = %d AND reader_id = %d AND return_date IS NULL;", book_id, reader_id); sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL); if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); db_exec("ROLLBACK;"); printf("没有找到该读者的未归还记录\n"); return 0; } int record_id = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); /* 填归还日期 */ snprintf(sql, sizeof(sql), "UPDATE borrow_records SET return_date = date('now') WHERE id = %d;", record_id); if (!db_exec(sql)) { db_exec("ROLLBACK;"); return 0; } /* 库存加回 */ snprintf(sql, sizeof(sql), "UPDATE books SET available = available + 1 WHERE id = %d;", book_id); db_exec(sql); /* 读者计数减一,防止减到负数 */ snprintf(sql, sizeof(sql), "UPDATE readers SET borrowed_count = borrowed_count - 1 " "WHERE id = %d AND borrowed_count > 0;", reader_id); db_exec(sql); db_exec("COMMIT;"); printf("还书成功\n"); return 1; }

逻辑说明:先 SELECT 确认存在未归还记录,再 UPDATE,避免重复还书把库存加多。borrowed_count > 0这个条件防止计数变成负数,是踩过坑之后加的。

参数说明:return_date IS NULL是判断未归还的关键,不能用= '',空字符串和 NULL 在 SQL 里不是一回事,这是数据库课最经典的考点之一。

4.3 菜单和主循环怎么组织

主函数就是一个while循环加switch,把上面这些函数串起来:

int main(void) { if (!db_open("library.db")) return 1; db_exec("CREATE TABLE IF NOT EXISTS books(...);"); /* 建表语句略 */ db_exec("CREATE TABLE IF NOT EXISTS readers(...);"); db_exec("CREATE TABLE IF NOT EXISTS borrow_records(...);"); int choice; while (1) { printf("\n1.图书列表 2.借书 3.还书 4.退出\n请选择: "); if (scanf("%d", &choice) != 1) { while(getchar()!='\n'); continue; } switch (choice) { case 1: list_books(); break; case 2: { int b, r; printf("书ID 读者ID: "); scanf("%d %d", &b, &r); borrow_book(b, r); break; } case 3: { int b, r; printf("书ID 读者ID: "); scanf("%d %d", &b, &r); return_book(b, r); break; } case 4: db_close(); return 0; default: printf("无效选择\n"); } } }

逻辑说明:scanf返回值判断是防呆,用户输入字母时scanf失败,缓冲区里残留字符会导致死循环,while(getchar()!='\n')清掉这一行。建表语句放在db_open之后,保证每次启动表都存在。

参数说明:library.db是相对路径,程序在哪运行就在哪生成数据库文件。演示时建议把可执行文件和.db放同一目录,避免找不到文件。

5. 避坑与排查:课设里最容易翻车的五个点

5.1 编译报 undefined reference tosqlite3_open

现象:gcc 编译时提示一堆sqlite3_xxx未定义。原因:链接顺序不对,-lsqlite3放在了源文件前面,或者根本没加。解决:把-lsqlite3放到所有.c文件之后;如果用源码方式,确认sqlite3.c在编译命令里。

5.2 程序第二次运行就崩溃

现象:第一次跑正常,第二次启动直接报错退出。原因:建表语句没加IF NOT EXISTS,表已存在时sqlite3_exec返回错误,如果代码里对错误做了exit就崩了。解决:所有建表语句加IF NOT EXISTS,或者忽略「table already exists」这类错误。

5.3 借书后库存没变,但记录有了

现象:借阅记录插进去了,available还是原值。原因:事务里某一步失败但没回滚,或者 UPDATE 的 WHERE 条件写错。解决:检查每一步的返回值,失败立即ROLLBACK;确认WHERE id = ?的 id 传对了。用sqlite3 library.db命令行进去SELECT * FROM books;直接看数据最快。

5.4 中文书名显示乱码

现象:录入中文书名,列表里显示成问号或方块。原因:Windows 终端默认 GBK 编码,而源文件是 UTF-8,SQLite 存的是 UTF-8。解决:源文件保存为 UTF-8,Windows 下在main开头加system("chcp 65001");切换终端编码;Linux/macOS 一般没这问题。这是玄学问题,换台机器表现可能不一样,演示前一定在目标机器上试一遍。

5.5 忘记 finalize 导致查询越来越慢

现象:连续查询几十次后程序卡顿。原因:sqlite3_prepare_v2创建的语句对象没sqlite3_finalize,资源累积。解决:每个prepare配一个finalize,包括出错提前返回的分支。养成「prepare 和 finalize 成对出现」的习惯。

6. 让课设多拿几分:把查询做成可复用接口

前面list_books把查询和打印混在一起,演示够用,但想加「按作者查」「按 ISBN 查」就得复制粘贴。更好的做法是把查询结果装进结构体数组,打印和查询分离:

typedef struct { int id; char isbn[32]; char title[128]; char author[64]; int available; } Book; /* 按关键字查询,keyword 为空则查全部,返回实际条数 */ int search_books(const char *keyword, Book *out, int max) { const char *sql = (keyword && *keyword) ? "SELECT id,isbn,title,author,available FROM books " "WHERE title LIKE ?1 OR author LIKE ?1;" : "SELECT id,isbn,title,author,available FROM books;"; sqlite3_stmt *stmt; if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0; if (keyword && *keyword) { char pattern[130]; snprintf(pattern, sizeof(pattern), "%%%s%%", keyword); sqlite3_bind_text(stmt, 1, pattern, -1, SQLITE_TRANSIENT); } int n = 0; while (n < max && sqlite3_step(stmt) == SQLITE_ROW) { out[n].id = sqlite3_column_int(stmt, 0); snprintf(out[n].isbn, sizeof(out[n].isbn), "%s", (const char *)sqlite3_column_text(stmt, 1)); snprintf(out[n].title, sizeof(out[n].title), "%s", (const char *)sqlite3_column_text(stmt, 2)); snprintf(out[n].author, sizeof(out[n].author), "%s", (const char *)sqlite3_column_text(stmt, 3)); out[n].available = sqlite3_column_int(stmt, 4); n++; } sqlite3_finalize(stmt); return n; }

逻辑说明:?1是带编号的占位符,同一个参数在 SQL 里出现两次只需绑定一次,LIKE ?1两处共用。sqlite3_bind_text的SQLITE_TRANSIENT告诉 SQLite 复制一份字符串,因为pattern是栈上变量,函数返回就没了。snprintf拷贝列文本,避免finalize后指针失效。

参数说明:max是调用方给的数组容量,防止越界;pattern缓冲区比 keyword 大 2 字节放两边的%。这个接口返回条数,调用方拿到后自己决定怎么打印,加导出、加排序都不用改查询逻辑。

验证方法:录三本书,用作者名的一部分去搜,看能不能命中;再用不存在的关键字搜,应该返回 0 条而不是崩溃。这一步过了,说明参数绑定和边界处理都对。

我自己的习惯是,课设写完先不急着写报告,而是把数据库文件删掉,从零跑一遍完整流程:建表、录书、借、还、查、退出、再启动看数据还在不在。这一遍能过,基本就不会在演示现场翻车。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 7:32:12

in运算符详解:从Python容器到SQL性能优化的完整指南

这段时间整理工作笔记&#xff0c;翻到一份关于 in 运算符的零散记录&#xff0c;越看越觉得这个操作符被大多数人低估了。日常写代码时天天用到它&#xff0c;但真要说出它在不同容器、不同语言、不同场景下的行为差异和性能特征&#xff0c;能讲清楚的人并不多。尤其是线上接…

作者头像 李华
网站建设 2026/10/13 7:31:42

滑块验证原理与实战:从轨迹采集到风控判定

滑块验证这两年几乎成了网站的标配。我们平时刷网页、抢票、登录账号&#xff0c;都会遇到那个需要把拼图块拖到凹槽里的交互。明明只是拖一下、松一下手&#xff0c;但系统就能判断出“你是真人”还是“机器脚本”。很多人觉得这只是个交互小设计&#xff0c;但真正深挖之后你…

作者头像 李华
网站建设 2026/10/13 7:30:43

Docker部署Redis实战:从一条命令到生产级配置指南

说实话&#xff0c;我在自己的开发机上第一次正经跑起 Redis 时&#xff0c;不是用 Docker。那时候为了一个临时测试&#xff0c;我先得去官网找安装包&#xff0c;然后处理依赖、配置环境变量&#xff0c;再把它注册成后台服务&#xff0c;一套流程下来&#xff0c;光是排查启…

作者头像 李华
网站建设 2026/10/13 7:30:37

InterviewGuide 刷题笔记:LeetCode 80 题《删除排序数组中的重复项 II》——双指针原地去重,允许每个元素最多出现两次

教程 【免费下载链接】InterviewGuide &#x1f525;&#x1f525;「InterviewGuide」是阿秀从校园->职场多年计算机自学过程的记录以及学弟学妹们计算机校招&秋招经验总结文章的汇总&#xff0c;包括但不限于C/C 、Golang、JavaScript、Vue、操作系统、数据结构、计算机…

作者头像 李华
网站建设 2026/10/13 7:30:03

AutoIt脚本工具详解:实现Windows自动化与GUI测试

简介&#xff1a;这套 AutoIt v3.3.6.1 绿色汉化特别版是一份面向自动化脚本新手与运维人员的完整工具包。AutoIt 基于 BASIC 风格语法&#xff0c;能在 Windows 图形界面下模拟击键、鼠标移动和窗口/控件操作&#xff0c;适合批量配置 PC、完成重复性任务以及扩展其他语言的自…

作者头像 李华
网站建设 2026/10/13 7:29:59

基于ArcGIS与RUSLE的水土流失动态评估技术

1. 项目背景与核心价值水土流失是全球性的生态环境问题&#xff0c;直接影响农业生产、生态安全和可持续发展。传统的水土流失评估多依赖现场调查和统计方法&#xff0c;存在成本高、周期长、难以动态监测等局限。随着地理信息系统&#xff08;GIS&#xff09;和遥感技术的发展…

作者头像 李华