ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C语言课程设计ATM机源代码:从功能清单到可运行系统的完整实现

C语言课程设计ATM机源代码:从功能清单到可运行系统的完整实现

简介:这份C语言课程设计ATM机源代码面向高校计算机相关专业学生及C语言初学者,用于完成课程设计或巩固编程基础。资源包共3个文件,包含1个cpp源文件与2个txt文本文件,压缩包体积约2KB,其中cpp文件承载ATM核心业务逻辑,txt文件用于存储账户名、密码及客户信息等数据,结构精简,便于直接编译运行与二次修改。代码完整实现了登录验证、余额查询、取款、转账和存款五大功能模块,并对取款额不得超过存款余额等业务规则做了处理,适合作为课程设计参考模板或C语言综合练习素材。目前已有579人学习下载,读者可借此理解文件读写、结构体、函数封装与菜单交互等知识点,快速搭建可运行的ATM模拟程序,并在此基础上扩展修改密码、交易记录查询等功能。

1. 从一份“能跑起来”的ATM机课设说起

每年一到学期中后段,C语言课程设计的选题里,ATM机系统的出现频率几乎是最高的。原因不复杂:它同时踩中了文件操作、结构体、循环与分支、函数封装这几个C语言核心考点,又不像图书管理、学生成绩管理那样被写烂到毫无区分度。但真正动手写的时候,大部分同学会卡在同一个地方——功能列表能列出来,代码却拼不成一个能连续操作、数据能持久保存、退出再进还能查到余额的完整程序。

这份C语言课程设计ATM机源代码要解决的,就是“从功能清单到可运行系统”之间的断层。它面向的是正在做课设但不知道如何组织多文件、如何设计账户数据结构、如何用文件做持久化的学生,也适合需要一份结构清晰、能直接改造成自己题目的参考实现的开发者。整套代码围绕账户管理、存取款、转账、查询、修改密码、交易记录这几个模块展开,用结构体描述账户,用二进制文件保存数据,用函数分模块组织逻辑,最终交付一个能在控制台连续操作、退出后数据不丢失的完整程序。

2. 需求拆解与数据结构设计:账户模型和文件存储怎么定

2.1 功能边界:哪些必须做,哪些可以砍

课程设计的ATM机系统,功能不是越多越好,而是要在有限篇幅里把核心链路跑通。通常的做法是先把功能分成“必做”和“选做”两层。必做部分包括:开户(新增账户)、登录验证、余额查询、存款、取款、转账、修改密码、退出。选做部分包括:交易记录查询、账户注销、管理员查看所有账户、按账号排序输出。

砍功能的原则是看它是否依赖新的数据结构或新的文件操作。比如“交易记录查询”需要额外维护一个流水文件,工作量翻倍,如果课设周期只有两周,建议放到选做。而“转账”虽然涉及两个账户的读写,但本质上还是存款和取款的组合,属于必做范围。

这里有一个容易被忽略的点:登录失败次数限制。很多同学不写这个,答辩时老师一问“别人暴力试密码怎么办”,就答不上来。通常的做法是设置三次失败锁定,锁定状态可以存在账户结构体里,也可以只在当前运行周期内有效。如果要求持久化锁定,就需要在账户文件里加一个状态字段。

2.2 账户结构体:字段怎么定才够用

账户结构体是整个系统的数据核心,字段少了不够用,字段多了浪费空间。下面这份定义是我在多个课设版本里反复调整后比较稳定的一版:

// account.h #ifndef ACCOUNT_H #define ACCOUNT_H #define MAX_NAME 20 #define MAX_PWD 16 #define MAX_ACCOUNTS 1000 // 账户状态枚举 typedef enum { STATUS_NORMAL = 0, // 正常 STATUS_LOCKED = 1, // 锁定 STATUS_CLOSED = 2 // 已注销 } AccountStatus; // 账户结构体 typedef struct { char accountNo[20]; // 账号,唯一标识 char name[MAX_NAME]; // 户名 char password[MAX_PWD]; // 密码,明文存储仅用于课设演示 double balance; // 余额,单位:元 AccountStatus status; // 账户状态 int failCount; // 连续登录失败次数 char createTime[20]; // 开户时间,格式 YYYY-MM-DD HH:MM:SS } Account; #endif

