C++模拟ATM取款机源码解析:面向对象设计与现代改造指南

发布时间:2026/10/24 18:28:50
C++模拟ATM取款机源码解析:面向对象设计与现代改造指南

简介:这份使用C++语言编写的模拟ATM取款机系统源代码,面向需要完成面向对象课程设计、或对银行系统模拟感兴趣的C++初中级学习者。程序围绕ATM和consumer两个核心类展开,完整实现用户登录、密码核对与修改、取款、余额查询、退出系统以及多次输错密码后的锁机功能;ATM类中的welcome()、check_passwd()、fetchmoney()等方法分工明确,consumer类封装了姓名、卡号、余额和私有密码,仅通过get_passwd()、set_money()等成员函数访问修改,构造函数负责初始化各项用户信息,清晰体现了封装和友元类的使用。压缩包内含1个docx文件,体积仅23KB,代码与函数实现均可直接查阅;目前已有272人学习过这份资料。这份代码提供了一个简洁的ATM系统雏形,类结构清楚、流程完整,适合作为课设基础进行扩展,比如加入每日取款限额、交易记录、非数字输入处理等安全机制。同时也能帮助初学者理解对象关联、数据封装等概念,结合需求分析与代码实现,可用于教学演示或个人实践。

1. 模拟 ATM 取款机的 C++ 源代码:面向对象不是语法,是分工

这份 C++ 模拟 ATM 取款机源代码,是我见过少数能把类和对象用到“肉眼可见”的完整例子。登录、核对密码、修改密码、取款、查询余额、退出、锁机,八项操作被清清楚楚地拆进 ATM 和 consumer 两个类里,方法名直接对应机器面板上的按键,读起来像在脑内运行一台取款机。它是课程设计里常见的小系统选题,适合正在学面向对象却缺一个完整项目的人,也适合用来给初学者讲类设计。同时它保留了一股古早味写法:老式头文件、void main、裸 char 数组,读得懂,但坑也不少——这两面性,恰恰是它最值得动手复现的原因。

2. 两个类的分工:ATM 持卡、consumer 持数据的设计逻辑

这份代码的核心不是业务有多复杂,而是“机器”和“人”被斩钉截铁地分成了两个类。ATM 负责操作流程,consumer 负责持有用户数据,两者通过引用和友元配合。理解了这个分工,后面读每个功能函数都会很顺。

2.1 consumer 类:用 get/set 包住四个私有字段

先看源码里 consumer 类的定义部分:

// function.h 中 consumer 类的定义 class consumer { public: friend class ATM; consumer(char Name[], char Num[], float Money, char Password[]); protected: char* get_name(); // 取得姓名 char* get_num(); // 取得卡号 char* get_passwd(); // 取得密码 float get_money(); // 取得余额 void set_passwd(char pwd[]); // 设置密码 void set_money(float m); // 取钱 private: char passwd[8]; // 用户密码 char name[20]; // 用户姓名 char num[20]; // 用户卡号 float money; // 用户余额 };

四个数据成员全部放在 private,外部想碰数据只能走方法,这是最基本的封装。细看有两个不太寻常的地方。第一,get_ 和 set_ 系列方法没有放在 public,而是放在 protected,也就是说外部函数其实拿不到这些数据的访问权,只有类自身和友元能调。第二,set_money 的方法名是“设置余额”,实现却是money -= m,它实际干的是扣款,不是赋值。读这份代码时,别被方法名带偏,看实现才是真的。

构造函数把名字、卡号、余额、密码按顺序拷进对象:

consumer::consumer(char Name[], char Num[], float Money, char Password[]) { strcpy(name, Name); strcpy(num, Num); money = Money; strcpy(passwd, Password); } void consumer::set_money(float m) { money -= m; }

构造参数顺序是姓名、卡号、余额、密码,main 里那句consumer c1("jim","12345",5200.3f,"123")正好对应。注意密码 passwd 只有 8 字节,输入超过 7 个字符就会越界,这个隐患先记下,第 5 章的常见问题部分专讲它。

2.2 ATM 类:一台只服务一个客户的取款机

ATM 类的定义同样在 function.h 里:

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; // 引用关联用户 };

构造函数参数是consumer&,成员也是一个consumer&,所以 ATM 对象从出生起就绑定了一个已存在的用户对象。这里用引用而不是指针,好处是调用方不用判空,坏处是 ATM 不能脱离 consumer 单独构造,必须先在 main 里造一个用户。times 成员是跨方法用的状态变量,welcome 和 change_passwd 都会用它记录密码错误次数,达到 3 次就触发 lock。

