首页 / 资讯中心 / 文章详情

C++模拟ATM取款源码:面向对象封装与依赖注入实战解析

C++模拟ATM取款源码:面向对象封装与依赖注入实战解析 ★ FEATURED ARTICLE
简介这是一份以C模拟ATM取款机系统为主线的源码资源面向C初学阶段和面向对象课程设计者完整展示登录、密码核对与修改、取款、余额查询、退出以及连续输错触发锁机等功能流程。包体为单个docx文档大小约23KB文档将需求分析说明和关键类源代码放在一起便于对照阅读。实现围绕ATM和consumer两个类展开consumer把姓名、卡号、余额和密码封装为私有成员仅通过公有接口访问ATM借助友元关系读取用户资料清晰体现了封装、友元类和类职责划分等面向对象思想。文档中还给出ATM操作界面与功能菜单的实现框架并包含功能菜单的显示与循环调用逻辑适合在普通C环境下直接编译学习对理解构造函数、私有成员保护和友元机制尤其有帮助。已有272人学习浏览适合用来临摹源码、梳理ATM业务逻辑并可在此基础上扩展取款限额、交易记录、异常处理及安全机制形成更完整的银行模拟系统。1. 一个C模拟ATM取款源代码把面向对象最实在的用法摆在你面前第一次看到这份C模拟ATM取款源代码时我以为是某个课程设计的模板但读进去才发现它把面向对象最基础也最关键的封装、依赖注入、状态判断全演了一遍。这个项目模拟一台自动取款机用户登录、密码校验、修改密码、取款、查询余额、退出和三次输错锁机全部由两个类完成——ATM类和consumer类。适合正在学C面向对象的人或者想快速找一个能跑起来、能二次开发的命令行小项目练手的人。它不用数据库、不用网络编译完就能在终端里走一遍完整的取款流程代码量不大但结构清晰你能直接看到“机器依赖用户”这种关系是怎么用引用传参表达的。2. 项目结构与类设计ATM和consumer是怎么协作的2.1 文件拆分一份头文件和两份实现文件加一个入口整个工程没有用IDE自动生成的工程文件就是三个核心文件加一个主文件目录结构非常直白ATM_Project/ ├── function.h // 类声明ATM和consumer的定义 ├── consumer.cpp // consumer类成员函数实现 ├── ATM.cpp // ATM类成员函数实现 └── main.cpp // 入口构造用户并启动ATMfunction.h里声明了两个类consumer在下面ATM在上面但ATM的构造函数参数是consumer说明ATM需要引用一个已存在的consumer对象。这种设计在真实项目中叫依赖注入取款机不负责创建用户只负责操作外部传进来的用户。consumer类提供了一组get和set方法ATM类通过这层访问接口读写用户数据用户密码和余额被封装成private外部不能直接改。我一般会把这种结构理解为“数据类加业务类”的经典组合。consumer是数据实体ATM是业务操作两者通过friend class ATM成为友元这样ATM能直接调用consumer的protected方法而外部代码只能走public接口。这个设计在小型项目里够用但到了真实系统友元会破坏封装后面改造时可以换成公有接口或独立服务层。2.2 consumer类把用户数据封成黑匣子先看consumer的声明和实现。它的私有成员是char passwd[8]、char name[20]、char num[20]和float money全部是C风格字符数组和浮点数。构造函数用strcpy拷贝字符串同时初始化余额consumer::consumer(char Name[], char Num[], float Money, char Password[]) { strcpy(name, Name); strcpy(num, Num); money Money; strcpy(passwd, Password); }这里注意密码数组只有8个字节如果传入的密码超过7个字符strcpy就会越界写内存这是经典的安全隐患。真实项目中至少应该用std::string或者对输入长度做硬校验。set_money方法做的是减法不是赋值它表示扣款void consumer::set_money(float m) { money - m; }所以ATM取款时只要传入取款金额余额就会自动减少。但它没有做余额判断判断逻辑放在fetchmoney里。这样设计的好处是“扣钱”这个动作只允许减少不允许直接覆盖防止业务层把余额改乱。缺点是如果以后要支持存款还得再加一个add_money方法。2.3 ATM类依赖引用外部用户内置次数状态ATM类的成员包括一个int times和一个consumer cnsm引用。构造函数里ATM(consumer cn):cnsm(cn)把外部传入的用户绑定到引用上这样整个生命周期内ATM操作的都是同一个用户对象。class ATM { public: ATM(consumer cn):cnsm(cn) {} void welcome(); bool check_passwd(char n[], char pwd[]); void change_passwd(); void fetchmoney(); void information(); void exitATM(); void functionshow(); void lock(); private: int times; consumer cnsm; };times变量用来记录密码输入错误次数每次welcome开始时清零。三次错误就触发lock()模拟银行卡被吞。这个“次数状态”是ATM类内部的关键状态它没有暴露成public外部无法篡改只能通过实际操作来影响。这里有个值得注意的点ATM持有的是引用不是指针所以ATM对象本身不拥有用户的生命周期。main里先构造consumer再构造ATM这样用户对象一定先于ATM存在ATM使用期间用户不会被释放。如果你改成指针就得操心空指针和所有权问题引用在这个场景下更省心。2.4 编译与运行一条命令就把项目跑起来这份代码是早期C风格用的是iostream.h和stdlib.h编译时需要注意。我用的命令如下g main.cpp ATM.cpp consumer.cpp -o atm_simulator ./atm_simulator如果你的编译器版本较新可能提示找不到iostream.h这是老式头文件名。解决方法是把#includeiostream.h改成#includeiostream并在下面加一行using namespace std;。stdlib.h也建议换成cstdlib。改完后用上面的命令编译就能看到终端里的欢迎界面$ 欢迎使用若雪银行 ATM 自动取款机!~! 请输入卡号:注意这里卡号和密码的读取用了cin.get()逐字符读回车键才会结束输入和直接用cin的行为不一样。后面在第3章会详细讲这个细节。3. 核心模块拆解登录、取款、锁机的完整链路3.1 welcome()里隐藏的输入读取陷阱welcome()是整个程序的入口流程。它的第一行把times清零然后进入一个do-while循环循环条件while(times3)。每次循环让用户输入卡号和密码如果check_passwd返回false就提示重新输入同时times加一如果校验通过就进入functionshow()功能菜单。看这段输入逻辑cout 请输入卡号:; do { cin.get(ch); num[i] ch; } while (ch ! \n); num[i - 1] \0;这个循环用cin.get(ch)读取每一个字符包括最后的回车符。当ch等于换行符时循环退出然后把数组最后一个位置也就是回车符的位置替换成字符串结束符\0。这样做的好处是卡号可以包含空格而cin num遇到空格会截断。坏处是如果你在输入两个字符前就按回车数组里可能会残留一些不可见字符而且没有对下标i做边界检查全靠用户自觉不要输入超过19个字符的卡号。这里常见做法是改用cin.getline(num, 20)它能指定最大长度自动处理换行符更安全cin.getline(num, 20); cin.getline(pwd, 8);不过原始代码这种逐字符读法用来演示“缓冲区里到底发生了什么”很有教学价值新手能看到回车被当作字符读进来的全过程。3.2 check_passwd()与三次错卡没收check_passwd非常朴素就用strcmp把输入的卡号和密码和customer对象里的值逐一比对bool ATM::check_passwd(char num[], char pwd[]) { if (strcmp(num, cnsm.get_num()) 0 strcmp(pwd, cnsm.get_passwd()) 0) return true; else return false; }三个错误后do-while循环退出调用lock()void ATM::lock() { cout 对不起由于你的操作有误你的卡已经被没收! endl; exit(1); }exit(1)直接终止整个进程没有任何恢复机会。真实ATM不可能因为三次输错就杀进程它会把卡吞掉然后等待下一个用户。改造时至少应该让lock()回到初始状态把times清零重新欢迎下一个用户而不是退出程序。这里有个隐藏逻辑修改密码功能里也有times计数如果在修改密码时连续输错旧密码三次同样会调用lock()。也就是说登录和改密都会触发锁机但welcome里的times和change_passwd里的times是独立的后者在函数开头被重置成0。这是一个状态管理不一致的地方多次操作后times数值可能让人迷惑。3.3 fetchmoney()的余额校验和取款循环取款函数是业务逻辑最重的一块。先看代码void ATM::fetchmoney() { float m; char ch; do { cout 你要取多少钱: endl; cin m; while (m 0) { cout 请输入正确的数字! endl; cin m; } if (cnsm.get_money() - m 0) { cout 对不起你的余额不足! endl; } else { cout 操作成功请收好钱! endl; cnsm.set_money(m); } cout 是否要继续该项操作(Y/N) endl; cin ch; while (ch ! n ch ! N ch ! Y ch ! y) { cin ch; } } while (ch y || ch Y); }逻辑分三步先读一个浮点数m小于等于0就死循环重读然后判断余额 - m是否小于0如果余额不足就提示但不会扣款如果足够就调用cnsm.set_money(m)扣款。最后询问是否继续输入Y或y就再来一轮。注意set_money实现的是money - m所以取款成功后余额确实是变少了。但这里没有限制单次取款金额比如余额有5万你输100万只会提示余额不足你输4万全都给你这在真实银行不可能发生。改造时可以加上单次限额和每日累计限额后面第5章会讲。这段还有一个比较隐蔽的雷如果你输入的不是数字比如输入“abc”cin m会失败m保持原值但错误状态位被置位后面所有输入操作都会失效。需要用cin.clear()和cin.ignore()恢复流状态。这类输入校验在原始代码里完全没处理。3.4 change_passwd()的两次新密码校验修改密码的逻辑分为两步先验证旧密码再让用户输入两次新密码并比对。上面那段旧密码验证使用了check_passwd但它传入的是cnsm.get_num()也就是说用当前用户的卡号和新输入的密码去匹配。因为check_passwd只负责字符串比较不关心调用者怎么取卡号所以这样写也能通过。新密码验证是do-while循环int t 0; do { cout 请输入新密码; cin pwd; cout 请再输入一次新密码; cin repwd; if ((t strcmp(pwd, repwd)) ! 0) cout 你输入的两次密码不一样请重新输入! endl; } while (t ! 0); cnsm.set_passwd(pwd);strcmp返回值在相等时为0所以循环条件while(t!0)表示只要两次不一致就重来。一致后调用cnsm.set_passwd(pwd)写入新密码。这段代码在正常输入下没问题但数组边界依然没有保护输入超过7个字符的新密码会直接覆盖栈上其他变量可能连旧密码都一起被破坏。我建议把pwd和repwd都改成std::string然后用比较再检查长度。或者至少在做strcpy前加一句strlen(pwd) 8的判断。4. 避坑/常见问题这段代码里最容易踩的坑4.1 现象编译时报错找不到iostream.h原因这是老式头文件现代GCC默认不再提供。解决把#includeiostream.h改成#includeiostream并在全局加using namespace std;。stdlib.h改成cstdlibstring.h改成cstring。改完后再编译就正常了。4.2 现象输入卡号或密码时还没输完就自动进入下一步原因逐字符读取的do-while循环会把回车符算进字符数组如果上一个输入缓冲里残留了换行符cin.get()会先把那个换行读走导致看起来“什么都没输入就跳过了”。解决在每次读取前用cin.ignore()清理掉残留换行或者直接改用cin.getline()。4.3 现象密码超过8个字符时程序异常崩溃原因char passwd[8]只能容纳7个字符加一个结束符strcpy会把超出的部分写到栈上破坏相邻变量。解决把密码数组改成std::string或者在所有strcpy之前检查strlen。这是最值得新手注意的缓冲区越界问题。4.4 现象取款时输入英文或符号程序像是卡死了原因cin m读取失败会设置流错误状态后续所有cin 都会直接失败循环无法退出。解决在读取浮点数后判断cin.fail()如果是就cin.clear()清掉错误标志再用cin.ignore()丢弃非法字符重新读取。这是命令行程序最常见的翻车点。4.5 现象改名密码后再次登录用旧密码竟然还能通过原因仔细看change_passwd的旧密码验证它调用check_passwd时用的是cnsm.get_num()和用户输入的pwd但pwd是从char pwd[8]读入的。如果你的输入包含回车或缓冲区残留strcmp可能匹配上旧密码的残留内容。解决统一用getline读取密码别混用cin和cin.get()。5. 扩展改造把单用户demo变成多用户档案系统5.1 用文件读写保存用户数据原始代码只有一个写死的consumer对象这显然不贴近真实。最直接的改造是把用户信息存到文本文件里程序启动时读取文件建立用户列表退出时写回。我一般会先定义一个简单的存储格式卡号,姓名,余额,密码 12345,jim,5200.30,123 67890,amy,8800.00,456再封装一个UserStore类负责加载和保存。用标准库里的ifstream和ofstream就能搞定#include fstream #include sstream #include vector struct UserRecord { std::string num; std::string name; float money; std::string passwd; }; std::vectorUserRecord loadUsers(const std::string path) { std::vectorUserRecord users; std::ifstream fin(path); std::string line; while (std::getline(fin, line)) { std::stringstream ss(line); UserRecord ur; std::getline(ss, ur.num, ,); std::getline(ss, ur.name, ,); ss ur.money; ss.ignore(1, ,); std::getline(ss, ur.passwd, ,); users.push_back(ur); } return users; }这段代码把每一行按逗号拆成四个字段。注意ss ur.money读完后要ignore跳过逗号否则后面的getline会读出空字符串。这是文件解析里最常见的坑顺手记一下。改造后ATM类不再固定引用一个consumer而是接收一个用户列表登录时遍历匹配卡号。5.2 用vector管理多个账户原始代码的ATM(consumer cn)把用户写死改造的第一步是让ATM持有一个用户集合或者每次登录后在集合里查找目标用户。如果你不想大改可以做一个轻量改动在main里预创建多个consumer对象按卡号查找后传给ATM。但更好的做法是给consumer增加一个bool check_identity(const char* num, const char* pwd)方法把校验封装进用户自己ATM只负责调用。这样ATM不再需要知道用户内部数据结构减少耦合。我倾向把集合用std::vectorconsumer存consumer类里加一个静态成员函数findByCard传入卡号返回迭代器。注意consumer原来拷贝构造函数里是多个char数组直接复制没问题但改成string以后更省心。5.3 增加异常处理和单日取款限额真实业务系统里余额不足不能直接返回一条错误就完事最好用异常来终止业务逻辑。我给fetchmoney加一层防护class InsufficientFunds : public std::exception { public: const char* what() const noexcept override { return 余额不足取款失败; } }; void ATM::fetchmoney() { if (m 0) { throw std::invalid_argument(取款金额必须为正数); } if (dailyLimit - todayWithdraw m) { throw InsufficientFunds(); } if (cnsm.get_money() m) { throw InsufficientFunds(); } todayWithdraw m; cnsm.set_money(m); }注意这里我新增了dailyLimit和todayWithdraw两个成员变量用来模拟每日取款上限。每次登录时todayWithdraw清零取款前先检查当天累计取款和本次金额之和是否超过上限。这个逻辑在原始代码里完全没有加进来后你会发现业务边界清晰了很多。6. 二次开发实战给ATM加一个转账功能要动哪些类转账是ATM核心业务里最自然的扩展点。在原始代码上实现它不需要大改架构只需要三步在consumer里增加转出方法在ATM里增加transferMoney方法在functionshow的菜单里加一个选项。先从consumer开始void consumer::transfer_out(float amount) { money - amount; } void consumer::transfer_in(float amount) { money amount; }这里我特意把拆成两个方法区分出账和入账避免用一个set_money做加减导致语义混乱。原代码的set_money只做减法现在加上transfer_in语义清晰多了。然后是ATM的转账方法。你需要接收目标卡号和转账金额先检查自己的余额是否足够再检查目标卡号是否存在。因为当前ATM只引用了一个consumer所以转账目标需要外部传入一个指向目标消费者的指针void ATM::transferMoney(consumer* target, float amount) { if (target nullptr) { cout 目标账户不存在转账失败 endl; return; } if (amount 0) { cout 转账金额必须为正数 endl; return; } if (cnsm.get_money() amount) { cout 余额不足转账失败 endl; return; } cnsm.transfer_out(amount); target-transfer_in(amount); cout 转账成功当前余额 cnsm.get_money() endl; }这里有一个我自己踩过的坑转账时一定要先检查余额再动手扣款和入账否则中间任何一步出错都会导致两边账不平。如果是真实线上系统这个操作应该放到数据库事务里要么都成功要么都回滚。模拟系统里至少要把三处逻辑写成一个不可分割的顺序块别在中间加任何可能return的路径。改完consumer和ATM再把functionshow的菜单从四项改成五项cout 1) 修改密码 endl 2) 取款 endl 3) 查询余额 endl 4) 转账 endl 5) 退出系统 endl;switch里加一个case 4调用transferMoney。注意原代码的退出系统是case 4你加了一个选项后要把退出改成case 5别让转账和退出重叠。改完后建议按这个顺序验证先登录查余额转账到另一个账户再查询余额确认扣款成功再登录目标账户确认入账。这条路径走通整个改造就没有大问题。我从这个项目里学到的习惯是改老代码前先把输入边界和状态计数列出来比如times会在哪几个函数被清零、会在哪几个函数被增加列完再动手。用户输入永远是最大的雷后来我每写一个带交互的C程序都会先处理一次cin.clear()和cin.ignore()再写业务逻辑。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站