账号用字符串而不是整数,是为了支持前导零和固定位数,比如“10000001”这种格式。密码在课设里通常明文存储,但答辩时如果被问到安全性,要能说出“生产环境应该存哈希值,课设为了演示简化了”。余额用double,虽然浮点数有精度问题,但课设场景下金额不大,通常可以接受;如果要求严格,可以改用分作为单位的整数。

状态字段和失败次数字段是很多同学会漏掉的。加上它们之后,锁定逻辑和注销逻辑才有地方落脚,不用额外维护全局变量。

2.3 文件存储方案:二进制还是文本

数据持久化有两种选择:二进制文件和文本文件。二进制文件读写快、结构体可以直接整体写入,但可读性差,调试时看不到内容。文本文件可读性好,但解析麻烦,需要自己处理分隔符和换行。

课设场景下,我一般推荐二进制文件。原因是代码量少,用fwrite和fread直接操作结构体数组,不需要写解析函数。下面是一个典型的加载和保存实现:

// file_ops.c #include <stdio.h> #include <string.h> #include "account.h" #define DATA_FILE "accounts.dat" // 从文件加载所有账户到数组,返回实际数量 int loadAccounts(Account accounts[], int maxCount) { FILE *fp = fopen(DATA_FILE, "rb"); if (fp == NULL) { // 文件不存在时返回0,表示首次运行 return 0; } int count = fread(accounts, sizeof(Account), maxCount, fp); fclose(fp); return count; } // 将账户数组保存到文件,返回写入数量 int saveAccounts(Account accounts[], int count) { FILE *fp = fopen(DATA_FILE, "wb"); if (fp == NULL) { printf("错误:无法写入数据文件。\n"); return -1; } int written = fwrite(accounts, sizeof(Account), count, fp); fclose(fp); return written; }

加载时如果文件不存在,返回0,主程序据此判断是首次运行,可以初始化一个默认管理员账户。保存时用"wb"模式,每次全量覆盖写入。这种方案的缺点是每次操作都要全量读写,账户数量大时效率低,但课设场景下账户数量通常不超过几百,完全够用。

注意:二进制文件在不同编译器或平台之间可能存在对齐差异,如果换环境后读出来的数据乱码,优先检查结构体是否有未初始化的填充字节。可以在结构体定义后加#pragma pack(1)强制紧凑排列,但会牺牲一点性能。

3. 核心模块实现:登录、存取款与转账的函数拆分

3.1 登录验证:三次锁定与状态检查

登录模块看起来简单,但要把失败计数、锁定状态、账户查找三件事串起来,代码就容易写乱。我通常把它拆成两个函数:一个负责查找账户,一个负责验证密码并更新失败计数。

// auth.c #include <string.h> #include "account.h" // 按账号查找账户,返回数组下标,找不到返回-1 int findAccount(Account accounts[], int count, const char *accountNo) { for (int i = 0; i < count; i++) { if (strcmp(accounts[i].accountNo, accountNo) == 0) { return i; } } return -1; } // 登录验证,成功返回账户下标,失败返回-1 int login(Account accounts[], int count, const char *accountNo, const char *password) { int idx = findAccount(accounts, count, accountNo); if (idx == -1) { printf("账号不存在。\n"); return -1; } if (accounts[idx].status == STATUS_LOCKED) { printf("该账户已被锁定,请联系管理员。\n"); return -1; } if (accounts[idx].status == STATUS_CLOSED) { printf("该账户已注销。\n"); return -1; } if (strcmp(accounts[idx].password, password) != 0) { accounts[idx].failCount++; if (accounts[idx].failCount >= 3) { accounts[idx].status = STATUS_LOCKED; printf("密码连续错误三次,账户已锁定。\n"); } else { printf("密码错误,还剩 %d 次机会。\n", 3 - accounts[idx].failCount); } return -1; } // 登录成功,重置失败计数 accounts[idx].failCount = 0; return idx; }

这里的关键设计是:失败计数存在账户结构体里,登录成功时清零。这样即使程序退出再进入,锁定状态依然有效,因为状态已经写入了文件。如果课设不要求持久化锁定,可以把failCount改成局部静态变量,但那样换一次运行就重置了,答辩时容易被质疑。

3.2 存款与取款:金额校验和余额更新