这 8 个成员函数的职责可以整理成一张表,读代码时对着看会清楚很多:

成员函数职责关键行为
welcome登录界面逐字符读取卡号和密码,失败计次
check_passwd核对密码strcmp 同时比较卡号与密码
functionshow功能菜单循环展示 1-4 操作,非法序号重输
change_passwd修改密码验旧密码 3 次、新密码双次确认
fetchmoney取款金额合法性检查加余额判断
information查询余额打印姓名、卡号、余额
exitATM退出系统打印提示后调用 exit(0)
lock锁机没收打印提示后调用 exit(1)

2.3 三个文件的组织方式:声明与实现分离

源码的自然组织方式是三个部分:function.h 放两个类的声明和 ATM 类的方法声明,consumer 的实现单独放一段,ATM 的实现和 main 函数放在一起。编译时是多个源文件协作:

g++ consumer.cpp ATM.cpp -o atm

两个 .cpp 文件都#include "function.h",各自编译成目标文件后链接。头文件的作用是让两个编译单元都能看到类定义,类的方法声明与实现分离后,改实现不需要重新编译调用方。很多初学者喜欢把所有代码堆在一个文件里,这份源码至少示范了正确的文件划分思路。

提示:如果下载到的是一段粘贴版代码,也可以全部放进一个 atm.cpp 里编译,只是失去多文件组织意义,不影响功能。

3. 核心流程逐行拆解:登录、改密、取款的运行路径

看懂了类结构,再走一遍核心功能。welcome 负责登录与状态流转,functionshow 是登录后的菜单中枢,fetchmoney 和 change_passwd 是两项关键操作。这三段代码把“状态机”的概念用小系统演示了一遍:密码错几次进入锁机、操作中选退出直接结束进程。

3.1 登录流程:三次错误触发锁机

welcome 是整个程序的入口:

void ATM::welcome() { times = 0; cout << "$ 欢迎使用若雪银行 ATM 自动取款机!~! " << endl; char pwd[8], num[20], ch; int i = 0; do { i = 0; cout << endl << "请输入卡号:"; do { cin.get(ch); num[i++] = ch; } while (ch != '\n'); num[i - 1] = '\0'; i = 0; cout << "请输入密码:"; do { cin.get(ch); pwd[i++] = ch; } while (ch != '\n'); pwd[i - 1] = '\0'; if (!check_passwd(num, pwd)) { cout << "你输入的卡号或密码有误,请重新输入" << endl; times++; } else { functionshow(); } } while (times < 3); lock(); }

读入部分用的是cin.get(ch)配合 do-while 循环,逐个字符读到回车为止。回车符'\n'会被放进数组里,所以紧接着有一行num[i - 1] = '\0'把换行符替换成字符串结束符。这样做的直接好处是输入可以带空格,但代价是数组长度没有上限检查,输入一长就写穿缓冲区。

外层 do-while 的条件是times < 3,意味着只有三次机会。三次都失败后循环退出,直接调 lock(),锁机后打印“你的卡已经被没收”并exit(1)终止进程。注意 times 在进入 welcome 时被清零,所以每次新开机都是三次机会重新计算。

check_passwd 的逻辑很直接:

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; }

strcmp 相等时返回 0,所以两个条件都是== 0才通过。这套系统里只有一个 consumer 对象,没有账户表,因此这里的 num 匹配本质上是和唯一客户的卡号比。想支持多用户,这个函数必须改成遍历账户容器,第 6 章会给改造方向。

3.2 取款逻辑:非法金额、余额不足、循环取款

fetchmoney 是业务逻辑最完整的一个函数:

void ATM::fetchmoney() { float m; char ch; do { cout << endl << "你要取多少钱:" << "\n$>\\" << endl; cin >> m; while (m <= 0) { cout << "请输入正确的数字!" << endl; cout << "$ >\\ "; cin >> m; } if (cnsm.get_money() - m < 0) { cout << "对不起,你的余额不足!" << endl; } else { cout << endl << "操作成功,请收好钱!" << endl; cnsm.set_money(m); } cout << "是否要继续该项操作:(Y/N) " << endl; cin >> ch; while (ch != 'n' && ch != 'N' && ch != 'Y' && ch != 'y') { cout << "$ >\\"; cin >> ch; } } while (ch == 'y' || ch == 'Y'); }

取款流程分三层。第一层排除非正数,m <= 0会一直卡住让用户重输,这里有个小问题:输入字母时 cin 会进入失败状态,m 保持旧值,于是无限循环,这是老代码里用cin >> float的经典毛病。第二层判断余额,用get_money() - m < 0而不是get_money() < m,结果一样,但前者留下了浮点减法的精度隐患。第三层才是真正扣款,set_money 内部做减法。

