银行管理系统的业务血肉 —— 流程、状态机、输入校验与持久化(下篇)

零、上篇回顾,下篇起点

上篇我们建立了银行系统的骨架:结构体、动态数组、扩容、伪删除、验证。

但一个银行系统如果只能"创建账户"和"冻结账户",那它连玩具都算不上。

下篇要解决的问题

  1. 存款、取款、查询余额 ------ 如何优雅地复用验证逻辑?

  2. 菜单与流程控制 ------ 为什么 do-while 是菜单的最佳选择?

  3. 输入校验 ------ 用户输入 -100 元怎么办?输入 abc 呢?

  4. 账号唯一性 ------ 100000 + count 为什么是错的?

  5. 文件持久化 ------ 程序关了,数据还在吗?

  6. 资源释放 ------ 为什么 free 后还要置 NULL

这些问题,每一个都是 C 语言面试的高频考点。


一、存款、取款、查询 ------ 验证的复用

上篇我们写了 verifyAccount,它返回账户下标。

存款、取款、查询的本质是:

  1. 验证账户

  2. 拿到下标

  3. 操作余额

存款实现

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 防御野指针

上下两篇合起来,就是一个完整的银行管理系统。

相关推荐
foundbug9992 小时前
自适应滤除直达波干扰的MATLAB实现
开发语言·算法·matlab
爱编码的小八嘎3 小时前
C语言完美演绎9-12
c语言
CN-Dust3 小时前
【C++】while语句例题专题
数据结构·c++·算法
灵智实验室4 小时前
PX4位置速度估计技术详解(四):LPE 激光雷达高度融合的实现错误
算法·无人机·px 4
CQU_JIAKE4 小时前
【A】3742,3387,并查集
算法
gihigo19984 小时前
CHAN时延估计算法(二维/三维定位实现)
算法
freexyn4 小时前
Matlab自学笔记七十六:表达式的展开、因式分解、化简、合并同类项
笔记·算法·matlab
样例过了就是过了4 小时前
LeetCode热题 不同路径
c++·算法·leetcode·动态规划
dog2505 小时前
圆锥曲线和二次曲线
开发语言·网络·人工智能·算法·php