ARTICLE DETAIL

资讯详情

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

C++控制台ATM系统:结构体建模+文件持久化实现银行存取款逻辑

C++控制台ATM系统:结构体建模+文件持久化实现银行存取款逻辑 简介本资源是一份面向C初学者与高校《面向对象程序设计》课程学习者的ATM机存取款管理系统课设实践材料聚焦面向对象建模、基础数据结构应用与控制台交互逻辑实现。内容涵盖完整可运行的C源码241行单文件、详细课程设计报告及用户使用手册系统支持卡号密码验证3次失败锁卡、余额查询、100元倍数取款≤5000元且不透支、非负存款、6位以上差异化密码修改等核心功能并明确使用STL容器如vector/map管理用户与交易数据。资源为1个PDF文件大小1.25MB内容结构清晰含概述、功能设计、类图说明、模块代码与运行示例适合作为OOP入门项目参考与课程设计答辩素材。目前已有1599人学习下载对夯实类封装、继承、异常处理及命令行系统开发能力具有直接训练价值。1. 用纯 C 控制台实现 ATM 存取款逻辑不依赖 GUI 框架、不连数据库靠结构体文件持久化完成真实业务闭环这不是一个“画界面”的练习——它要解决银行柜员机最底层的业务约束账户余额不能为负、取款金额必须是 100 的整数倍、每日取款上限 5000 元、密码输错三次锁定账户、所有操作必须落盘可追溯。很多初学者用cout 请输入金额就算完成了 ATM但真实场景里一次cin amount后没做校验、没写日志、没更新文件、没处理异常输入整个系统在第二笔交易时就会崩掉。本方案面向 C 入门后想落地业务逻辑的学习者也适合嵌入式或资源受限环境下的轻量级金融模拟需求。核心不在于炫技而在于把「银行规则」翻译成 C 可执行的判断链、状态机和文件 I/O 流程。你不需要 Qt 或 Web 框架只需要fstream、vector、string和对struct生命周期的清晰认知——这恰恰是 C 工程师在金融终端、POS 机固件、IoT 设备后台真正要写的代码。2. 用结构体建模账户实体用 vector 管理多用户用文本文件实现持久化存储2.1 账户结构体设计封装业务字段与校验逻辑C 中结构体不是数据容器而是行为载体。ATM 系统中账户必须自带校验能力否则每次存取款都要在主流程里重复写if (balance 0)。我们定义Account结构体将业务规则内聚#include iostream #include string #include vector #include fstream #include iomanip #include ctime struct Account { std::string accountNumber; // 卡号唯一标识 std::string name; // 持卡人姓名 std::string password; // 密码明文存储仅用于教学生产环境需哈希 double balance; // 当前余额单位元 int withdrawalCount; // 当日取款次数用于限额控制 double dailyWithdrawalSum; // 当日累计取款额 bool isLocked; // 是否被锁定密码错误 ≥3 次 // 构造函数初始化默认值 Account() : balance(0.0), withdrawalCount(0), dailyWithdrawalSum(0.0), isLocked(false) {} // 校验取款金额是否合法正数、100 整数倍、不超过余额、不超过日限额 bool canWithdraw(double amount) const { if (amount 0) return false; if (static_castint(amount) % 100 ! 0) return false; // 必须是 100 的整数倍 if (amount balance) return false; if (dailyWithdrawalSum amount 5000.0) return false; return true; } // 执行取款更新内部状态 void withdraw(double amount) { balance - amount; dailyWithdrawalSum amount; withdrawalCount; } // 执行存款仅更新余额 void deposit(double amount) { balance amount; } // 重置当日取款计数每日首次登录时调用 void resetDailyLimit() { withdrawalCount 0; dailyWithdrawalSum 0.0; } };提示canWithdraw()返回bool而非直接修改余额这是关键设计。它把“能否操作”和“执行操作”分离避免因校验失败导致状态不一致。实际调用时先if (acc.canWithdraw(x)) acc.withdraw(x);而不是acc.withdraw(x)再检查返回值——后者无法回滚已发生的扣减。2.2 用户列表管理用 vector 存储运行时账户集合std::vectorAccount是最自然的选择支持随机访问按卡号查找、动态扩容新增用户、线性遍历打印所有账户。不使用 map 是因为本系统不强调 O(1) 查找且卡号字符串查找用find_if已足够高效更重要的是vector 更贴近内存布局直觉便于理解对象生命周期。// 全局账户列表实际项目应封装为类成员 std::vectorAccount accounts; // 根据卡号查找账户返回指针nullptr 表示未找到 Account* findAccount(const std::string cardNo) { auto it std::find_if(accounts.begin(), accounts.end(), [cardNo](const Account a) { return a.accountNumber cardNo; }); return (it ! accounts.end()) ? (*it) : nullptr; } // 加载账户数据从 accounts.txt 读取每行格式为 卡号,姓名,密码,余额,当日取款次数,当日取款总额,是否锁定 void loadAccounts() { std::ifstream file(accounts.txt); if (!file.is_open()) { std::cout 警告accounts.txt 不存在将创建空账户列表\n; return; } std::string line; while (std::getline(file, line)) { if (line.empty()) continue; std::vectorstd::string fields split(line, ,); // 辅助函数见下文 if (fields.size() 7) continue; Account acc; acc.accountNumber fields[0]; acc.name fields[1]; acc.password fields[2]; acc.balance std::stod(fields[3]); acc.withdrawalCount std::stoi(fields[4]); acc.dailyWithdrawalSum std::stod(fields[5]); acc.isLocked (fields[6] 1); accounts.push_back(acc); } file.close(); } // 辅助函数按分隔符拆分字符串C11 无内置 split需手写 std::vectorstd::string split(const std::string s, char delimiter) { std::vectorstd::string tokens; std::string token; std::istringstream tokenStream(s); while (std::getline(tokenStream, token, delimiter)) { // 去除首尾空格 size_t first token.find_first_not_of( ); size_t last token.find_last_not_of( ); if (first ! std::string::npos) tokens.push_back(token.substr(first, (last - first 1))); } return tokens; }注意loadAccounts()中std::stod和std::stoi可能抛出std::invalid_argument异常。生产环境必须加try-catch此处为简化未展开但你在实操时务必补上——否则一行格式错误的文本会让整个系统崩溃退出。2.3 文件持久化序列化与反序列化账户数据C 没有反射所以必须手动定义文本格式。我们采用 CSV 风格字段间用英文逗号分隔确保可读、可编辑、易调试// 保存账户列表到文件覆盖原内容 void saveAccounts() { std::ofstream file(accounts.txt); if (!file.is_open()) { std::cerr 错误无法写入 accounts.txt\n; return; } for (const auto acc : accounts) { file acc.accountNumber , acc.name , acc.password , std::fixed std::setprecision(2) acc.balance , acc.withdrawalCount , std::fixed std::setprecision(2) acc.dailyWithdrawalSum , (acc.isLocked ? 1 : 0) \n; } file.close(); } // 初始化示例账户首次运行时调用 void initDefaultAccounts() { if (accounts.empty()) { Account admin; admin.accountNumber 6228480000000000000; admin.name 张三; admin.password 123456; admin.balance 10000.0; accounts.push_back(admin); Account user; user.accountNumber 6228480000000000001; user.name 李四; user.password 654321; user.balance 5000.0; accounts.push_back(user); } }提示std::fixed std::setprecision(2)确保金额输出为10000.00而非1e04这对后续std::stod解析至关重要。若不设精度文件中可能写入10000下次读取时stod(10000)虽然正确但若余额为9999.99不设精度会输出9999.990000导致解析误差。统一用两位小数是金融系统的硬性要求。3. 实现 ATM 主循环登录验证、菜单驱动、状态机控制与错误恢复3.1 登录模块三次失败锁定 时间戳重置逻辑ATM 登录不是简单比对密码它涉及状态变更锁定/解锁和时间感知每日重置。我们不依赖系统时间库做复杂判断而是采用“首次登录即重置当日限额”的策略——更符合银行实际柜员机开机即视为新一天开始// 登录函数返回指向账户的指针nullptr 表示失败 Account* login() { std::string cardNo, password; int attempt 0; const int MAX_ATTEMPTS 3; while (attempt MAX_ATTEMPTS) { std::cout \n ATM 登录 \n; std::cout 请输入卡号: ; std::cin cardNo; std::cout 请输入密码: ; std::cin password; Account* acc findAccount(cardNo); if (acc nullptr) { std::cout 错误卡号不存在\n; attempt; continue; } if (acc-isLocked) { std::cout 错误该账户已被锁定请联系银行\n; return nullptr; } if (acc-password password) { // 登录成功重置当日限额 acc-resetDailyLimit(); std::cout 欢迎回来 acc-name \n; return acc; } else { std::cout 错误密码不正确\n; attempt; if (attempt MAX_ATTEMPTS) { acc-isLocked true; std::cout 警告密码错误超过 3 次账户已被锁定\n; } } } return nullptr; }注意resetDailyLimit()在登录成功后立即调用而非在程序启动时全局重置。这是因为多个用户可能在同一天不同时间登录每人登录时刻即为其“当日”起点。若在main()开头统一重置会导致早登录用户限额被晚登录用户意外清空。3.2 主菜单与操作路由用 switch-case 实现状态跳转C 控制台程序的状态流转必须显式编码。我们用while(true)主循环 switch分支每个 case 对应一个功能并在操作完成后break回到菜单避免深层嵌套// ATM 主操作循环 void atmMainLoop(Account* currentAccount) { int choice; while (true) { std::cout \n currentAccount-name 的 ATM 菜单 \n; std::cout 1. 查询余额\n; std::cout 2. 存款\n; std::cout 3. 取款\n; std::cout 4. 修改密码\n; std::cout 5. 退出登录\n; std::cout 请选择 (1-5): ; std::cin choice; switch (choice) { case 1: showBalance(currentAccount); break; case 2: doDeposit(currentAccount); break; case 3: doWithdraw(currentAccount); break; case 4: changePassword(currentAccount); break; case 5: std::cout 感谢使用再见\n; return; default: std::cout 无效选择请重新输入\n; break; } } } // 查询余额 void showBalance(const Account* acc) { std::cout \n--- 账户余额 ---\n; std::cout 卡号: acc-accountNumber \n; std::cout 姓名: acc-name \n; std::cout 当前余额: ¥ std::fixed std::setprecision(2) acc-balance \n; std::cout 今日已取款: acc-withdrawalCount 次共计 ¥ std::fixed std::setprecision(2) acc-dailyWithdrawalSum \n; std::cout 日取款限额剩余: ¥ std::fixed std::setprecision(2) (5000.0 - acc-dailyWithdrawalSum) \n; } // 存款操作 void doDeposit(Account* acc) { double amount; std::cout \n--- 存款 ---\n; std::cout 请输入存款金额: ; std::cin amount; if (amount 0) { std::cout 错误存款金额必须大于 0\n; return; } acc-deposit(amount); std::cout 存款成功当前余额: ¥ std::fixed std::setprecision(2) acc-balance \n; } // 取款操作 void doWithdraw(Account* acc) { double amount; std::cout \n--- 取款 ---\n; std::cout 请输入取款金额必须为 100 的整数倍: ; std::cin amount; if (!acc-canWithdraw(amount)) { if (amount 0) { std::cout 错误取款金额必须大于 0\n; } else if (static_castint(amount) % 100 ! 0) { std::cout 错误取款金额必须是 100 的整数倍\n; } else if (amount acc-balance) { std::cout 错误余额不足\n; } else if (acc-dailyWithdrawalSum amount 5000.0) { std::cout 错误超过当日取款限额 5000 元\n; } return; } acc-withdraw(amount); std::cout 取款成功当前余额: ¥ std::fixed std::setprecision(2) acc-balance \n; std::cout 今日已取款: acc-withdrawalCount 次累计 ¥ std::fixed std::setprecision(2) acc-dailyWithdrawalSum \n; }提示doWithdraw()中的if (!acc-canWithdraw(amount))后用else if链明确告知用户具体失败原因。这比笼统的“操作失败”更友好也符合银行系统 UX 规范——用户需要知道是“钱不够”还是“今天取超了”。3.3 密码修改与输入安全避免明文回显与缓冲区溢出控制台密码输入需隐藏字符C 标准库无跨平台getch()我们用std::cin.get()逐字符读取并输出*同时限制最大长度防溢出// 安全输入密码不回显 std::string secureInputPassword(const std::string prompt) { std::cout prompt; std::string password; char ch; while ((ch std::cin.get()) ! \n) { if (ch \b || ch \r) { // 处理退格 if (!password.empty()) { password.pop_back(); std::cout \b \b; // 删除显示的 * } } else if (password.length() 16 ch 32 ch 126) { // ASCII 可见字符限长 16 password ch; std::cout *; } } std::cout \n; return password; } // 修改密码 void changePassword(Account* acc) { std::string oldPass secureInputPassword(请输入原密码: ); if (oldPass ! acc-password) { std::cout 错误原密码不正确\n; return; } std::string newPass secureInputPassword(请输入新密码最多 16 位: ); if (newPass.empty()) { std::cout 错误新密码不能为空\n; return; } acc-password newPass; std::cout 密码修改成功\n; }注意此实现未处理 Windows 下CtrlZ终止输入等边界情况但在教学场景已足够。生产环境应使用termiosLinux或conio.hWindows做更底层控制但那已超出本项目范围。4. 编译与运行配置VS Code CMake MinGW 环境搭建实操指南4.1 VS Code 中配置 C/C 编译任务tasks.jsonC 新手常卡在编译环节。本方案使用 MinGW-w64推荐 x86_64-10.2.0-release-win32-seh-rt_v7-rev1作为编译器因其轻量、免安装 Visual Studio。在 VS Code 中需手动配置tasks.json{ version: 2.0.0, tasks: [ { type: cppbuild, label: C/C: g.exe build active file, command: D:\\mingw64\\bin\\g.exe, args: [ -g, ${file}, -o, ${fileDirname}\\${fileBasenameNoExtension}.exe, -stdc17, -Wall, -Wextra ], options: { cwd: ${fileDirname} }, problemMatcher: [$gcc], group: build, detail: compiler: D:\\mingw64\\bin\\g.exe } ] }提示-stdc17启用结构化绑定、std::optional等现代特性-Wall -Wextra开启全部警告能提前发现std::stod未捕获异常、vector越界等隐患。路径D:\\mingw64\\bin\\g.exe需按你实际安装位置修改。4.2 CMakeLists.txt声明标准与链接选项为便于跨平台迁移建议用 CMake 管理构建。根目录下创建CMakeLists.txtcmake_minimum_required(VERSION 3.10) project(ATMSystem) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 添加可执行文件 add_executable(atm main.cpp) # 如果使用第三方库如后续扩展日志在此链接 # target_link_libraries(atm PRIVATE some_library)然后在终端执行mkdir build cd build cmake .. -G MinGW Makefiles mingw32-make生成的atm.exe即可双击运行无需依赖任何 DLL。4.3 关键编译参数与常见错误修复错误信息原因解决方案error: stoi is not a member of stdC 标准未指定在#include string后添加#include cstdlib或确保-stdc11及以上undefined reference to WinMain16Windows 下链接为 GUI 程序在g命令后加-mconsole参数强制控制台模式fatal error: bits/stdc.h: No such file or directory使用了非标准头文件删除#include bits/stdc.h改用具体头文件iostream,vector等注意bits/stdc.h是 GCC 扩展不可移植。本项目所有头文件均来自 ISO C 标准确保在 Clang、MSVC 下同样编译通过。5. 运行时调试与日志增强添加操作流水与错误追踪机制5.1 事务日志文件记录每一笔存取款详情银行系统必须留痕。我们在每次存款/取款后向transaction.log追加一行格式为时间|卡号|类型|金额|余额#include chrono #include thread // 获取当前时间字符串格式2023-10-05 14:23:15 std::string getCurrentTime() { auto now std::chrono::system_clock::now(); auto time_t std::chrono::system_clock::to_time_t(now); auto ms std::chrono::duration_caststd::chrono::milliseconds(now.time_since_epoch()) % 1000; std::stringstream ss; ss std::put_time(std::localtime(time_t), %Y-%m-%d %H:%M:%S); ss . std::setfill(0) std::setw(3) ms.count(); return ss.str(); } // 记录交易日志 void logTransaction(const std::string cardNo, const std::string type, double amount, double balance) { std::ofstream log(transaction.log, std::ios::app); if (log.is_open()) { log getCurrentTime() | cardNo | type | std::fixed std::setprecision(2) amount | std::fixed std::setprecision(2) balance \n; log.close(); } } // 修改 doDeposit 和 doWithdraw在操作后调用 logTransaction void doDeposit(Account* acc) { // ... 原有逻辑 acc-deposit(amount); logTransaction(acc-accountNumber, DEPOSIT, amount, acc-balance); // ... } void doWithdraw(Account* acc) { // ... 原有逻辑 acc-withdraw(amount); logTransaction(acc-accountNumber, WITHDRAW, amount, acc-balance); // ... }提示std::chrono::system_clock::now()获取高精度时间戳ms.count()提供毫秒级精度避免同一秒内多笔交易日志时间相同。日志文件自动追加std::ios::app无需手动管理文件大小——这是开发阶段的合理取舍。5.2 错误码与断言定位运行时逻辑漏洞当findAccount()返回nullptr却被当作有效指针解引用程序会崩溃。我们用assert在调试版中捕获此类逻辑错误#include cassert // 在关键操作前加入断言 void showBalance(const Account* acc) { assert(acc ! nullptr showBalance: 账户指针为空); // 调试时启用 // ... 后续逻辑 } // 在 release 版本中用 if 替代 assert #ifdef NDEBUG if (acc nullptr) { std::cerr 严重错误空账户指针传入 showBalance\n; return; } #endif注意assert仅在NDEBUG未定义时生效即 Debug 模式。发布时应移除所有assert改用显式错误处理否则用户看到断言失败弹窗会误以为是病毒。5.3 内存泄漏检测用 _CrtDumpMemoryLeaksWindows验证资源释放本项目未使用new/delete理论上无内存泄漏。但为建立工程习惯可在main()结尾添加检测#ifdef _MSC_VER #include crtdbg.h #endif int main() { #ifdef _MSC_VER _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif loadAccounts(); initDefaultAccounts(); while (true) { Account* acc login(); if (acc ! nullptr) { atmMainLoop(acc); } else { std::cout 登录失败退出系统。\n; break; } } saveAccounts(); return 0; }提示_CrtDumpMemoryLeaks()是 MSVC 特有功能仅在 Windows Debug 模式下有效。它会在程序退出时打印未释放内存块帮助发现new后忘记delete的问题。本项目虽未用堆内存但此习惯应贯穿所有 C 工程。最终这个 C ATM 系统不是玩具代码——它用结构体封装业务规则、用 vector 管理运行时状态、用文本文件实现 ACID-like 持久化、用状态机控制交互流程、用日志满足审计要求。你可以把它编译成单个.exe文件拷贝到任意 Windows 电脑运行所有数据存于同目录下的accounts.txt和transaction.log。下一步若要升级为网络服务只需将atmMainLoop()替换为accept()fork()循环若要接入 MySQL只需将saveAccounts()改为INSERT INTO accounts ...。根基扎实演进才有底气。本文还有配套的精品资源点击获取
返回列表