Y/N 确认循环写得比较完整,非法字符会被 while 卡住直到输入合法选项,然后外层用ch == 'y' || ch == 'Y'决定是否再来一轮。实际 ATM 场景里,这个确认环节通常是在出钞前而不是扣款后,但这版代码是先扣款再问要不要继续,设计上属于模拟简化。

3.3 修改密码:旧密码校验与新密码双重确认

change_passwd 的流程和登录有部分重叠:

void ATM::change_passwd() { char pwd[8], repwd[8]; times = 0; do { cout << endl << "请输入旧密码:"; cin >> pwd; if (!check_passwd(cnsm.get_num(), pwd)) times++; else break; } while (times < 3); if (times == 3) lock(); 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); cout << "密码修改成功,请牢记!" << endl; }

旧密码校验用的是 check_passwd,但第一个参数直接传cnsm.get_num(),相当于把卡号条件写死成恒真,只校验密码。这种写法少一次输入,但严格来说不够规范,真实场景应该让用户重新输入卡号或者单独留一个密码校验接口。新密码采用双次输入保证一致性,strcmp 非 0 就循环重来,直到两次完全一致才 set_passwd。

一个明显缺失是:新密码没有长度与复杂度限制,输一个字符也能当密码。此外旧密码与本轮输入的新密码完全相同也会通过,没有“新密码不能与旧密码相同”这类约束。模拟系统的定位可以接受,但写进真实产品会被审计打回。

4. 边界与输入校验:这版代码真正卡人的三个细节

很多人在复现这份源码时,功能能跑通,但一遇到异常输入就露馅。这三个细节决定了这个系统离“可用”还有多远。

4.1 逐字符读入的代价:回车截断与数组越界

welcome 里用cin.get(ch)逐字符读取直到回车,这个设计能读带空格的字符串,却带走了缓冲区安全。num 和 pwd 都是定长数组,内层循环没有任何长度限制,用户一旦输入超过 19 个字符的卡号,多余字符直接写到栈上相邻变量里。结果是程序开始出现各种“玄学”行为:有时是密码校验莫名通过,有时是 functionshow 菜单乱跳,严重时直接段错误。

回车截断那一行num[i - 1] = '\0'也有边界问题:如果用户不输入任何内容直接按回车,i 会变成 1,num[0] = '\0'得到空字符串,行为还算正常;但如果输入恰好达到数组容量,回车符写入数组末尾之外,结束符位置错乱,后面所有字符串操作都会失控。

4.2 浮点余额:5200.3f 的精度陷阱

main 里建账户用的是5200.3f,这是个典型的二进制浮点无法精确表示的数。float 底层是二进制小数,0.3 在二进制里是无限循环小数,存进内存时已经被截断。

consumer c1("jim", "12345", 5200.3f, "123"); // 余额 5200.3f

如果连续取款几次,余额的累减会产生肉眼可见的误差。比如取 0.1 元,余额可能变成 5200.199707 而不是 5200.2。对演示程序来说问题不大,但真实 ATM 系统绝不能用 float 存钱。通用做法是金额换算成整数“分”存储,显示时再除以 100。

4.3 菜单死循环与单用户模型

functionshow 的循环结构是所有功能的中枢:

void ATM::functionshow() { int n; do { // 打印菜单 cin >> n; while (n < 1 || n > 4) { cout << "请输入正确的操作序号!" << endl; cin >> n; } switch (n) { case 1: change_passwd(); break; case 2: fetchmoney(); break; case 3: information(); break; case 4: exitATM(); break; } } while (true); }

这是一个没有 break 出口的无限循环,退出完全依赖 exitATM 里的exit(0)。进程级退出不会触发栈上对象的析构函数,如果以后给 ATM 类加了文件句柄或动态内存,这里就是泄漏点。更合理的做法是让 functionshow 返回一个状态值,由 main 决定是否结束循环。

另外这套系统从头到尾只有一个 consumer 对象,welcome 里没有“查询用户表”的动作,任何其他卡号都登录不进去。多用户支持是这个项目走向“真实可用”的第一步,具体方案放到第 6 章。

5. 避坑:老式 C++ 代码最容易翻车的五个地方

这份代码最大的价值是能跑,最大的风险也是“能跑”背后的老式写法。以下五条是从实际编译和运行里踩出来的,按现象、原因、解决写清楚。

5.1 现代编译器直接报错:找不到 iostream.h

现象:下载源码后,用当前主流编译器执行g++ ATM.cpp,第一行就报fatal error: iostream.h: No such file or directory。

原因:iostream.h 是 90 年代非标准头文件,早已从标准库中移除。同理还有 string.h 和 stdlib.h,现代标准库对应的是<iostream>、<cstring>、<cstdlib>。

解决:把三个头文件替换成标准形式,并引入命名空间:

#include <iostream> #include <cstring> #include <cstdlib> using namespace std;

改完这三行,再执行g++ consumer.cpp ATM.cpp -o atm就能过编译。

5.2 void main 的警告或拒绝编译

现象:void main()在部分编译器下报错,在另一部分编译器下给出 warning,运行时退出码不稳定。

原因:C++ 标准规定的入口签名只有int main()和int main(int argc, char* argv[])。void 返回值属于历史遗留写法,主流工具链出于兼容性放行,但严格模式会直接拒绝。

解决:改成标准签名:

int main() { // 原有代码 return 0; }

5.3 超过 7 位密码导致程序行为错乱

现象:登录时输入 12345678(8 位),程序没有正常进入菜单,而是直接跳过或崩溃。改密码时输入超长字符串,甚至会出现“密码修改成功”但密码完全变掉。

原因:char passwd[8]只有 8 字节,strcpy 和读入循环都没有长度检查,超长输入写穿栈上数组,覆盖了相邻变量甚至返回地址。

解决:把字符数组全部替换成std::string,靠标准库自动管理长度。如果坚持用字符数组,读入时用cin.getline(pwd, 8)明确上限。

5.4 取款后余额出现小数点后一长串“怪数”

现象:取款 37.6 元后查询余额,显示 5162.699951 而不是 5162.7。

原因:float 用二进制近似表示十进制小数,0.6、0.3 这类数无法精确存储,减法时误差暴露。

解决:余额字段从 float 改成整数,单位用分。取款 37.6 元就是用户输入 3760 分,余额减 3760,显示时打印money / 100.0。

5.5 点退出系统后控制台一闪而过

现象:在菜单里选“4”退出系统,窗口瞬间关闭,连“欢迎下次光临”都看不清。

原因:exitATM()里调用了exit(0),进程立即终止,没有给用户留出查看输出的时间,也不会运行局部对象的析构。

解决:把退出改成返回值传播。ATM 类的方法都返回状态码,main 里循环判断退出条件,最后走正常 return 结束流程。如果是练习环境急着看输出,至少也在return 0前加一句system("pause")或cin.get()挂着屏幕。

6. 改造成现代 C++:把单用户 ATM 变成一个账户管理系统

如果只停留在“能跑”,这份代码就是一段课程作业;真想让它有实用价值,我建议按三步改造,每一步都有明确的收益。

第一步,字符数组全部换成 std::string。strcpy、strcmp、get_ 系列方法全部改成 string 版本,直接消灭缓冲区溢出这一类问题,密码字段不再有 8 字节上限。第二步,把单用户 consumer 换成一个账户容器,main 里维护一个std::vector<consumer>,welcome 登录时遍历匹配卡号和密码。第三步,金额换成整数分,加每日取款限额和交易流水数组。

三段改造合并后,登录部分的示意长这样:

int main() { std::vector<consumer> users; users.push_back(consumer("jim", "12345", 520030, "123")); std::string num, pwd; std::cout << "请输入卡号:"; std::cin >> num; std::cout << "请输入密码:"; std::cin >> pwd; auto it = std::find_if(users.begin(), users.end(), [&](consumer& u) { return num == u.get_num() && pwd == u.get_passwd(); }); if (it != users.end()) { ATM atm(*it); atm.run(); // 改造后的主循环 } else { std::cout << "卡号或密码错误" << std::endl; } return 0; }

这里的金额 520030 表示 5200.30 元,get_num、get_passwd 在改造后返回 string,配合 find_if 做精确匹配。new 出来的 ATM 对象在作用域结束自动析构,不再需要 exitATM 里那记粗暴的 exit(0)。

再往后还可以加每日累计取款、操作日志、密码错误锁定后冻结时间,这些都是在 consumer 里加字段、在 ATM 对应方法里加校验的事。每次拿到这种老代码,我都先做一遍三件套:字符数组换 string、void main 换 int main、进程级 exit 改成返回值,这三处改完再谈功能扩展。这次拆这份 ATM 源码,也从“能跑”走到了“敢拿出去讲清楚每条边界”的状态。希望帮到你。

本文还有配套的精品资源,点击获取