存款和取款的逻辑对称,但校验规则不同。存款只需要校验金额是否为正数,取款还要校验余额是否充足。我一般把这两个操作写成独立函数,参数是账户指针和金额,返回操作结果码。

// transaction.c #include <stdio.h> #include "account.h" #define RESULT_OK 0 #define RESULT_INVALID_AMT 1 #define RESULT_NO_BALANCE 2 // 存款,返回结果码 int deposit(Account *acc, double amount) { if (amount <= 0) { return RESULT_INVALID_AMT; } acc->balance += amount; return RESULT_OK; } // 取款,返回结果码 int withdraw(Account *acc, double amount) { if (amount <= 0) { return RESULT_INVALID_AMT; } if (amount > acc->balance) { return RESULT_NO_BALANCE; } acc->balance -= amount; return RESULT_OK; }

用结果码而不是直接printf,是为了把业务逻辑和界面输出分离。这样在转账函数里可以复用取款和存款,不用重复写校验。调用方根据返回码决定打印什么提示,代码更干净。

转账的实现就是先对转出账户调用withdraw,成功后再对转入账户调用deposit。如果转入账户不存在,需要把转出的钱加回去,这就是事务回滚的雏形。课设里可以不写完整的事务日志,但至少要处理“转入账户不存在”的情况,否则钱就凭空消失了。

3.3 主循环与菜单:状态机式的流程控制

主程序的结构通常是一个while循环加switch分支,但直接写容易变成“面条代码”。我习惯把界面分成两层:未登录时的主菜单和登录后的操作菜单。用一个状态变量控制当前处于哪一层。

// main.c #include <stdio.h> #include "account.h" void showMainMenu() { printf("\n===== ATM 系统 =====\n"); printf("1. 开户\n"); printf("2. 登录\n"); printf("3. 退出\n"); printf("请选择:"); } void showUserMenu(const char *name) { printf("\n===== 欢迎,%s =====\n", name); printf("1. 查询余额\n"); printf("2. 存款\n"); printf("3. 取款\n"); printf("4. 转账\n"); printf("5. 修改密码\n"); printf("6. 返回主菜单\n"); printf("请选择:"); } int main() { Account accounts[MAX_ACCOUNTS]; int count = loadAccounts(accounts, MAX_ACCOUNTS); int choice; int currentUser = -1; // -1 表示未登录 while (1) { if (currentUser == -1) { showMainMenu(); scanf("%d", &choice); switch (choice) { case 1: /* 开户逻辑 */ break; case 2: /* 登录逻辑,成功则 currentUser = idx */ break; case 3: saveAccounts(accounts, count); return 0; default: printf("无效选择。\n"); } } else { showUserMenu(accounts[currentUser].name); scanf("%d", &choice); switch (choice) { case 1: /* 查询余额 */ break; case 2: /* 存款 */ break; case 3: /* 取款 */ break; case 4: /* 转账 */ break; case 5: /* 修改密码 */ break; case 6: currentUser = -1; break; default: printf("无效选择。\n"); } } } return 0; }

这个骨架把登录态和未登录态分开处理,每个case里只写业务调用,不混界面逻辑。实际写的时候,每个case里会调用前面定义的函数,然后根据返回码打印结果。退出时记得调用saveAccounts,否则数据全丢。

提示:scanf读取整数时如果输入了字母,会陷入死循环。课设里通常不要求处理这种异常输入,但如果想稳一点,可以在scanf后加while(getchar() != '\n');清空缓冲区。

4. 避坑与常见问题:课设里最容易翻车的五个点

4.1 文件读写后数据错乱

现象:存了三个账户,重新打开程序后只读出一个,或者读出来的姓名和余额对不上。

