ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++图书管理系统源代码拆解:面向对象、文件持久化与STL改造

C++图书管理系统源代码拆解:面向对象、文件持久化与STL改造 简介这份 C 图书管理系统设计源代码文档面向计算机专业课程设计、C 面向对象编程练习者及需要完成图书管理类大作业的学生围绕借书、归书、书籍管理、读者管理与检索等典型业务提供可参考的源码组织思路。内容涵盖按图书编号查询现存量并登记借阅者学号、还书时校验读者信息以及新增、删除、修改图书和读者的确认流程搜索模块细分书名、书号、作者、出版社、出版时间及读者姓名、学号等入口便于对照实现。文档同时给出 book 类与 reader 类的私有成员定义和公共成员函数声明如书名、编号、库存、借阅状态、可借天数及各类增删改查接口有助于理解封装与数据抽象方式。压缩包内共 1 个 doc 文件约 158KB体积轻便适合按章节阅读并迁移到自己的工程中。目前已有 4357 人学习下载可作为课程设计、期末项目与上机实验阶段的参考资料。1. 一个 100 行就够用的 C 图书管理系统为什么值得逐行拆开看很多人第一次看到「C 图书管理系统设计源代码」这类资源第一反应是「课程设计作业」扫一眼就关掉。但真正把它跑起来、改一遍你会发现这是一份很典型的 C 面向对象入门模板两个类book和reader一个getch()驱动的字符菜单一个裸FILE*二进制读写文件的持久化层加上借书、还书、增删改查这些标准动作。它不精致但五脏俱全正好用来理解「类设计 → 文件存储 → 交互菜单」这条最朴素的链路。这份资源适合两类人一是要交课程设计、需要一份能读懂能改的 C 代码的人二是想从零散语法过渡到「写一个完整小程序」的人。它的价值不在于功能多而在于每一处都能对应到具体的语法点和坑点比如成员数组为什么设成char name[50]store和total为什么要分开维护落盘时为什么用fwrite而不是文本流。后面几章就把这些点一个个摊开讲。2. book 与 reader 两个类的成员设计与封装逻辑2.1 为什么书和读者要拆成两个类图书管理系统的核心矛盾是「书的数据」和「人的数据」两套独立的实体。借书这个动作同时涉及两者书要减库存、记录借阅人读者要记录借了哪本书、借书日期、可借天数。如果把所有字段塞进一个结构体字段会膨胀到二十多个逻辑会迅速失控。拆成book和reader之后每个类只关心自己的状态耦合点集中在借书和还书两个函数里。这份源码里book的私有成员是书名、编号、作者、借书人、借书人学号、现存量、总存量、出版时间、价格、出版社reader的私有成员是姓名、学号、年级、班级、性别、所借书名、借书日期、可借天数。可以看到设计者的思路是「谁的信息就放在谁的类里」借阅关系在两边各存一份。2.2 char 数组与 int 学号带来的实际影响源码里姓名、书名、作者都用char[]学号用int。这个选择有一连串后果值得逐条看字段类型设计后果name[50]书名char 数组输入含空格的书名会被cin 截断只能读到第一个空格前writer[10]作者char 数组只有 10 字节稍长的作者名直接缓冲区溢出num学号int只能纯数字无法录入学号里的字母或前导零borr_num借书人学号int用 0 表示「无人借阅」但借书人也用borrower0表示空闲date[10]借书日期char 数组2024/03/15刚好 10 字符没有余量存\0作者的窄数组是个真实隐患。常见做法是把char writer[10]放宽到char writer[30]或者直接用std::string代码会短一大截还不用担心越界。不过这份源码用 C 风格字符数组也有教学意图能看清strcpy到底干了什么。// 采用 C 风格数组时的字段定义示例缓冲区留足余量 class book { private: char name[50]; // 书名中文 UTF-8 下 50 字节约 16 个汉字 char writer[30]; // 作者从 10 放宽到 30避免写溢出 char borrower[20]; // 借书人姓名 int id; // 书号从 1001 起 int borr_num; // 借书人学号0 表示该书未借出 int store; // 现存量可借数量 int total; // 总存量馆藏总数 int pub_time; // 出版时间此设计只存年份 float price; // 价格 char publish[50]; // 出版社 public: book(); int book_add(); // 增加书籍 int book_del(); // 注销书籍 int book_mod(); // 修改书籍 int book_out(); // 借书 int book_in(); // 还书 // ... 其余搜索函数省略 };参数说明store和total必须分开维护total是馆藏总数增删书时变store是可借数借还书时变。判断能否借书只看store ! 0判断这本书存不存在只看id。三者的含义不能混否则会出现「书删了但借阅记录还在」的脏数据。2.3 getter/setter 一组函数的存在意义reader类里定义了一整套getname / setname / getnum / setnum。字段已经是私有的外部不能直接访问所以要通过这组函数读写。set系列返回int而不是void源码里统一return 0是个习惯性写法表示成功。这组函数的实际价值在于如果哪天要把int num换成char num[15]只需要改 getter/setter 内部实现调用方不用动。3. 二进制文件落盘与借还书的 store/total 联动3.1 用 fwrite 整块写数组的存储模型这份源码最特别的地方是持久化方式定义book books[SIZE]SIZE 为 100和reader stud[AMOUNT]AMOUNT 为 50两个全局数组所有增删改都在内存数组上操作到函数末尾再用fwrite把整个数组覆盖写回文件books。它没有用文本文件逐行拼接而是把定长结构体数组按二进制整块落盘。// 增书后整体落盘把 100 个 book 对象一次性写回文件 FILE *bfp; bfp fopen(books, wb); // wb二进制写会清空原文件 if (bfp NULL) { cout 无法打开文件. endl; return 0; } for (int n 0; n SIZE; n) { fwrite(books[n], sizeof(books[n]), 1, bfp); // 每个对象 1 次写入 } fclose(bfp); // 不 fclose 数据可能还在缓冲区逻辑说明fopen用wb打开会截断原文件所以每次保存都是「全量覆盖」而非追加。fwrite(ptr, size, count, fp)中size是单个元素字节数count是元素个数这里每次写 1 个book循环写够 100 个。这种方式的代价是数组必须定长且结构体里不能放指针或std::string否则存下去的是地址而不是内容。注意fwrite写的是结构体的内存布局包含char数组里的未初始化垃圾字节。只要同一个编译器重新编译读写能对上换编译器或改了字段顺序旧文件就读成乱码。这就是它只适合课程设计、不适合生产的原因。3.2 借书函数的 store 判断与状态写入借书的核心逻辑在book_out()按书号查书看现存量再让用户输学号。把它整理成清晰的版本如下。int book::book_out() { FILE *bfp; int i, in_id; cout 请输入书号: ; cin in_id; // 用户输入要借的书号 bfp fopen(books, rb); // 先读全部书籍到内存 for (i 0; i SIZE; i) { fread(books[i], sizeof(books[i]), 1, bfp); } fclose(bfp); for (i 0; i SIZE; i) { if (books[i].id in_id) { // 命中目标书籍 if (books[i].store 0) { // 现存量不足 cout 该书已全部借出. endl; return 0; } cout 请输入借阅者学号: ; cin books[i].borr_num; // 记录借阅人学号 cin books[i].borrower; // 记录借阅人姓名 books[i].store--; // 现存量减一total 不变 cout 该书已被借出. endl; break; } } // 覆盖写回文件与 3.1 落盘代码相同 bfp fopen(books, wb); for (i 0; i SIZE; i) fwrite(books[i], sizeof(books[i]), 1, bfp); fclose(bfp); return 0; }参数说明与坑点store--只改现存量total保持不动这样还书时store就能回到原值。borrower用0表示空但字符数组不能直接赋0源码里写char borrower[10]却用cin 输入若无人借阅实际是空字符串。真正稳的写法是借出时置borrower为学号对应的姓名归还时用memset(books[i].borrower, 0, 10)清空。归还函数book_in()的逻辑是反过来的先按学号找读者再输书号找到书后store并清空借阅人字段。3.3 增删改中的数组腾挪与死循环风险删书函数book_del()用的是向前覆盖法删除// 找到目标下标 i 后后续元素整体前移一格 if (s 1) { // s 为用户确认标志1 为删除 for (; i SIZE; i) { books[i] books[i 1]; // 前移覆盖末尾元素重复 } }这段代码边界有瑕疵循环到i SIZE - 1时会读books[SIZE]越界一格。正确写法是i SIZE - 1。另外「末尾元素重复」意味着被删书的记录会在数组末尾残留一份下次新增时若按id 0找空位可能找不到位置。更常见的做法是给book加一个int valid标记删除时置 0所有查询都跳过无效项避免数组腾挪。增书函数里有个典型的查重死循环do { cout 请输入一个新的书号: ; cin books[n].id; for (i 0; i SIZE; i) { if (books[n].id books[i].id n ! i) { cout 这个书号已经存在. endl; break; // 只跳出 for没跳出 do-while } } } while (books[n].id books[i].id); // 用 i 的值判断逻辑不稳问题在于break只跳出内层for而while条件用的是循环结束后的i判定对象不是「有没有重号」容易出现要么多问一次要么误判。稳妥的写法是引入一个bool dup标志bool dup; do { dup false; cin books[n].id; for (i 0; i SIZE; i) { if (books[n].id books[i].id n ! i) { dup true; cout 这个书号已经存在. endl; break; } } } while (dup); // 有重号就重输干净利落注意输入id前最好用cin.clear()处理非法输入否则用户误输字母会让cin进入错误状态后面所有读取都失效菜单看起来像死掉一样。4. 五路搜索、菜单驱动与常见编译运行排错4.1 五个搜索函数为什么只差一个比较字段书籍搜索支持书名、书号、作者、出版社、出版时间五种方式源码里对应book_name、book_id、book_pub、book_time、book_writer五个函数。它们结构几乎一样只是比较的字段不同。用strcmp做字符串比较时注意它返回 0 才表示相等。int book::book_name() { char target[50]; cout 请输入书名: ; cin target; // 书名带空格会被截断见下文 for (int i 0; i SIZE; i) { // strcmp 返回 0 表示完全相等 if (books[i].id ! 0 strcmp(books[i].name, target) 0) { cout 书名: books[i].name endl; cout 书号: books[i].id endl; cout 现存量: books[i].store endl; return 0; } } cout 未找到该书籍. endl; // 找不到直接回落主界面 return 0; }参数说明books[i].id ! 0这个前置判断是在跳过无效槽位。因为数组定长且未初始化的元素id默认为 0不加这个条件会把空槽位也拿去比较。字符串输入用cin 是这份源码的老问题书名含空格只能读到第一段。要支持带空格的书名把cin target换成cin.getline(target, 50)并注意先处理上一行残留的换行符。4.2 getch 菜单与 switch 分发主菜单book::book()构造函数里用goto menu做循环用switch(getch())分发。getch()来自conio.h按一个键立刻返回不需要回车这是它比cin更适合菜单的地方但也是它跨平台差的原因Linux 下没有conio.h。switch (getch()) { // 单字符输入无需回车 case 1: book_out(); break; // 借书 case 2: book_in(); break; // 还书 case 3: book1(); break; // 书籍管理 case 4: read.read1(); break; // 读者管理 case 5: seach(); break; // 搜索 case 0: exit(0); break; // 退出 default: system(cls); // 清屏后重画菜单 cout 错误请再输入一遍! endl; } goto menu; // 回到菜单重新选择逻辑说明每个case里调用对应成员函数函数执行完自然回到goto menu。system(cls)和system(color 17)是 Windows 专有命令前者清屏后者设背景色。跨平台项目通常不会这么写会用#ifdef _WIN32包起来Linux 下换成system(clear)。4.3 编译与环境常见报错现象原因处理方式找不到conio.h在非 Windows 环境编译改用getchar()或引入 ncurses 替代getchgets/strcpy警告使用了不安全函数换fgets/strncpy或在文件顶部#define _CRT_SECURE_NO_WARNINGS数据读出来是乱码结构体布局变了还读旧文件删掉books文件重新录入运行后菜单刷屏getch读到回车等特殊键确认包含conio.h且用字符比较case 1而非数字case 1提示缺少 MSVC 运行库使用了 Visual C 编译安装对应版本的 Microsoft Visual C Redistributable编译命令上用 g 时把源文件存成.cpp直接g main.cpp -o libraryWindows 下如果依赖conio.h建议用 MinGW 或直接进 Visual Studio 建控制台项目。源码文件名是.doc实际是文本需要先把代码部分复制到.cpp文件里再编译别指望 Word 文档能直接跑。5. 把定长数组换成 vector 与 string 的改造路径这份源码最值得动手改造的一步是去掉定长数组和裸文件指针。book books[SIZE]限制了最多 100 本书char name[50]限制了字段长度fwrite限制了可移植性这三者互相绑定。换成 STL 容器和文本流代码量不增反减。#include vector #include string #include fstream #include sstream struct Book { std::string name; // 不再担心缓冲区长度 std::string writer; std::string borrower; int id 0; // 0 表示空槽与源码语义一致 int borrNum 0; int store 0; int total 0; int pubTime 0; float price 0; std::string publish; }; // 用逗号分隔的文本行存储肉眼可读跨编译器无歧义 void saveBooks(const std::vectorBook v, const std::string path) { std::ofstream out(path); // 文本写覆盖模式 for (const auto b : v) { if (b.id 0) continue; // 跳过无效槽位 out b.id , b.name , b.writer , b.price , b.total , b.store , b.publish , b.pubTime , b.borrower , b.borrNum \n; } } std::vectorBook loadBooks(const std::string path) { std::vectorBook v; std::ifstream in(path); std::string line; while (std::getline(in, line)) { // 逐行读天然支持不定长 if (line.empty()) continue; Book b; std::stringstream ss(line); std::string tmp; std::getline(ss, tmp, ,); b.id std::stoi(tmp); std::getline(ss, b.name, ,); std::getline(ss, b.writer, ,); std::getline(ss, tmp, ,); b.price std::stof(tmp); std::getline(ss, tmp, ,); b.total std::stoi(tmp); std::getline(ss, tmp, ,); b.store std::stoi(tmp); std::getline(ss, b.publish, ,); std::getline(ss, tmp, ,); b.pubTime std::stoi(tmp); std::getline(ss, b.borrower, ,); std::getline(ss, tmp, ,); b.borrNum std::stoi(tmp); v.push_back(b); } return v; }逻辑说明文本存储把「内存布局」这件事解耦了字段顺序、编译器、位宽都不再影响读取。std::stoi/std::stof做类型转换遇到脏数据会抛异常所以生产代码要把它们包在try里。改用vector后删除一本书就是v.erase(v.begin() i)不用手写前移循环也不会越界。书名含空格的问题用std::getline(std::cin, b.name)一并解决。注意std::stoi对空字符串或纯字母会抛std::invalid_argument读旧数据文件做迁移时先做字段数量校验按逗号切分后确认有 10 个字段再转换避免程序中途崩溃。一个具体技巧迁移旧数据时先用原fread把 100 个book读出来只保留id ! 0的直接写进新的文本文件之后所有代码走文本流。这个「一次性转换脚本」十几行就能写完比逐字段手录省事得多也顺手验证了旧文件有没有越界或乱码。本文还有配套的精品资源点击获取
返回列表