简介这份文档面向C语言与MySQL数据库入门学习者围绕一个可在控制台运行的简易图书管理系统展开帮助读者把基础语法、数据库连接和SQL增删改查串联成一个小型项目实践。压缩包内共1个doc文件约137KB以单一文档集中呈现系统结构、功能说明与关键代码示例便于本地对照复现。内容覆盖会员借阅、归还、查询图书以及管理员添加、修改、删除图书等模块并涉及mysql_query、mysql_error、结果集获取与释放等数据库交互流程示例还展示按编号借还、按全部或关键词检索、录入书名作者及更新借阅状态等操作也提示结果集使用后应及时释放。目前已有1574人学习适合作为课程设计、课后练习或入门练手项目的参考资料从中理解菜单循环、条件判断、字符串拼接SQL和基础错误处理。1. 从一份能跑起来的课设说起C 语言控制台配 MySQL 的图书管理系统带过几届课设最容易被选、也最容易在答辩现场翻车的题目就是 C 语言结合 MySQL 的图书管理系统。选它图的是控制台不用碰图形界面MySQL 又能把数据库课的知识点全串起来对初学者来说这是少数能把 C 语言、SQL、连接层 API 一次练完的题目。麻烦在于大部分人交上来的版本只有半条命菜单能跑借书能改状态可查状态那句 SQL 字段名拼错了缓冲区开 20 字节去接 200 字节的语句账号密码明文躺在源码里。这套方案要做的是把一个「能演示」的 Demo 补成能答辩、能验收的完整课设两张核心表的数据模型、两种角色的权限分流、增删改查四个动作的闭环以及一条照着就能搭起来的实现路径。适合手里有 C 语言和数据库基础、但不知道项目怎么收口的人。2. 数据模型与需求边界两张核心表撑起四个业务动作动手之前先把边界划清楚。控制台程序最怕的需求描述是「做一个图书管理系统」因为这句话可以无限扩张要不要预约、要不要罚款、要不要用户注册。最后交上来的东西功能列表写了两页能跑通的只有三个。我一般会先把需求压到「四个动作、两种角色、两张表」先把闭环跑通再在文档里写扩展方向答辩时反而显得思路清楚。2.1 角色与路径划分会员四条、管理员五条会员的操作范围是借书、还书、查书、退出。管理员的范围是加书、改书、删书、查书、退出。这两套动作的差别在于会员只改变书的状态字段管理员才动书本身的数据。这条界线必须写死在函数里而不是靠菜单文案暗示——很多人把「删除图书」也放到会员菜单里测试的时候没事一答辩被问「会员凭什么删书」就说不清楚。代码组织上两个函数各管一摊userfunction收会员的动作adminfunction收管理员的动作主函数只负责连接数据库、登录校验、按角色把人送进对应函数。这样做的好处是任何一次权限检查都只需要看一个入口不用在每个分支里重复判断「当前是不是管理员」。菜单的循环不要用递归也不要用全局标志位反复穿透。原文用goto ll回到菜单开头这种写法在小体量课设里其实是能接受的因为它短、直观、不出栈。真正的风险在后面第 5 章会讲到goto配合scanf残留很容易出现菜单疯狂刷屏的连锁反应。2.2 book 与 userinfo 的字段设计状态字段怎么定表结构是整个项目里最该先定下来的东西后面所有 SQL 都是围绕它写的。按原文的字段走两张表长这样-- 图书表bid 是主键bstate 只允许两个取值 create table if not exists book ( bid int not null primary key comment 图书编号, bname varchar(60) not null comment 书名, bauthor varchar(40) not null comment 作者, bstate varchar(10) not null default 未借出 comment 借阅状态 ) engineInnoDB default charsetgbk; -- 用户表uid 是整型账号upwd 是整型密码够课设用 create table if not exists userinfo ( uid int not null primary key comment 账号, upwd int not null comment 密码, uname varchar(20) comment 昵称 ) engineInnoDB default charsetgbk; -- 一条管理员账号账号固定为 1001 insert ignore into userinfo(uid, upwd, uname) values (1001, 1001, admin);几个字段值得单独说。bstate用varchar存「已借出 / 未借出」而不是tinyint原因是控制台打印的时候不用做映射查询结果直接就能看懂调试成本最低代价是拼写错一个汉字就查不出来所以建表时给默认值、代码里用常量字符串统一表示状态别在两处地方写不同的字。bid用int而不是自增是因为课设演示时希望编号可控借书直接输 1001 这类数字但这也意味着添加图书时编号冲突要自己处理第 4 章会给具体做法。upwd用int是这个项目里比较典型的偷懒做法好处是scanf(%d)直接读不用处理字符串代价是密码不能带前导零也没法做哈希。课设场景下可以用但文档里必须写一句「生产环境应改为哈希存储」这是答辩时最容易被追问的点之一。注意建表和连接必须用同一个字符集。建表用 gbk 而连接用 utf8表现出来就是查询结果里所有中文全变成问号而且只在某些机器上出现非常折腾。2.3 连接层落地从 mysql_init 到字符集设置连接数据库的代码只在main里出现一次但它决定了后面所有操作的成败。原文的写法是mysql_init(0)加mysql_real_connect然后mysql_query(pconn, set names gbk)。这个顺序没错但缺了三样东西连接失败之后的处理、字符集的可靠设置方式、以及连接超时。#include mysql.h #include stdio.h /* 打开数据库连接成功返回句柄失败返回 NULL */ MYSQL *db_open(void) { MYSQL *conn mysql_init(NULL); if (conn NULL) { fprintf(stderr, mysql_init 失败内存不足\n); return NULL; } /* 数据库服务刚启动时连不上是常事给 5 秒超时比默认值更可控 */ unsigned int timeout 5; mysql_options(conn, MYSQL_OPT_CONNECT_TIMEOUT, timeout); /* 参数依次为句柄、主机、账号、密码、库名、端口、socket、标志位 */ if (mysql_real_connect(conn, localhost, root, root, bookinfo, 3306, NULL, 0) NULL) { fprintf(stderr, 连接失败%s\n, mysql_error(conn)); mysql_close(conn); return NULL; } /* 用 API 设字符集比执行 set names 语句更早生效也不占一次查询 */ if (mysql_set_character_set(conn, gbk) ! 0) { fprintf(stderr, 字符集设置失败%s\n, mysql_error(conn)); } return conn; }这段代码的每个参数都有实际含义答辩时能讲清楚就是加分项。主机写localhost表示走本机端口写 3306 是默认端口写成 0 也等价第七个参数unix_socket在 Windows 下传NULL即可因为用不上最后一个client_flag传 0需要多语句执行时才改成CLIENT_MULTI_STATEMENTS。字符集这一步我一般会推荐用mysql_set_character_set而不是执行set names gbk。区别在于前者在客户端就把编码约定好了后者要额外发一次查询更重要的是如果连接本身失败了set names那条语句会给出一个和真实原因无关的错误把排查方向带偏。原文先打印「连接成功」再设编码这个顺序是合理的但连接失败时它也会打印「连接成功」这是第一个要修的地方。mysql_init(NULL)里的参数在旧版本里可以传已有的句柄现在的 API 基本都传 NULL。传 0 和传 NULL 效果一样但写成 NULL 更清楚。3. 主流程实现登录分流与借还书的状态闭环主流程是这个项目里唯一一条贯穿所有模块的线它的顺序是连接数据库、读账号密码、查用户表、按角色进函数、函数内部循环、退出时关连接。任何一环出错表现出来的现象都差不多——「登录没反应」或者「一直提示密码错误」。所以这一段代码要写得能让错误自己说话。3.1 登录校验一条查询决定走哪个函数登录的本质是一次带条件的查询账号和密码同时匹配就返回一行记录。原文用mysql_num_rows(res2) 0判断思路是对的但结果集释放漏了而且从row[1]里再取一次 uid 是多余的。/* 返回 1 表示登录成功0 表示账号密码不匹配-1 表示数据库出错 */ int login(MYSQL *conn, int uid, int upwd, int *is_admin) { char sql[256]; MYSQL_RES *res; int rows; snprintf(sql, sizeof(sql), select uid from userinfo where uid %d and upwd %d, uid, upwd); if (mysql_query(conn, sql) ! 0) { fprintf(stderr, 登录查询失败%s\n, mysql_error(conn)); return -1; } res mysql_store_result(conn); if (res NULL) { /* 取结果集失败也要报出来 */ fprintf(stderr, 取结果集失败%s\n, mysql_error(conn)); return -1; } rows (int)mysql_num_rows(res); mysql_free_result(res); /* 不管成功失败必须释放 */ if (rows 0) { return 0; } *is_admin (uid 1001); /* 管理员用固定账号识别 */ return 1; }这里有两个设计取舍值得写进文档。第一判断管理员身份用的是用户输入的账号而不是从结果集里取出来的字段因为结果集只select了uid一列取row[1]会越界读到 NULL原文那段atoi(row[1])就是踩了这个坑只是刚好结果没被用到测试时看不出来。第二查询只select uid而不select *减少列数能降低取错列的概率控制台程序里这种「最小查询」习惯养成之后后面所有查询都会稳很多。密码比对放在 SQL 里做而不是查出来在 C 里比原因是能省一次字符串比较也避免把用户表整行读进内存。代价是不能做哈希校验这个在第 4 章末尾还会提。提示登录失败后问一句「是否重新登录」读入Y或y再回去这个交互在原文里已经有了保留它答辩演示时比起直接退出体面很多。3.2 借书与还书先查后改两次访问数据库借书是这个项目里唯一一个「需要根据当前状态决定要不要执行」的动作也是最容易写错的地方。原文的思路是先把状态查出来用strcmp判断再更新但代码里select bstaet字段名拼错了mysql_store_result又放在了mysql_query前面结果查出来的永远是空判断逻辑形同虚设。正确的顺序是两次独立的数据库访问第一次查状态第二次带条件更新。/* 返回值0 成功-1 书不存在-2 已被借出-3 数据库错误 */ int borrow_book(MYSQL *conn, int bid) { char sql[256]; MYSQL_RES *res; MYSQL_ROW row; /* 第一步查当前状态用 bstate 而不是拼错的字段名 */ snprintf(sql, sizeof(sql), select bstate from book where bid %d, bid); if (mysql_query(conn, sql) ! 0) { fprintf(stderr, 查询状态失败%s\n, mysql_error(conn)); return -3; } res mysql_store_result(conn); if (res NULL) { fprintf(stderr, 取结果集失败%s\n, mysql_error(conn)); return -3; } row mysql_fetch_row(res); if (row NULL) { /* 一行都没有说明编号不存在 */ mysql_free_result(res); return -1; } if (strcmp(row[0], 未借出) ! 0) { /* 原代码这里判断的是已借出 */ mysql_free_result(res); return -2; } mysql_free_result(res); /* 第二步把状态条件写进 where防止两次操作之间被别人改掉 */ snprintf(sql, sizeof(sql), update book set bstate 已借出 where bid %d and bstate 未借出, bid); if (mysql_query(conn, sql) ! 0) { fprintf(stderr, 更新失败%s\n, mysql_error(conn)); return -3; } if (mysql_affected_rows(conn) 0) { /* 影响 0 行说明状态已经变了 */ return -2; } return 0; }关键点在于第二条 SQL 的where里带了bstate 未借出。控制台单人操作其实碰不到并发但这个写法能顺手解决另一个问题如果第一步查完状态、第二步更新前程序出了任何岔子更新语句本身也不会把已借出的书再写一遍。mysql_affected_rows的返回值就是用来兜这个底的返回 0 表示没有行被改动。还书的实现更简单一条update加mysql_affected_rows判断就够了。但要注意还书不需要检查当前状态——重复还书虽然没意义但把它当成错误处理反而会让演示难做直接更新成「未借出」并提示成功是课设里更稳妥的选择。原文用sprintf拼 SQL缓冲区声明成 200 字节。这个长度对单条 update 够用但加上书名和作者就悬了第 4 章会统一换成snprintf。3.3 查询模块全量列表与模糊搜索共用一个打印函数查询被会员和管理员各写了一遍代码几乎一样这类重复在课设里很常见也是最容易在维护时出问题的地方——改了一个忘了另一个。合理的做法是抽一个「执行并打印」的函数两边都调它。/* 执行已经发出去的查询并把结果按列对齐打印出来 */ void print_result(MYSQL *conn) { MYSQL_RES *res mysql_store_result(conn); MYSQL_ROW row; unsigned int i, n; if (res NULL) { if (mysql_field_count(conn) 0) { /* 不是查询语句 */ printf(影响行数%llu\n, mysql_affected_rows(conn)); return; } printf(取结果集失败%s\n, mysql_error(conn)); return; } n mysql_num_fields(res); MYSQL_FIELD *fields mysql_fetch_fields(res); for (i 0; i n; i) { printf(%-14s, fields[i].name); /* 先打表头 */ } printf(\n); while ((row mysql_fetch_row(res)) ! NULL) { unsigned long *len mysql_fetch_lengths(res); for (i 0; i n; i) { /* row[i] 可能是 NULL长度要用 mysql_fetch_lengths 取 */ printf(%-14.*s, (int)len[i], row[i] ? row[i] : NULL); } printf(\n); } mysql_free_result(res); }全量查询就是select * from book加一次print_result。模糊搜索的关键在于通配符和转义char kw[64]; printf(请输入书名关键字); scanf(%63s, kw); char esc[128]; /* 先把用户输入里的单引号转义再加百分号做模糊匹配 */ mysql_real_escape_string(conn, esc, kw, (unsigned long)strlen(kw)); char sql[512]; snprintf(sql, sizeof(sql), select * from book where bname like %%%s%%, esc); if (mysql_query(conn, sql) ! 0) { printf(查询失败%s\n, mysql_error(conn)); } else { print_result(conn); }mysql_real_escape_string的作用是把输入里的、\这类字符转义成 SQL 里的安全形式。原文直接把temp拼进like语句理论上输一个单引号就能把语句截断虽然课设没人会这么测但这是「SQL 注入」这个词在答辩里唯一的落点写上就是加分。注意mysql_use_result和mysql_store_result不能混用。前者是逐行读读完之前不能发新查询后者一次性把结果拉到客户端。控制台程序数据量小统一用mysql_store_result更省心。4. 管理员端的增删改SQL 拼装、主键冲突与输入长度管理员这几个功能表面上是三个独立的菜单项实际上是同一套模式读输入、拼 SQL、执行、看返回值、给提示。模式本身不难难的是那些「正常操作碰不到、一换输入就炸」的边界。4.1 添加图书主键冲突与字段长度兜底添加图书最容易翻车的地方不是 SQL 写错而是编号重复。bid是主键插一个已存在的编号mysql_query会返回非 0mysql_error会给你一句「Duplicate entry」。如果代码只是笼统打印「添加失败」演示的时候还得去翻数据库才知道为什么体验很差。正确做法是用mysql_errno区分错误类型int add_book(MYSQL *conn, int bid, const char *name, const char *author) { char name_esc[128], author_esc[128]; char sql[512]; /* 输入先转义书名里带单引号也不会把语句打断 */ mysql_real_escape_string(conn, name_esc, name, (unsigned long)strlen(name)); mysql_real_escape_string(conn, author_esc, author, (unsigned long)strlen(author)); snprintf(sql, sizeof(sql), insert into book(bid, bname, bauthor, bstate) values(%d, %s, %s, 未借出), bid, name_esc, author_esc); if (mysql_query(conn, sql) ! 0) { if (mysql_errno(conn) 1062) { /* 1062 主键重复 */ printf(添加失败编号 %d 已存在\n, bid); } else { printf(添加失败%s\n, mysql_error(conn)); } return -1; } return 0; }1062 这个错误码值得单独记一下MySQL 里所有主键或唯一键冲突都是它。除了这个还应该顺手处理 1406字段太长。原文里book_name[20]配scanf(%s, book_name)两个问题叠在一起一是数组名取地址在某些编译器下会给出警告二是超过 19 个字符的输入直接把栈写穿。char book_name[64]; printf(请输入书名不超过 60 个字符); /* scanf 加宽度限制最多读 63 个字符 结尾 0不会溢出 */ if (scanf(%63s, book_name) ! 1) { printf(输入无效\n); return -1; }宽度限制%63s里的 63 是「最多读多少字符」不是缓冲区大小所以数组开 64 正好。这个数字和varchar(60)对齐超长输入在 C 层就被截断不会走到数据库报 1406。提示scanf读字符串遇到空格就停。如果演示时书名带空格要么改成fgets再手动去掉换行要么就在文档里声明「书名不支持空格」。前者更稳妥。4.2 修改图书按旧编号定位、按新编号落库修改功能的需求描述通常是「改一本书的信息」但这里藏着一个歧义能不能改编号原文的做法是允许改update book set bid 新编号 ... where bid 旧编号。这个操作在数据库层面是合法的但会带来两个后果一是新编号如果已经存在直接主键冲突二是原来借这本书的人手里的编号就失效了。课设里最简单的处理是允许改编号但把冲突判断清楚同时提示风险。int update_book(MYSQL *conn, int old_bid, int new_bid, const char *name, const char *author) { char name_esc[128], author_esc[128], sql[640]; mysql_real_escape_string(conn, name_esc, name, (unsigned long)strlen(name)); mysql_real_escape_string(conn, author_esc, author, (unsigned long)strlen(author)); snprintf(sql, sizeof(sql), update book set bid %d, bname %s, bauthor %s where bid %d, new_bid, name_esc, author_esc, old_bid); if (mysql_query(conn, sql) ! 0) { if (mysql_errno(conn) 1062) { printf(修改失败新编号 %d 已被占用\n, new_bid); } else { printf(修改失败%s\n, mysql_error(conn)); } return -1; } if (mysql_affected_rows(conn) 0) { printf(修改失败编号 %d 不存在\n, old_bid); return -1; } return 0; }注意mysql_affected_rows返回 0 有两种可能编号不存在或者新值和旧值完全一样。这两种情况提示语不同但处理方式都是「不当作成功」代码里统一提示「不存在」是为了简化文档里写清楚即可。如果更保守可以干脆不允许改编号只改书名和作者把编号从修改表单里去掉。我一般倾向于这个方案因为借阅状态是挂在编号上的改编号等于换了一本书。4.3 改用预处理语句把拼接 SQL 的口子堵上前面用mysql_real_escape_string已经能挡住大部分注入问题但它对「整数」是无能为力的——数值输进去就是数值没有转义的概念。真正干净的做法是预处理语句参数和语句分离数据库自己处理类型。int add_book_prepared(MYSQL *conn, int bid, const char *name, const char *author) { const char *sql insert into book(bid, bname, bauthor, bstate) values(?, ?, ?, 未借出); MYSQL_STMT *stmt mysql_stmt_init(conn); MYSQL_BIND bind[3]; if (stmt NULL) { return -1; } if (mysql_stmt_prepare(stmt, sql, (unsigned long)strlen(sql)) ! 0) { fprintf(stderr, 预处理失败%s\n, mysql_stmt_error(stmt)); mysql_stmt_close(stmt); return -1; } memset(bind, 0, sizeof(bind)); bind[0].buffer_type MYSQL_TYPE_LONG; /* bid 是 int */ bind[0].buffer bid; unsigned long name_len (unsigned long)strlen(name); bind[1].buffer_type MYSQL_TYPE_STRING; /* bname 是字符串 */ bind[1].buffer (void *)name; bind[1].buffer_length name_len; bind[1].length name_len; unsigned long author_len (unsigned long)strlen(author); bind[2].buffer_type MYSQL_TYPE_STRING; bind[2].buffer (void *)author; bind[2].buffer_length author_len; bind[2].length author_len; if (mysql_stmt_bind_param(stmt, bind) ! 0 || mysql_stmt_execute(stmt) ! 0) { fprintf(stderr, 执行失败%s\n, mysql_stmt_error(stmt)); mysql_stmt_close(stmt); return -1; } mysql_stmt_close(stmt); return 0; }这段代码比拼接版长不少答辩时不必全讲讲清楚三件事就行?是占位符MYSQL_BIND里的buffer_type决定这个参数按什么类型传length指针用于变长字符串。课设里全用预处理没必要挑「添加」和「登录」这两个入口用上其余保留拼接版文档里说明取舍反而是个有说服力的设计点。5. 常见问题与避坑五个最容易在验收时被问住的点调试这个项目的血泪经验是报错信息往往指向的地方和真正的问题不在同一处。下面这几条按「现象 → 原因 → 解决」整理基本覆盖了这个题目 80% 的翻车场景。5.1 编码类中文乱码与 set names gbk 的坑现象程序里打印出来的书名全是问号或者命令行窗口里中文变成了方块换个同学电脑跑又正常了。原因MySQL 表的字符集、连接的字符集、Windows 控制台的代码页这三层任何一层不一致都会乱码。原文用set names gbk如果建表时用的是utf8这条语句反而是把连接改成了 gbk结果就是写进去乱、读出来更乱。解决三层统一。建表语句显式写default charsetgbk连接用mysql_set_character_set(conn, gbk)控制台在程序开头执行一次system(chcp 936)切到简体中文代码页。如果要做成跨平台的就把这三处统一换成utf8mb4Windows 控制台那边用chcp 65001。关键是「统一」不是「哪个更好」。/* 放在 main 开头让控制台代码页和数据库字符集对齐 */ system(chcp 936 nul);现象设置完编码代码里写死的汉字还是乱码。原因源文件本身的保存编码和控制台代码页不一致。VS 默认用 GBK 存源文件如果代码页切到了 65001字符串常量就会乱。解决源文件另存为带 BOM 的 UTF-8同时代码页用 65001或者反过来都留在 GBK。别一半一半。5.2 结果集类没释放、没判断、把状态查成了空现象借书功能不管书是不是已借出永远提示「借阅成功」。原因原文先mysql_store_result(pconn)再判断但前面那条查状态的mysql_query根本没执行或者执行了但字段名写成了bstaet导致res是空的strcmp拿一个未初始化或者空指针去比结果不可预期。解决把「发查询 → 取结果 → 判断行数 → 取值 → 释放」当成一个固定动作每一步都判断返回值。字段名靠 SQL 报错发现不要靠肉眼。写代码时养成习惯每次mysql_query之后立刻跟一个if把mysql_error(conn)打出来这句错误信息能省掉大量猜测。现象程序跑一段时间后内存占用越来越高或者第二次查询直接失败。原因mysql_store_result拿到的MYSQL_RES *没有mysql_free_result。原文在添加、修改、删除的分支里都没有释放只有查询分支释放了。解决把结果集变量声明在函数内部每次使用后立刻释放别跨分支保存。如果逻辑复杂就抽一个「查询并打印」的函数让释放动作只出现在一个地方——第 3.3 节的print_result就是这个思路。注意mysql_store_result返回 NULL 有两种情况一种是出错一种本身就是非查询语句比如 insert。判断方式是看mysql_field_count(conn)是不是 0不看mysql_error有没有内容。5.3 交互类scanf 残留、goto 跳转和明文密码现象菜单疯狂刷屏输入一个数字之后程序自己往下跑了好几屏。原因scanf(%d, x)读整数时如果输入的是字母这个字符不会被读走留在缓冲区里下一次scanf立刻又读到它形成死循环。原文用goto回到菜单开头正好把这个循环放大成了刷屏。解决所有scanf都判断返回值读失败就清空缓冲区。int x; if (scanf(%d, x) ! 1) { int ch; while ((ch getchar()) ! \n ch ! EOF) { } /* 丢弃整行 */ printf(输入无效请重新选择\n); continue; }现象答辩时被问「密码是怎么存的」答不上来。原因upwd是intSQL 里明文比对源码里还能直接看到1001/1001这个管理员账号。解决课设阶段不必真上哈希但要在文档和答辩里主动说明当前用整型明文是为了降低课设复杂度正式系统应改为varchar存加盐哈希登录时在服务端计算后比对。主动说出这句话比被追问出来要好得多。另外管理员账号判断尽量走用户表里的角色字段而不是在代码里硬编码uid 1001硬编码是答辩里第二个高频追问点。现象goto跳转之后某些变量的值不符合预期。原因goto可以往前跳但跳过去的代码如果依赖跳转前初始化的局部变量行为就不确定。原文case 4里有个goto ll后面跟着printf那行代码永远不会执行。解决goto只用于「回到菜单开头」这一种场景跳转标签放在菜单循环的第一行跳转前不做任何需要保留的局部状态。真要写复杂流程改成while (1)加switch加break可读性立刻上一个台阶。6. 验收与答辩一份可以照着走的检查单验收这一步很多人把时间花在「把功能演示一遍」结果被问三个问题就卡住。我一般建议反过来先准备好一份检查单自己按顺序走一遍走不通的地方就是答辩当天的雷。第一轮查连通性。删掉数据库里的bookinfo库从建库建表脚本开始重建再启动程序。这一步能暴露所有「依赖我电脑上某个配置」的问题。检查单上的条目是连接失败有没有明确提示、字符集设没设、管理员账号在不在、空表能不能正常查询。第二轮查状态闭环。准备三条数据一本未借出的书、一本已借出的书、一个不存在的编号。分别走借书、还书、重复借、重复还、查不存在的书。这五个动作里最容易出问题的是「已借出的书再借一次」和「不存在的编号还书」前者考验状态判断后者考验mysql_affected_rows有没有用上。第三轮查输入边界。书名输 60 个字符、输带空格的、输带单引号的编号输 0 和负数菜单输字母。这几项不需要全部处理得完美但要做到「不崩溃、有提示」。带单引号那条尤其重要它是 SQL 注入在答辩里唯一的实证。验收项通过标准常见失败原因建库建表在干净环境执行脚本无报错脚本里漏了字符集声明登录分流1001 进管理员其他账号进会员角色判断写死在菜单里借书已借出的书提示失败状态不变查状态的 SQL 字段名写错还书不存在的编号有提示没判断mysql_affected_rows查询中文正常显示结果集正确释放编码不统一、漏mysql_free_result添加编号重复有明确提示只打印了mysql_error删除删完再查确实没了拼接 SQL 时编号写成了字符串答辩环节我通常会让对方准备一张纸画三样东西表结构、函数调用关系、一条借书的完整数据流。能画出来说明真的理解自己写了什么画不出来代码跑得再顺也只是背下来的。被问到「为什么用goto」时标准答案是「课设体量小用goto减少嵌套层级正式项目会改成循环加状态机」不用硬撑说它好。被问到「安全性」时主动说清楚密码明文是简化预处理语句在哪几处用了比含糊过去强。最后一条是我自己的习惯答辩前一天把程序在一个没装过 MySQL 客户端的虚拟环境里跑一遍。听起来多余但我见过太多次「在我电脑上好好的」问题全出在 DLL 路径和字符集上。从那以后我每次交课程设计都强制走一遍「干净环境 检查单」宁可多花两小时也不要在讲台上对着一个黑匣子解释为什么它今天不听话。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?