原因:fwrite和fread是按结构体大小整块读写的,如果结构体里有指针成员,或者写入和读取时的结构体定义不一致(比如一个用了#pragma pack另一个没用),就会错位。

解决:确保读写两端使用同一个头文件里的结构体定义。不要在结构体里放指针,字符串一律用字符数组。如果换编译器后出问题,检查两边编译选项是否一致。

4.2 登录后操作影响到了别的账户

现象:A登录后存款,B登录后发现余额也变了。

原因:函数传参时传了账户数组的副本,或者用了全局变量导致所有操作都作用在同一个账户上。

解决:登录成功后保存的是账户在数组中的下标,所有操作通过accounts[currentUser]访问。传参时传Account *指针,不要传Account值。如果用了全局数组,确保下标正确。

4.3 转账后钱对不上

现象:A转给B一百块,A扣了钱,B没收到,或者B收到了但A没扣。

原因:转账逻辑写成了先加后减,或者转入账户查找失败后没有回滚。

解决:先执行转出账户的扣款,扣款成功后再查找转入账户。如果转入账户不存在,立即把扣款加回去。顺序不能反,否则会出现“钱没扣但对方收到了”的情况。

4.4 密码修改后旧密码还能登录

现象:修改密码成功提示了,退出再用旧密码登录,居然还能进。

原因:修改密码只改了内存里的结构体,没有调用saveAccounts写回文件。或者写回了文件,但登录时读的是另一份缓存数据。

解决:任何修改账户数据的操作之后,都要调用一次saveAccounts。课设里数据量小,全量写入最简单。不要试图只写单个账户,偏移量计算容易出错。

4.5 菜单输入非数字导致死循环

现象:在主菜单输入字母,程序疯狂刷屏“无效选择”。

原因:scanf读取整数失败后,非法字符留在缓冲区里,下一次循环又读到同一个字符,无限重复。

解决:在scanf后检查返回值,如果返回值不是1,说明读取失败,用while(getchar() != '\n');清空缓冲区。这个处理加一次,所有scanf的地方都受益。

5. 验收与答辩:怎么证明你的ATM机真的能跑

5.1 验收清单:逐项打勾再交

答辩前自己走一遍下面这个清单,每项都手动操作一次,不要只看代码:

序号验收项操作步骤预期结果
1开户输入账号、姓名、密码提示成功,文件大小增加
2重复开户用已存在的账号再开户提示账号已存在
3登录成功用正确账号密码登录进入用户菜单
4登录失败锁定连续输错三次密码提示锁定,再登录被拒
5存款存入100元余额增加100
6取款超额取款金额大于余额提示余额不足
7转账A转给B 50元A减50,B加50
8转账到不存在账户转给一个不存在的账号提示失败,A余额不变
9修改密码改密码后退出重登旧密码失效,新密码可登录
10数据持久化退出程序再打开所有账户和余额与退出前一致

这张表可以直接打印出来,答辩时放在手边。老师问“你怎么证明数据没丢”,你就把第10项演示一遍。

5.2 答辩时被问到的三个高频问题

第一个问题通常是“你的密码为什么明文存储”。回答要点:课设环境为了简化演示,生产环境应该用哈希加盐,可以提一句MD5或SHA-256,但不要展开,否则容易被追问实现细节。

第二个问题是“如果两个账户同时操作会怎样”。课设是单用户控制台程序,不存在并发。但如果老师追问,可以说“当前是单线程模型,如果扩展到多用户需要加文件锁或数据库事务”。

第三个问题是“你的代码有多少行,哪些是你自己写的”。提前统计好行数,把核心模块的文件名和行数报出来。如果参考了网上代码,要能说清楚自己改了哪些部分,比如“文件存储部分参考了二进制读写的通用写法,但账户结构体和锁定逻辑是自己设计的”。

5.3 一个让答辩加分的习惯

我每次带课设都会让学生做一件事:在代码里加一个隐藏的“调试模式”。启动时如果带参数-debug,就打印当前账户数量、文件路径和内存中所有账户的摘要信息。这个功能不写在菜单里,但答辩时如果老师质疑数据不对,你可以现场启动调试模式,把内存状态打出来自证。

// 在main函数开头加 if (argc > 1 && strcmp(argv[1], "-debug") == 0) { printf("[调试] 数据文件:%s\n", DATA_FILE); printf("[调试] 已加载账户数:%d\n", count); for (int i = 0; i < count; i++) { printf("[调试] 账号=%s 户名=%s 余额=%.2f 状态=%d\n", accounts[i].accountNo, accounts[i].name, accounts[i].balance, accounts[i].status); } }

这个习惯是我从第一次带课设翻车之后养成的。当时有个学生答辩时被问到“你怎么知道文件里存了几条记录”,他答不上来,场面很尴尬。从那以后我每次做涉及文件持久化的项目,都强制加一个调试输出入口,不占多少代码,但关键时刻能救命。希望帮到你。

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

返回列表