零、上篇回顾,下篇起点
上篇我们建立了银行系统的骨架:结构体、动态数组、扩容、伪删除、验证。
但一个银行系统如果只能"创建账户"和"冻结账户",那它连玩具都算不上。
下篇要解决的问题:
-
存款、取款、查询余额 ------ 如何优雅地复用验证逻辑?
-
菜单与流程控制 ------ 为什么
do-while是菜单的最佳选择? -
输入校验 ------ 用户输入
-100元怎么办?输入abc呢? -
账号唯一性 ------
100000 + count为什么是错的? -
文件持久化 ------ 程序关了,数据还在吗?
-
资源释放 ------ 为什么
free后还要置NULL?
这些问题,每一个都是 C 语言面试的高频考点。
一、存款、取款、查询 ------ 验证的复用
上篇我们写了 verifyAccount,它返回账户下标。
存款、取款、查询的本质是:
-
验证账户
-
拿到下标
-
操作余额
存款实现
cpp
void deposit(BankSystem* bank) {
int idx = verifyAccount(bank);
if (idx == -1) return; // 验证失败,直接返回
double money;
printf("请输入存款金额:");
scanf("%lf", &money);
// 输入校验:金额必须为正
if (money <= 0) {
printf("存款金额必须大于0\n");
return;
}
bank->accounts[idx].balance += money;
printf("存款成功!当前余额:%.2f\n", bank->accounts[idx].balance);
}
取款实现
cpp
void withdraw(BankSystem* bank) {
int idx = verifyAccount(bank);
if (idx == -1) return;
double money;
printf("请输入取款金额:");
scanf("%lf", &money);
// 双重校验:正数 + 余额充足
if (money <= 0) {
printf("取款金额必须大于0\n");
return;
}
if (money > bank->accounts[idx].balance) {
printf("余额不足!当前余额:%.2f\n", bank->accounts[idx].balance);
return;
}
bank->accounts[idx].balance -= money;
printf("取款成功!当前余额:%.2f\n", bank->accounts[idx].balance);
}
查询余额
cpp
void queryBalance(BankSystem* bank) {
int idx = verifyAccount(bank);
if (idx == -1) return;
printf("账号:%lld 当前余额:%.2f\n",
bank->accounts[idx].accountNumber,
bank->accounts[idx].balance);
}
设计原则:
-
验证逻辑只写一次,到处复用
-
每个业务的校验(金额正负、余额充足)各自负责
二、菜单系统 ------ do-while 的经典用法
cpp
void menu(BankSystem* bank) {
int choice;
do {
printf("\n====== 银行管理系统 ======\n");
printf("1. 创建账户\n");
printf("2. 存款\n");
printf("3. 取款\n");
printf("4. 查询余额\n");
printf("5. 冻结账户\n");
printf("6. 退出系统\n");
printf("请选择:");
scanf("%d", &choice);
switch (choice) {
case 1: createAccount(bank); break;
case 2: deposit(bank); break;
case 3: withdraw(bank); break;
case 4: queryBalance(bank); break;
case 5: freezeAccount(bank); break;
case 6: printf("感谢使用\n"); break;
default: printf("无效选项,请重新输入\n");
}
} while (choice != 6);
}
为什么用 do-while 而不是 while?
-
菜单至少显示一次
-
先显示,再输入,再判断是否退出
-
do-while天然适合这种"至少执行一次"的场景
switch 的注意事项:
-
每个
case末尾要break,否则会"穿透" -
故意穿透有时用于合并逻辑(如 case 1 和 case 2 共用代码)
三、输入校验 ------ 用户不按套路出牌怎么办?
问题 1:负数金额
我们已经用 if (money <= 0) 拦截了。
问题 2:用户输入 abc 而不是数字
scanf("%lf", &money) 会返回 0(表示没读到有效数字),但 money 的值不确定。
更健壮的写法:
cpp
double getPositiveAmount(const char* prompt) {
double money;
int ret;
while (1) {
printf("%s", prompt);
ret = scanf("%lf", &money);
// 清理输入缓冲区(关键!)
while (getchar() != '\n');
if (ret != 1 || money <= 0) {
printf("输入无效,请输入正数\n");
} else {
return money;
}
}
}
为什么需要 while (getchar() != '\n')?
-
用户输入
abc后,abc还留在输入缓冲区 -
下次
scanf会再次读到abc,形成死循环 -
清空缓冲区是必须的
四、账号唯一性 ------ 100000 + count 为什么是错的?
上篇我们用:
cpp
newAcc->accountNumber = 100000 + bank->count;
问题:
-
删除账户后,
count不变,但账号不会回收 -
这倒也没大问题(账号不用回收)
-
真正的问题是:多线程/分布式下不唯一
更好的方案:时间戳
cpp
#include <time.h>
long long generateAccountNumber() {
// 秒级时间戳(未来可加随机数)
return (long long)time(NULL);
}
注意 :time(NULL) 返回秒数,约 1.7e9,仍在 long long 范围内。
再进一步:时间戳 + 自增序列
cpp
static long long _seq = 0;
long long generateAccountNumber() {
return ((long long)time(NULL) * 1000) + (++_seq % 1000);
}
这样同一秒内也能生成不同账号。
五、文件持久化 ------ 数据不能丢
银行系统如果每次重启数据都清空,那就是个笑话。
保存数据到文件
cpp
void saveToFile(BankSystem* bank, const char* filename) {
FILE* fp = fopen(filename, "wb");
if (fp == NULL) {
printf("保存失败\n");
return;
}
// 先写元数据(账户数量)
fwrite(&bank->count, sizeof(int), 1, fp);
// 再写所有账户
fwrite(bank->accounts, sizeof(Account), bank->count, fp);
fclose(fp);
printf("数据已保存\n");
}
从文件加载数据
cpp
void loadFromFile(BankSystem* bank, const char* filename) {
FILE* fp = fopen(filename, "rb");
if (fp == NULL) {
printf("未找到存档,将创建新系统\n");
return;
}
// 先读账户数量
int count;
fread(&count, sizeof(int), 1, fp);
// 确保容量足够
if (count > bank->capacity) {
// 需要扩容
Account* newMem = (Account*)realloc(bank->accounts,
count * sizeof(Account));
if (newMem == NULL) {
printf("加载失败\n");
fclose(fp);
return;
}
bank->accounts = newMem;
bank->capacity = count;
}
// 读账户数据
fread(bank->accounts, sizeof(Account), count, fp);
bank->count = count;
fclose(fp);
printf("加载成功,共 %d 个账户\n", bank->count);
}
"wb" / "rb" 是二进制模式:
-
直接写内存字节,速度快
-
但不同平台间不兼容(
long long大小、字节序) -
跨平台需要用文本格式(JSON、CSV)
六、资源释放 ------ 为什么 free 后还要置 NULL?
cpp
void freeBankSystem(BankSystem* bank) {
if (bank->accounts != NULL) {
free(bank->accounts);
bank->accounts = NULL; // 关键!
}
bank->count = 0;
bank->capacity = 0;
}
如果不置 NULL:
cpp
free(bank->accounts);
// 这里 bank->accounts 仍然指向一块已释放的内存(野指针)
// 如果后续代码不小心再 free 一次 → 双重释放 → 崩溃
规则:
free 之后,指针立即置 NULL。
这不是多此一举,这是防御性编程。
七、完整的下篇代码
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <stdbool.h>
#include <time.h>
#define MAX_ACCOUNT 10
#define PWD_LEN 7
#define DATA_FILE "bank.dat"
typedef struct Account {
long long accountNumber;
char password[PWD_LEN];
double balance;
int isActive;
} Account;
typedef struct BankSystem {
Account* accounts;
int count;
int capacity;
} BankSystem;
// ==================== 辅助函数 ====================
void printAccount(const Account* p) {
printf("账号:%lld | 余额:%.2f | 状态:%s\n",
p->accountNumber, p->balance, p->isActive ? "活跃" : "冻结");
}
// 生成唯一账号(时间戳 + 序列)
long long generateAccountNumber() {
static long long seq = 0;
return (long long)time(NULL) * 1000 + (++seq % 1000);
}
// 安全输入正数金额
double getPositiveAmount(const char* prompt) {
double money;
int ret;
while (1) {
printf("%s", prompt);
ret = scanf("%lf", &money);
while (getchar() != '\n'); // 清空缓冲区
if (ret == 1 && money > 0) {
return money;
}
printf("输入无效,请输入正数\n");
}
}
// ==================== 内存管理 ====================
void initBankSystem(BankSystem* bank) {
assert(bank != NULL);
bank->capacity = MAX_ACCOUNT;
bank->count = 0;
bank->accounts = (Account*)malloc(bank->capacity * sizeof(Account));
if (bank->accounts == NULL) {
printf("内存分配失败\n");
exit(1);
}
}
bool expandBankSystem(BankSystem* bank) {
int newCap = bank->capacity * 2;
Account* newMem = (Account*)realloc(bank->accounts, newCap * sizeof(Account));
if (newMem == NULL) {
printf("扩容失败\n");
return false;
}
bank->accounts = newMem;
bank->capacity = newCap;
return true;
}
void freeBankSystem(BankSystem* bank) {
if (bank->accounts) {
free(bank->accounts);
bank->accounts = NULL;
}
bank->count = 0;
bank->capacity = 0;
}
// ==================== 持久化 ====================
void saveToFile(BankSystem* bank) {
FILE* fp = fopen(DATA_FILE, "wb");
if (!fp) return;
fwrite(&bank->count, sizeof(int), 1, fp);
fwrite(bank->accounts, sizeof(Account), bank->count, fp);
fclose(fp);
}
void loadFromFile(BankSystem* bank) {
FILE* fp = fopen(DATA_FILE, "rb");
if (!fp) return;
int count;
fread(&count, sizeof(int), 1, fp);
if (count > bank->capacity) {
Account* newMem = (Account*)realloc(bank->accounts, count * sizeof(Account));
if (!newMem) {
fclose(fp);
return;
}
bank->accounts = newMem;
bank->capacity = count;
}
fread(bank->accounts, sizeof(Account), count, fp);
bank->count = count;
fclose(fp);
}
// ==================== 业务逻辑 ====================
int verifyAccount(BankSystem* bank) {
long long num;
char pwd[PWD_LEN];
printf("请输入账号:");
scanf("%lld", &num);
printf("请输入密码:");
scanf("%s", pwd);
for (int i = 0; i < bank->count; i++) {
if (bank->accounts[i].accountNumber == num &&
strcmp(bank->accounts[i].password, pwd) == 0 &&
bank->accounts[i].isActive == 1) {
return i;
}
}
printf("验证失败\n");
return -1;
}
void createAccount(BankSystem* bank) {
if (bank->count == bank->capacity) {
if (!expandBankSystem(bank)) return;
}
Account* acc = &bank->accounts[bank->count];
acc->accountNumber = generateAccountNumber();
printf("设置6位数字密码:");
scanf("%s", acc->password);
if (strlen(acc->password) != 6) {
printf("密码必须6位\n");
return;
}
acc->balance = 0.0;
acc->isActive = 1;
bank->count++;
printf("创建成功!账号:%lld\n", acc->accountNumber);
saveToFile(bank);
}
void deposit(BankSystem* bank) {
int idx = verifyAccount(bank);
if (idx == -1) return;
double money = getPositiveAmount("存款金额:");
bank->accounts[idx].balance += money;
printf("存款成功,余额:%.2f\n", bank->accounts[idx].balance);
saveToFile(bank);
}
void withdraw(BankSystem* bank) {
int idx = verifyAccount(bank);
if (idx == -1) return;
double money = getPositiveAmount("取款金额:");
if (money > bank->accounts[idx].balance) {
printf("余额不足\n");
return;
}
bank->accounts[idx].balance -= money;
printf("取款成功,余额:%.2f\n", bank->accounts[idx].balance);
saveToFile(bank);
}
void queryBalance(BankSystem* bank) {
int idx = verifyAccount(bank);
if (idx == -1) return;
printf("余额:%.2f\n", bank->accounts[idx].balance);
}
void freezeAccount(BankSystem* bank) {
int idx = verifyAccount(bank);
if (idx == -1) return;
bank->accounts[idx].isActive = 0;
printf("账户已冻结\n");
saveToFile(bank);
}
// ==================== 菜单 ====================
int main() {
BankSystem bank;
initBankSystem(&bank);
loadFromFile(&bank);
int choice;
do {
printf("\n1.创建 2.存款 3.取款 4.查询 5.冻结 6.退出\n");
printf("请选择:");
scanf("%d", &choice);
switch (choice) {
case 1: createAccount(&bank); break;
case 2: deposit(&bank); break;
case 3: withdraw(&bank); break;
case 4: queryBalance(&bank); break;
case 5: freezeAccount(&bank); break;
case 6: printf("再见\n"); break;
default: printf("无效\n");
}
} while (choice != 6);
freeBankSystem(&bank);
return 0;
}
下篇总结
| 知识点 | 为什么重要 |
|---|---|
| 复用验证逻辑 | DRY 原则 |
do-while 菜单 |
至少执行一次 |
| 输入缓冲区清空 | 防止死循环 |
| 时间戳生成账号 | 解决唯一性问题 |
| 二进制文件读写 | 数据持久化 |
free 后置 NULL |
防御野指针 |
上下两篇合起来,就是一个完整的银行管理系统。