C语言商品进销存管理系统实战:结构体、文件读写与DevC++调试全解析
发布时间:2026/9/17 1:54:29 作者:尧图编辑部 阅读量:1,286

简介这是一份面向 C 语言初学者的商品进销管理系统课程设计资源围绕进货、销售、库存增减与销售金额计算等核心业务设计了输入、显示、增加、查找、排序、统计六项功能并通过菜单方式引导用户操作适合课程设计、期末作业或基础项目练手。压缩包共 6 个文件包含 DevC 工程文件、C 语言源码、可执行程序、Makefile 辅助配置等整体仅 49KB轻量易用下载后可直接打开工程或运行 exe 查看效果。目前已有 106 人学习使用。资源在源码层面保留了完整的项目结构与核心算法读者可借此学习结构体数组管理商品信息、日期区间统计、按数量排序等写法也能体会从需求分析、模块划分到编码调试的软件过程是巩固 C 语言综合能力的实用参考。1. C语言商品进销管理系统到底在练什么很多人在学完C语言基础后都会找带菜单的管理系统来练手。商品进销管理系统就是其中最典型的一种它没有复杂算法却能把结构体、函数、文件读写、循环菜单这些知识点全部串起来。更关键的是进销存天然包含三个容易出错的业务场景——进货要加库存销售要减库存单价和数量的金额计算不能想当然。用DevC写这个项目的人往往代码写完才发现中文乱码、控制台闪退、文件存不上这不是业务逻辑问题而是环境与工程方法的问题。这里就从实际可运行的C代码出发从数据结构、核心业务流程、文件持久化到DevC调试技巧把这个项目完整拆开。适合正在做课程设计或者想用一个小项目检验指针和文件操作功底的C语言学习者。2. 先定数据结构与代码文件划分2.1 用结构体定义商品信息商品进销存的核心对象是“商品”。一个商品在程序里至少需要编号、名称、进货价、售价、库存、销量这六个属性。在C语言里这组属性必须用结构体struct放在一起否则函数之间传递参数会非常痛苦。#define MAX_GOODS 500 #define NAME_LEN 50 typedef struct { int id; // 商品编号全局唯一 char name[NAME_LEN]; // 商品名称不含空格 double costPrice; // 进货价 double salePrice; // 售价 int stock; // 当前库存数量 int sold; // 累计销售数量 } Goods;这段代码里double比float更稳妥因为进销存经常涉及单价和金额float在多次累加后会出现明显误差。NAME_LEN定义为50是给字符串末尾的\0留出空间商品名最长只能49个字符。stock和sold用int足够课程设计场景不太可能超过21亿但如果做企业级原型考虑改成long long。结构体定了之后要决定用数组还是链表。常见做法是用全局数组Goods goods[MAX_GOODS]和计数器int total。这个方案的优势是简单数组元素连续存放排序用qsort直接完成查找用循环下标。链表在删除和插入时不需要搬移元素但这个项目里删除并不频繁没必要为了展示指针而引入额外的内存管理负担。2.2 按业务模块拆函数而不是让main承载一切不少初学者会把菜单循环和所有业务写在一个200行的main里这样做在DevC里编译没问题但一旦某个功能出错调试定位就非常慢。更合理的做法是按操作类型拆成独立函数让main只做菜单分发。下表列出了这个项目最基础的函数模块划分模块函数原型职责文件读写int saveToFile(Goods goods[], int total)把商品数组写入文件文件读写int loadFromFile(Goods goods[], int *total)程序启动时读回数据商品管理int addGoods(Goods goods[], int *total)新增商品检查编号重复商品管理int findById(Goods goods[], int total, int id)按编号查数组下标进销存void purchaseGoods(Goods goods[], int total)进货增加库存进销存void sellGoods(Goods goods[], int total)销售减库存并累计销量查询统计void showAll(Goods goods[], int total)显示全部商品查询统计void showRank(Goods goods[], int total)按销量排序findById是一个辅助函数进货和销售都要用到单独拆出来可以避免在两个地方写相同的for循环。实际写项目时我一般还会加一个modifyGoods用于修改价格但那是后话先把这个骨架跑通最重要。2.3 数组容量与全局数据传递的边界使用数组方案时MAX_GOODS就是系统的硬上限。500个商品对课程设计完全够用但你要在每次新增前判断总数量是否越界。更完整的做法是把total封装成文件头字段每次保存时先写总数量读文件时先读这个数量再循环读取商品数据。这里有一个DevC初学者容易忽略的点函数参数里写Goods goods[]和Goods *goods是等价的都退化成指针。所以在addGoods内部执行goods[*total] g; (*total);可以直接修改外部数组和计数变量。如果只把total的值传进函数函数内部改变total却不会影响外部这是C语言新手最容易犯的错也是这个项目的重要考点。3. 把进销存核心操作写成可复现的C函数3.1 商品录入先查重再写入addGoods看起来只是scanf几个值但处理不好会留下很多坑。先看基本版本int addGoods(Goods goods[], int *total) { if (*total MAX_GOODS) { printf(当前商品数量已达上限 %d\n, MAX_GOODS); return 0; } Goods g; printf(请输入商品编号: ); scanf(%d, g.id); for (int i 0; i *total; i) { if (goods[i].id g.id) { printf(该编号已存在录入失败\n); return 0; } } printf(请输入商品名称(不含空格): ); scanf(%s, g.name); printf(请输入进货价: ); scanf(%lf, g.costPrice); printf(请输入售价: ); scanf(%lf, g.salePrice); g.stock 0; g.sold 0; goods[*total] g; (*total); printf(录入成功当前共有 %d 种商品\n, *total); return 1; }这段代码里最容易被忽略的是goods[*total] g后面的(*total)。因为total是指针*total自增必须加括号写成*total会先对指针移动再取值属于隐藏的越界写操作。scanf(%lf, ...)对应double不能写成%f否则大数读取会变成0.000000。scanf(%s, g.name)不会读取空格因此商品名如果叫“牛奶 250ml”只能读到“牛奶”。想彻底解决需要换成fgets但fgets会吞掉回车要和scanf混用就得手动清缓冲。我的建议是这个项目就统一用scanf(%s)商品名只放连续无空格的短词把精力放在业务逻辑上。3.2 进货入库库存累加并更新成本价进货模块要区分两个概念修改商品信息里的“统一改价”和进货时的“增量入库”。void purchaseGoods(Goods goods[], int total) { int id, qty; printf(输入进货商品编号: ); scanf(%d, id); int idx findById(goods, total, id); if (idx -1) { printf(商品不存在\n); return; } printf(输入进货数量: ); scanf(%d, qty); if (qty 0) { printf(进货数量必须大于 0\n); return; } goods[idx].stock qty; printf(进货成功商品 [%s] 当前库存 %d\n, goods[idx].name, goods[idx].stock); }进货时是否需要更新costPrice取决于你的业务口径。如果每次进货单价不同但想让利润计算使用最新成本价那就在进完货后用新的进货价覆盖goods[idx].costPrice。课程设计通常只要求“数量增加”所以我这里不覆盖价格避免产生“库存数量变了成本均价没变”的困惑。想要更严谨应该维护一个totalCost字段进销存行业里叫移动加权平均但那是大项目的事C语言课程设计不一定要做。这里的findById是整个系统的复用重点int findById(Goods goods[], int total, int id) { for (int i 0; i total; i) { if (goods[i].id id) { return i; } } return -1; }选择返回下标而不是返回指针是因为后续很多函数既需要读写结构体又需要显示“第几条记录”回到数组下标会更直观。3.3 销售出库库存不足必须拦截销售是进销存的核心也是最容易出逻辑漏洞的地方。void sellGoods(Goods goods[], int total) { int id, qty; printf(输入销售商品编号: ); scanf(%d, id); int idx findById(goods, total, id); if (idx -1) { printf(商品不存在\n); return; } printf(输入销售数量: ); scanf(%d, qty); if (qty 0) { printf(销售数量必须大于 0\n); return; } if (qty goods[idx].stock) { printf(库存不足当前库存 %d销售失败\n, goods[idx].stock); return; } goods[idx].stock - qty; goods[idx].sold qty; printf(销售成功销售额 %.2f 元\n, qty * goods[idx].salePrice); }注意销售额的计算发生在库存扣减之后用qty * salePrice临时算不需要单独加一个字段。还需要留意执行顺序先校验再修改数据。如果先做了stock - qty发现库存变负数再想恢复就会引入更多状态位。另一个容易错的地方是如果销售后要记录“一条销售流水”你得再准备一个SaleRecord结构体数组这里不展开但对于想提高评分的人来说是一个很好的加分项。3.4 用qsort快速实现销量排行查询商品时最常见的需求是按销量降序排列。C标准库的qsort能在O(n log n)内完成排序比自己写冒泡更专业。int cmpBySold(const void *a, const void *b) { const Goods *ga (const Goods *)a; const Goods *gb (const Goods *)b; return gb-sold - ga-sold; } void showRank(Goods goods[], int total) { Goods tmp[MAX_GOODS]; for (int i 0; i total; i) { tmp[i] goods[i]; } qsort(tmp, total, sizeof(Goods), cmpBySold); printf(%-5s %-12s %-8s %-8s\n, 编号, 名称, 销量, 销售额); for (int i 0; i total; i) { printf(%-5d %-12s %-8d %.2f\n, tmp[i].id, tmp[i].name, tmp[i].sold, tmp[i].sold * tmp[i].salePrice); } }这里的技巧是复制一份数组再排序避免原始数组顺序被破坏。你可能会问为什么不用goods直接排如果后面还要按编号精确查找排序后的数组会让二分查找失效而课程设计往往在菜单中先显示再操作顺序错乱会让用户以为程序丢了数据。复制数组在500个商品规模下毫无性能压力换取的是逻辑简单。4. 文件持久化把数据写进txt并在启动时加载4.1 文本文件比二进制文件更适合DevC课程设计进销存系统关闭程序后数据不能每次都从main里重新录入。C语言持久化有两种常见选择文本文件和二进制文件。对比项文本文件.txt/.dat二进制文件.bin可读性用记事本能直接看到商品内容打开是乱码需要程序读回跨编译器兼容格式统一DevC和VS都能读受结构体字节对齐影响调试难度出错时容易定位第几行需要借助hexdump等工具推荐程度课程设计首选企业级原型更合适文本文件还有一个额外优势写试卷或者答辩时老师可以直接打开你的数据文件检查价格和库存是否正确。这是课程设计评分的一个重要加分细节。4.2 saveToFile与loadFromFile的成对实现保存数据时要先写总条数再逐条写商品信息。读取时严格按相同顺序读回。#define DATA_FILE goods.txt int saveToFile(Goods goods[], int total) { FILE *fp fopen(DATA_FILE, w); if (fp NULL) { perror(保存文件失败); return 0; } fprintf(fp, %d\n, total); for (int i 0; i total; i) { fprintf(fp, %d %s %.2f %.2f %d %d\n, goods[i].id, goods[i].name, goods[i].costPrice, goods[i].salePrice, goods[i].stock, goods[i].sold); } fclose(fp); return 1; } int loadFromFile(Goods goods[], int *total) { FILE *fp fopen(DATA_FILE, r); if (fp NULL) { printf(找不到数据文件将按空数据启动\n); *total 0; return 0; } if (fscanf(fp, %d, total) ! 1) { printf(数据文件格式错误\n); fclose(fp); return 0; } for (int i 0; i *total; i) { int r fscanf(fp, %d %s %lf %lf %d %d, goods[i].id, goods[i].name, goods[i].costPrice, goods[i].salePrice, goods[i].stock, goods[i].sold); if (r ! 6) { printf(数据损坏读取到第 %d 条时中断\n, i 1); *total i; break; } } fclose(fp); return 1; }注意fopen时选择w会直接截断原文件所以保存操作必须有明确的用户确认。fprintf中%.2f限制了单价只保留两位小数。读取时检查fscanf的返回值是否为6这是防止用户手动改坏文件后程序不崩溃的关键没这个检查文件最后一行缺字段时会读到goods[i].name的随机值后续扣库存会越界。还有一个藏在细节里的坑fscanf(%s)读取同样不能含空格所以文件名中尽量避免空格。如果坚持要带空格的名称格式要改成用|分隔再用fgets按行解析代码复杂度会明显上升。4.3 DevC在调试时找不到文件工作目录问题很多人写完读写逻辑后在DevC里按F11运行发现fopen返回NULL但到资源管理器里双击exe就能运行。原因是DevC运行时程序的工作目录通常是项目里的.exe所在目录或者DevC的默认临时目录不一定是你的代码文件目录。用相对路径goods.txt时文件会被创建在当前工作目录下。最简单的排查办法是在程序启动时打印当前工作目录#include direct.h void showWorkDir() { char buffer[256]; if (getcwd(buffer, sizeof(buffer)) ! NULL) { printf(当前工作目录: %s\n, buffer); } }在main最开始调用showWorkDir()然后去这个目录找goods.txt。如果文件确实生成在这里说明程序本身没有错是之前找错了地方。另一种做法是把DATA_FILE改成绝对路径比如C:\\temp\\goods.txt但这会让交作业时路径失效所以更推荐在代码里维持相对路径运行时按打印的目录去操作系统里核对。5. DevC调试桌面用 5 个技巧收住边界5.1 中文乱码不是逻辑bug是执行字符集问题DevC的控制台窗口在中文Windows下默认使用GBK编码而新版MinGW编译器默认把源码按UTF-8处理导致printf(商品)输出乱码。解决方法是打开DevC的“工具 - 编译器选项 - 编译器”在编译器命令行加入-fexec-charsetGBK然后重新编译。这条参数指示编译器在生成的可执行文件里把窄字符串转成GBK编码能解决中文乱码却不影响代码里的UTF-8注释。如果你把源码文件另存为ANSI编码GBK不加强制参数也能正常输出但换到Linux服务器上用gcc编译就会乱所以学习阶段最好理解这两种路径的区别。建议保留UTF-8源码只加编译参数这样跨平台时改动最小。5.2 用fopen返回值与assert夹住输入边界开发阶段可以在容易越界的位置加断言让问题在发生时立刻暴露。比如addGoods写完后加一行assert(*total MAX_GOODS);这行代码在发布版里可以用#define NDEBUG关掉。assert比单纯printf强的地方是它同时打印出错的源文件名和行号便于从DevC的调试输出窗口直接定位。对指针的判断也可以用断言assert(goods ! NULL);。要注意断言失败时程序会中断这不是崩溃是在提醒你上游逻辑越界。5.3 用一张测试表验证库存和销售额是否正确系统写完别急着交。我习惯手工设计一组覆盖边界的数据按顺序执行操作输入期望结果新增商品A编号1名称cola进价2.0售价3.0库存0销量0进货商品1数量10库存10销售商品1数量12提示库存不足销量不变销售商品1数量4库存6销量4再次销售商品1数量6库存0销量10保存退出-goods.txt中库存0销量10重新打开-读取后库存0销量10写这样一份清单放在项目文档里能让答辩演示更有说服力。尤其要测试“库存不足”这个分支它证明你考虑了业务异常而不仅仅是正常路径。5.4 用scanf缓冲残留的教训scanf混合输入数字和字符串时回车键会留在缓冲区导致下一次scanf(%s)读到空串。常见修法是在每次scanf后加while (getchar() ! \n);。但注意这个循环不适用于文件末尾没有换行的情况。在这个项目里菜单通常用数字选择输入非法字符时scanf会失败并陷入死循环可以这样兜底if (scanf(%d, choice) ! 1) { while (getchar() ! \n); printf(请输入数字\n); continue; }当choice读取成功后continue会回到主菜单顶部不会因为非数字输入把程序拖进死循环。这个项目里所有涉及scanf的地方都应该按同样的方式检查返回值而不是假设用户一定会老实输入。之后再结合5.3的测试表把每一组数据从新增到销售再到文件落盘完整走一遍这套C语言商品进销管理系统就算真正能在DevC里稳定运行了。本文还有配套的精品资源点击获取