简介:这是一份结合C语言与MySQL数据库的简单图书管理系统文档,专为初学C语言和数据库编程的读者设计,适合作为课程设计或项目入门练习。文档完整展示了控制台图书管理系统的核心代码,涵盖借阅、归还、查询图书等主要功能模块,并重点讲解了mysql_query、mysql_use_result、mysql_fetch_row等MySQL C API的使用方式,以及mysql_error错误处理技巧,帮助读者理解C语言与数据库进行交互的完整流程。资源包内含1个doc文档,仅137KB,体积小巧,便于随时查阅。目前已有1574人学习,内容实用、结构清晰,读者可以对照代码了解图书管理系统的基本架构与循环菜单设计,同时学习在Windows控制台环境中配置MySQL开发环境、编写SQL语句及处理结果集的方法,是巩固C语言基础、上手数据库编程的不错参考。
1. 初学者的第一套 C 语言 + MySQL 项目:这个图书管理系统值不值得下
对绝大多数 C 语言初学者来说,第一个能真正「存数据、查数据、改数据」的控制台程序,多半就是图书管理系统。这套用 C 语言配合 MySQL 数据库实现的简单图书管理系统,正好卡在「刚学完指针和结构体,还没碰过真实数据库」的节点上——代码不长,却把 mysql.h 连接、SQL 拼接、结果集读取、角色分流这些以后做任何带存储的程序都会遇到的事全走了一遍。适合正在找课程设计参考的新手,也适合想搞懂 MySQL C API 怎么调用的读者。源码里还有几个让人哭笑不得的 bug,但恰恰是这些 bug,让它的教学价值比十份封装完美的框架代码都高。
2. 先把环境立住:mysql.h 链接配置与 book 表建表
对这套代码来说,最大的门槛不是 C 语法,而是「让编译器找到 MySQL 的头文件和库」。很多初学者把代码抄进编辑器一编译,满屏 error C2065 和 LNK2019,第一反应是代码写错了,其实九成是链接库没配好。
2.1 链接库配置:winsock.h 和 libmysql.lib 缺一不可
代码开头四行 include 顺序是有讲究的:
#include <stdio.h> #include <string.h> #include <winsock.h> #include <mysql.h>winsock.h 是最容易被忽略的一行。有人看到 winsock 就以为是网络功能,直接删掉,结果 mysql.h 一编译就报一堆类型找不到的错误——MySQL 的 Windows 客户端库底层依赖 Winsock 的 socket 类型定义,删了它等于拆了地基。代码里先 include winsock.h 再 include mysql.h 也是对的,如果调换顺序,可能触发头文件重复定义的问题。
真正卡住大部分人的是链接阶段。mysql_init、mysql_query 这些函数声明在头文件里,实现却在 libmysql.lib 里,而编译器默认不会去搜这个库。我一般按下面三步配置(Visual Studio 系列都适用):
- 项目属性 → C/C++ → 常规 → 附加包含目录,填 MySQL 安装目录下的 include 文件夹;
- 项目属性 → 链接器 → 常规 → 附加库目录,填 MySQL 安装目录下的 lib 文件夹;
- 项目属性 → 链接器 → 输入 → 附加依赖项,填 libmysql.lib。
配置完再编译,unresolved external symbol mysql_init 这类 LNK2019 错误才会消失。如果还报错,八成是位数不匹配——装了 64 位 MySQL 却把 64 位 libmysql.lib 配给 32 位工程,或者反过来。查这类链接错误,先确认工程平台和库位数一致,再看路径里有没有中文。
提示:这段代码虽然引入了 winsock.h,但并没有真正用到网络通信,它只是为了满足 mysql.h 在 Windows 平台上的编译依赖。连接的还是本机 localhost,不涉及任何远程数据库场景。
2.2 建表语句:book 表与 userinfo 表字段对齐
代码里用到的库名是 bookinfo,两张表:book 和 userinfo。按代码里引用的字段反推,建表 SQL 我一般写:
create database bookinfo default charset gbk; use bookinfo; create table book ( bid int primary key, bname varchar(50) not null, bauthor varchar(50) default '佚名', bstate varchar(20) default '未借出' ); create table userinfo ( uid int primary key, upwd varchar(20) not null ); insert into userinfo values (1001, 123456);两个关键点。第一,default charset gbk 必须和代码里的 set names gbk 对齐。如果建库用了 utf8mb4,而代码里 set names gbk,则中文写入时会按 gbk 解释字节流,查出来大概率乱码。字符集问题贯穿整个系统,也是这个项目最大的一个坑,后面避坑章单独讲。第二,book 表的 bstate 有默认值 '未借出',所以管理端添加图书时可以少写一列;但如果建表时漏掉 default,insert 就必须显式带上 '未借出',代码里正是显式写的,两种写法都能跑,语义不同。
userinfo 表里管理员账号是 1001。登录 SQL 是where uid='%d' and upwd='%d',账号密码都以数字形式参与比较,所以 upwd 定义成 varchar 也能正常存。对课设来说够用,但真实项目不会把密码存成明文数字,这是后话。
2.3 连接参数:mysql_real_connect 的七个参数怎么理解
main 函数里这四行是整套系统的心脏:
MYSQL *pconn = mysql_init(0); if (!mysql_real_connect(pconn, "localhost", "root", "root", "bookinfo", 0, 0, 0)) { printf("连接失败,错误为:%s\n", mysql_error(pconn)); } printf("连接成功!\n");mysql_init(0) 创建连接句柄,参数 0 表示让库自己分配内存;mysql_real_connect 的七个参数按位置分别是:
| 参数位置 | 本例取值 | 含义 |
|---|---|---|
| host | localhost | MySQL 服务端主机,本机就用 localhost |
| user | root | 登录账号 |
| passwd | root | 登录密码 |
| db | bookinfo | 默认数据库,连接后免写 use |
| port | 0 | 端口,0 表示默认 3306 |
| unix_socket | 0 | 本机套接字路径,Windows 直接传 0 |
| clientflag | 0 | 客户端标志位,默认 0 |
这段代码最大的隐患是:连接失败后没有退出。mysql_real_connect 返回空指针时,pconn 依然指向一个未连接成功的句柄,程序继续往下执行 mysql_query,会得到 "MySQL server has gone away" 之类的错误,让人误以为是 SQL 写错,排查半天才发现是连接根本没建立。稳妥写法是失败后立即 return 或 exit(1),别让程序带着一个半死的连接往下跑。main 开头还有一个int n = 100;,从头到尾没用到,属于典型的课设残留代码,留着不碍事,但值得意识到它没用。
3. 登录与权限分流:mysql_store_result 与账号角色判断
3.1 登录校验:先执行查询,再数行数
登录片段是理解 mysql_store_result 的最佳案例:
jj: printf("请输入账户和密码\n"); scanf("%d", &account); scanf("%d", &pwd); char sql_login[200]; sprintf(sql_login, "select * from userinfo where uid='%d' and upwd='%d'", account, pwd); if (mysql_query(pconn, sql_login)) { printf("登录失败"); } MYSQL_RES *res2 = mysql_store_result(pconn); int num = mysql_num_rows(res2); if (num > 0) { printf("登录成功!\n"); ... } else { ... }mysql_query 负责把 SQL 发给服务端,成功后结果集还挂在连接上;mysql_store_result 把整个结果集一次性缓冲到内存,之后 mysql_num_rows 才能数出匹配行数。行数大于 0 说明账号密码对上了。这个「先 query 再 store 再 num_rows」的套路是 MySQL C API 最常用的登录校验方式,三步缺一不可。
有两个新手常踩的点。第一,mysql_query 返回非零说明 SQL 本身有错(比如表名写错),此时结果集是空的,继续 mysql_num_rows(res2) 会访问到空指针。正确做法是先判断 mysql_query 的返回值,成功后再 store_result。第二,登录 SQL 用 sprintf 直接拼接用户输入,存在 SQL 注入风险——比如密码位置输入1 or 1=1这类内容,拼接后的 SQL 可能绕过校验。课设代码到处都是这种拼法,理解它没问题,但要知道真实项目里必须用预处理语句(mysql_stmt_prepare 系列)来杜绝。
源码里还留了一大段被注释掉的早期登录逻辑,用的是 mysql_store_result 加 mysql_num_fields 判断,写法更绕。对比下来,现在这版用 mysql_num_rows 直接数行数,思路清晰得多。看别人代码时,保留的注释块往往是理解作者演进思路的最好素材,别急着删。
3.2 角色分流:账号硬编码的边界在哪里
登录成功后,代码用一行判断决定调用哪个菜单:
if (account == 1001) { adminfunction(pconn); } else { userfunction(pconn); }这是最简单的角色控制:用账号本身区分管理员和会员,不建角色表,不读权限字段。对课程设计和小练习够用,但边界很清楚——想加第二个管理员就得改代码,想给某类会员开放添加图书权限也得改代码。真实项目里一般把角色做成 userinfo 的一个字段,登录时一并取出来:
// 假设 userinfo 增加 role 字段,1 为管理员,0 为会员 char sql_role[200]; sprintf(sql_role, "select uid, role from userinfo where uid='%d' and upwd='%d'", account, pwd); if (mysql_query(pconn, sql_role)) { printf("查询失败:%s\n", mysql_error(pconn)); return; } MYSQL_RES *res_role = mysql_store_result(pconn); MYSQL_ROW row_role = mysql_fetch_row(res_role); if (row_role && atoi(row_role[1]) == 1) { adminfunction(pconn); } else { userfunction(pconn); } mysql_free_result(res_role);角色字段的方案比账号硬编码多写几行,但以后加管理员只需要插一条记录。这就是课设代码往上走的第一步重构方向。
3.3 set names gbk:字符集对齐的第一道关口
if (mysql_query(pconn, "set names gbk")) { printf("编码有错误为:%s\n", mysql_error(pconn)); }set names gbk 一句顶三句,它同时设置了三个会话级变量:character_set_client、character_set_connection、character_set_results。作用是告诉服务端「我这个连接上收发的字符串都按 gbk 解释」。它只影响当前连接,不改变数据库本身的字符集。
所以正确用法讲究两端对齐:建库建表用 gbk,连接后 set names gbk,控制台代码页切到 GBK,三处一致才不会有乱码。最常见的翻车组合是:数据库默认 utf8mb4 + 代码 set names gbk + 控制台默认代码页,三处互相矛盾,查出来的中文要么是问号,要么是「绁炵」这类错位字符。判断乱码属于哪一环,最快的方法是在 MySQL 命令行里手动执行 insert 一条中文数据、再 select 出来看:命令行正常而程序乱码,问题在 set names 或控制台代码页;命令行本身就乱码,问题在建库字符集。建议新手直接全链路 gbk,省心;想上 utf8mb4 的话,建库、set names、控制台代码页三处全改,不能只改一处。
4. 核心功能拆解:借阅、归还、查询与管理的 SQL 构造
4.1 借阅流程:update 语句与前置状态检查
借书的核心是一条 update:
int bid1; printf("请输入图书编号:"); scanf("%d", &bid1); char jieyue[200]; sprintf(jieyue, "update book set bstate='已借出' where bid = %d", bid1); if (mysql_query(pconn, jieyue)) { printf("借阅失败,错误为:%s\n", mysql_error(pconn)); } else { printf("借阅成功\n"); }sprintf 把用户输入的编号拼进 SQL,单引号包住中文状态值,mysql_query 返回 0 表示成功,非零则用 mysql_error 取详情。这条语句本身没问题,问题出在它前面的状态检查。原代码这样写:
char state[200]; sprintf(state, "select bstaet from book where bid = %d", bid1); MYSQL_RES *res; res = mysql_store_result(pconn); if (strcmp(state, "已借出")) { printf("\n 此书已借出!\n"); goto ll; }三个问题叠一起。第一,state 里装的是拼出来的 SQL 文本,不是执行结果,strcmp(state, "已借出") 永远不相等,if 永远为真,所以任何书都借不出来。第二,列名 bstaet 拼错,就算执行这条 select 也会报错。第三,在已有结果集没释放的情况下又调 store_result,内存越积越多。正确的状态检查应该先执行查询、再取结果、再判断:
char state_sql[200]; sprintf(state_sql, "select bstate from book where bid = %d", bid1); if (mysql_query(pconn, state_sql)) { printf("查询失败:%s\n", mysql_error(pconn)); goto ll; } MYSQL_RES *res_state = mysql_store_result(pconn); MYSQL_ROW row_state = mysql_fetch_row(res_state); if (row_state && strcmp(row_state[0], "已借出") == 0) { printf("此书已借出!\n"); mysql_free_result(res_state); goto ll; } mysql_free_result(res_state);这段非常适合初学者复盘:strcmp 返回值是 0 才表示两个字符串相等,原代码里if (strcmp(...))是「不相等才为真」,逻辑完全反了。我帮 A 同学调这段的时候,他盯着变量名想了半天——state 里存的到底是 SQL 文本还是查询结果,这是这类代码最容易混淆的点,没有之一。
4.2 查询功能:mysql_use_result 逐行打印与 like 转义
查询全部图书:
char query[20]; sprintf(query, "select * from book"); if (mysql_query(pconn, query)) { printf("Error making query:%s\n", mysql_error(pconn)); } else { MYSQL_RES *res; MYSQL_ROW row; res = mysql_use_result(pconn); if (res) { while ((row = mysql_fetch_row(res)) != NULL) { for (int t = 0; t < mysql_num_fields(res); t++) printf("%8s ", row[t]); printf("\n"); } } mysql_free_result(res); }mysql_use_result 是流式读取,结果不一次进内存,而是每次 mysql_fetch_row 从服务端拉一行。内存占用小,但结果集没读完之前不能在这个连接上发起下一次查询,否则报 Commands out of sync。这里的 while 循环把结果读干净后才释放,是标准写法。mysql_num_fields 返回列数,配合 printf("%8s") 做列对齐,在控制台里显示效果还算整齐。
两种读取结果集的方式对比一下更清楚:
| 方式 | 读取策略 | 内存占用 | 适用场景 |
|---|---|---|---|
| mysql_store_result | 一次性缓冲全部结果 | 大 | 结果集小、需要马上知道总行数 |
| mysql_use_result | 逐行流式读取 | 小 | 结果集大、边读边处理 |
注意:对课设这种结果集只有几十行的场景,无脑用 store_result 反而更省事,可以少踩 Commands out of sync 的坑。
模糊搜索那段用了 like:
char temp[20]; printf("请输入搜索图书信息:"); scanf("%s", temp); sprintf(query1, "select * from book where bname like '%%%s%%'", temp);注意 '%%%s%%' 里两个 %% 是 sprintf 的转义,最终生成的 SQL 是like '%关键字%'。如果直接写 '%s%',printf 家族会把 % 后面的字符当格式符解析,轻则输出错乱,重则越界。这是 C 语言拼 SQL 的核心知识点,自己写的时候最容易在这翻车。
4.3 管理端:insert、update、delete 三条语句的拼法
添加图书:
int id; char book_name[20], author[20]; printf("请输入编号:"); scanf("%d", &id); printf("请输入书名:"); scanf("%s", book_name); printf("请输入作者:"); scanf("%s", author); char tianjia[200]; sprintf(tianjia, "insert into book values (%d,'%s','%s','未借出')", id, book_name, author); if (mysql_query(pconn, tianjia)) { printf("添加失败,错误为:%s\n", mysql_error(pconn)); } else { printf("添加成功\n"); }原代码里写的是scanf("%s", &book_name),book_name 本身是 char[20] 数组,取地址后类型变成 char()[20],和 %s 期望的 char不一致。好在数组名能隐式转成首元素地址,scanf 写入的地址实际还是对的,所以能跑,但编译器会警告。建议直接写scanf("%s", book_name),少一层取地址,也少一个警告。
修改图书的逻辑是先输入待修改编号 temp1,再输入一组新值,SQL 写成update book set bid=%d, bname='%s', bauthor='%s' where bid=%d,注意 set 后面是 id1、where 后面是 temp1,顺序不能混。删除图书更简单,一条delete from book where bid = %d加一个 mysql_query 就完事。
管理端菜单有个隐蔽问题:提示文案写「5-继续操作 6-退出系统」,但 switch 里 case 5 是 return(退出),case 6 才是 goto kk(继续),第 5、6 两项的行为和文案完全对调。拿到这种别人的代码,先跑一遍摸清真实行为再动手改,别信打印出来的菜单。
5. 避坑排查:这套代码最容易翻车的五个现场
5.1 借阅永远提示「此书已借出」
现象:不管输入哪本书的编号,系统都提示已借出,借阅操作没有一次能成功。
原因:状态检查把拼好的 SQL 字符串存进 state 变量,strcmp 拿 SQL 文本和「已借出」比较永远不相等,if (strcmp(...)) 永远为真;另外列名 bstaet 拼错,真执行也会报错。
解决:先 mysql_query 执行 select bstate,再 mysql_store_result 取结果集,再用 mysql_fetch_row 拿到真实状态值,最后用 strcmp(row_state[0], "已借出") == 0 判断是否已借出。判断这个 bug 有个小技巧:在 MySQL 命令行里手动执行select bstate from book where bid=1,如果库里状态明明是「未借出」而程序还提示已借出,问题一定出在程序判断逻辑,不用怀疑数据。
5.2 中文乱码
现象:打印出来的书名、作者变成问号或「绁炵」这类错位字符,而且不报错。
原因:建库字符集、set names、控制台代码页三者不一致。代码固定了 set names gbk,如果建库时没写 default charset gbk,走了服务端默认字符集,写入的字节流就错位了。字符集错位是静默的,mysql_query 不会返回任何错误码,这是它难排查的主要原因。
解决:建库显式default charset gbk,并在 main 开头加system("chcp 936")把控制台代码页切到 GBK;如果坚持 utf8mb4,就得把建库、set names utf8mb4、控制台 chcp 65001 三处一起改,改任何单处都没用。最笨也最有效的排查办法:insert 一条数据立刻 select 出来,在命令行看是否乱码,逐步缩小范围。
5.3 like 查询搜不到结果
现象:搜索图书时永远返回空列表,偶尔伴随格式串相关的编译警告或异常输出。
原因:C 字符串里 % 是 sprintf 的格式符,直接写like '%s%',sprintf 会把第二个 % 当格式符开头去解析,生成错误的 SQL。
解决:写成'%%%s%%',两个 %% 转义成一个字面 %,最终 SQL 才是like '%关键字%'。如果编译开了 /W3 以上警告级别,这种错误通常会在 like 所在行给出 format 相关警告,看到警告先检查 %% 数量对不对。
5.4 连续操作报 Commands out of sync
现象:第一次查询正常,紧接着第二次查询时报错,错误信息是 "Commands out of sync; you can't run this command now"。
原因:mysql_use_result 的流式结果集没读完全部行就发了下一次查询,或者结果集用完没调用 mysql_free_result,连接上还挂着一个未完成的结果。
解决:使用 use_result 时保证 while 循环读完全部行,并显式 mysql_free_result;对课设这种结果集很小的场景,直接用 mysql_store_result 一次进内存,后续任意查询都不受影响。这个错误码是 2014,记住这个数字,以后在别的大项目里看到就能秒定位。
5.5 菜单文案与行为对调
现象:管理端选 5 想继续操作,结果程序直接退出;选 6 想退出,结果又跳回菜单。
原因:printf 的文案写「5-继续操作 6-退出系统」,switch 里 case 5 却执行 return,case 6 执行 goto kk,行为与文案完全对调。
解决:改文案或改 case,二选一。建议按业务语义改代码,把 case 5 定义为继续、case 6 定义为退出,避免下次看代码又被绕进去。这也是 goto 与 switch 混用的典型副作用——标签跳转削弱了 case 结构的可读性。
6. 进阶:把 goto 换成 while,让菜单和输入都变得可控
这套代码最影响阅读体验的是 goto。userfunction 和 adminfunction 里到处是 goto ll / goto kk,case 分支跳来跳去,稍不留神就掉进死循环,改动一处就要顺着标签把流程重新捋一遍。我的建议是拿它当一次重构练习,用 while(1) + switch 重写菜单骨架:
void userfunction(MYSQL *pconn) { int x; while (1) { printf("\n 请选择功能:(1-借阅图书 2-归还图书 3-查询图书 4-退出系统)"); scanf("%d", &x); getchar(); // 吃掉回车,避免影响下一次输入 switch (x) { case 1: borrow_book(pconn); break; case 2: return_book(pconn); break; case 3: query_book(pconn); break; case 4: printf("退出成功!\n"); return; default: printf("无效选择,请重试\n"); break; } } }每个 case 只做一件事,调用独立的业务函数,break 后回到 while 等待下一次输入,退出直接 return,流程一眼就能看懂。getchar() 把 scanf 留在缓冲区的回车清掉,解决「菜单输完数字,下一个字符串输入直接吃到空串」的问题。default 分支拦截非法输入,这是原代码完全没有的。
重构完可以照着下面这份清单做一轮验证:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 管理员 1001 登录,添加一本测试书 | 提示添加成功 |
| 2 | 会员账号登录,借阅测试书 | 提示借阅成功 |
| 3 | 再次借阅同一本书 | 提示此书已借出 |
| 4 | 归还测试书 | 提示归还成功 |
| 5 | 搜索书名关键词 | 返回包含关键词的记录 |
| 6 | 查询全部图书 | 表格式打印所有记录 |
每步执行后都去 MySQL 里确认一下 book 表的实际数据,bstate 字段有没有正确翻转,这是验证整套系统逻辑是否闭环最直接的办法——只盯着控制台输出不看库表,很容易被表象骗过去。
从那以后,我每次拿到一份 C 语言课设代码,都会先全局搜一遍 goto 和 sprintf,把 SQL 拼接、结果集释放、状态判断三个点单独过一遍再动手改,这个习惯帮我躲掉了不少坑。这类小系统翻车最密集的地方,翻来覆去也就是这三处。希望帮到你。
本文还有配套的精品资源,点击获取