47 C 语言实战项目——家庭收支记账软件

目录

1 需求说明

1.1 菜单显示

1.2 登记收入

1.3 登记支出

1.4 显示收支明细

1.5 退出

2 流程分析

2.1 总流程图

2.2 登记收入流程图

2.3 登记支出流程图

2.4 收支明细流程图

2.5 退出流程图

3 代码实现

3.1 框架搭建

3.2 收支明细功能

3.3 登记收入功能

3.4 登记支出功能

3.5 退出功能

4 安全升级

5 代码升级

5.1 抽取结构体

5.2 使用函数封装相应功能


1 需求说明

1.1 菜单显示

        进入系统,展示主菜单,输入各功能对应的数字编号选择要进行的操作,如下图:

1.2 登记收入

        输入2,进入 “登记收入”,需输入“本次收入金额” 和 “本次收入说明”,输入完成后会再次进入主菜单。

1.3 登记支出

        输入 3,进入“登记支出”,需输入 “本次支出金额” 和 “本次支出说明”,输入完成后会判断余额是否充足,如果余额充足会再次进入主菜单。

        如果余额不足,会给出相应的提示。

1.4 显示收支明细

        输入 1,进入“收支明细”,如下图所示:

        当没有任何收支明细时,提示 "当前没有收支明细... 来一笔吧!"

        显示完收支明细,会再次进入主菜单。

1.5 退出

        用户输入 4 退出时,给出提示 "你确定要退出吗? y/n",必须输入正确的 y/n ,否则循环输入指令,直到输入 y 或者 n。


2 流程分析

2.1 总流程图

2.2 登记收入流程图

2.3 登记支出流程图

2.4 收支明细流程图

2.5 退出流程图


3 代码实现

3.1 框架搭建

        下面的代码框架提供了一个基本的家庭收支记账软件的结构。通过一个主菜单,用户可以选择查看收支明细、登记收入、登记支出或退出程序。每个选项对应一个简单的提示信息,后续可以在此基础上扩展具体的功能实现。

#include <stdio.h>

int main()
{
    int loop = 1;      // 循环标志,用于控制程序是否继续运行

    // 主循环,直到用户选择退出
    do
    {
        // 打印菜单选项
        printf("\n\n---------家庭收支记账软件-----------------");
        printf("\n1     收支明细");
        printf("\n2     登记收入");
        printf("\n3     登记支出");
        printf("\n4     退出");
        printf("\n选择(1-4):");

        int selection;           // 存储用户的选择:1:收支明细  2:登记收入 3:登记支出 4:退出
        scanf("%d", &selection); // 读取用户的选择

        // 根据用户的选择执行相应的操作
        switch (selection)
        {
        case 1:
            // 显示收支明细的功能(此处仅为提示信息)
            puts("1.收支明细");
            break;
        case 2:
            // 登记收入的功能(此处仅为提示信息)
            puts("2.登记收入");
            break;
        case 3:
            // 登记支出的功能(此处仅为提示信息)
            puts("3.登记支出");
            break;
        case 4:
            // 用户选择退出,设置循环标志为 0 以结束循环
            puts("4.退出");
            loop = 0;
            break;
        default:
            // 如果用户输入了无效的选项,则不执行任何操作
            puts("无效的选择,请重新选择!");
            break;
        }

    } while (loop); // 当 loop 为 1 时,继续循环

    // 程序正常结束
    return 0;
}

3.2 收支明细功能

        “收支明细” 功能用于显示用户当前的所有收支记录。如果用户还没有进行任何收支记录,程序会提示用户当前没有收支明细,并建议用户进行一笔记录。如果已经有记录,程序会显示详细的收支记录信息,包括每笔收支的类型、金额、账户余额和说明。

#include <stdio.h>

int main()
{
    int loop = 1;      // 循环标志,用于控制程序是否继续运行
    int hasRecord = 0; // 标记是否有记录,0 表示没有记录,1 表示有记录
    // 初始化收支记录信息,包含标题
    // 注意:字符串如果太长,可以使用双引号进行换行隔开,双引号之间没有任何字符
    char details[3000] = "-----------------当前收支明细记录-----------------\n"
                         "收支\t收支金额\t\t账户金额\t\t说明\n"; // 存储收支的记录信息

    // 主循环,直到用户选择退出
    do
    {
        // 打印菜单选项
        printf("\n\n---------家庭收支记账软件-----------------");
        printf("\n1     收支明细");
        printf("\n2     登记收入");
        printf("\n3     登记支出");
        printf("\n4     退出");
        printf("\n选择(1-4):");

        int selection;           // 存储用户的选择:1:收支明细  2:登记收入 3:登记支出 4:退出
        scanf("%d", &selection); // 读取用户的选择

        // 根据用户的选择执行相应的操作
        switch (selection)
        {
        case 1:
            // 显示收支明细
            if (!hasRecord)
            {
                puts("当前没有收支明细…来一笔吧!"); // 如果没有记录,提示用户
            }
            else
            {
                puts(details); // 显示收支记录
            }

            break;
        case 2:
            // 登记收入的功能(此处仅为提示信息)
            puts("2.登记收入");
            break;
        case 3:
            // 登记支出的功能(此处仅为提示信息)
            puts("3.登记支出");
            break;
        case 4:
            // 用户选择退出,设置循环标志为 0 以结束循环
            puts("4.退出");
            loop = 0;
            break;
        default:
            // 如果用户输入了无效的选项,则不执行任何操作
            puts("无效的选择,请重新选择!");
            break;
        }

    } while (loop); // 当 loop 为 1 时,继续循环

    // 程序正常结束
    return 0;
}

3.3 登记收入功能

        “登记收入” 功能允许用户记录一笔收入,包括收入的金额和说明。程序会更新账户余额,并将这条收入记录追加到收支记录中,并标记 hasRecord 为 1,表示已经有记录。

#include <stdio.h>
#include <string.h> // 包含字符串处理函数的头文件

int main()
{
    int loop = 1;      // 循环标志,用于控制程序是否继续运行
    int hasRecord = 0; // 标记是否有记录,0 表示没有记录,1 表示有记录
    // 初始化收支记录信息,包含标题
    // 注意:字符串如果太长,可以使用双引号进行换行隔开,双引号之间没有任何字符
    char details[3000] = "-----------------当前收支明细记录-----------------\n"
                         "收支\t收支金额\t\t账户金额\t\t说明\n"; // 存储收支的记录信息
    double initBalance = 1000;  // 初始账户余额

    // 主循环,直到用户选择退出
    do
    {
        // 打印菜单选项
        printf("\n\n---------家庭收支记账软件-----------------");
        printf("\n1     收支明细");
        printf("\n2     登记收入");
        printf("\n3     登记支出");
        printf("\n4     退出");
        printf("\n选择(1-4):");

        int selection;           // 存储用户的选择:1:收支明细  2:登记收入 3:登记支出 4:退出
        scanf("%d", &selection); // 读取用户的选择

        // 根据用户的选择执行相应的操作
        switch (selection)
        {
        case 1:
            // 显示收支明细
            if (!hasRecord)
            {
                puts("当前没有收支明细…来一笔吧!"); // 如果没有记录,提示用户
            }
            else
            {
                puts(details); // 显示收支记录
            }

            break;
        case 2:
            // 登记收入的功能
            printf("%s", "本次收入金额:");
            double addMoney;
            scanf("%lf", &addMoney); // 读取收入金额

            printf("%s", "本次收入说明:");
            char addDetail[100];
            scanf("%s", addDetail); // 读取收入说明

            // 修改账户余额
            initBalance += addMoney;

            char addDetails[300]; // 临时存储用户输入的信息
            // 将用户输入的信息拼接成一个字符串,并追加到收支记录中
            sprintf(addDetails, "收入\t+%lf\t\t%lf\t\t%s\n", addMoney, initBalance, addDetail);
            strcat(details, addDetails);

            // 标记已有记录
            hasRecord = 1;
            puts("登记收入完成");

            break;
        case 3:
            // 登记支出的功能(此处仅为提示信息)
            puts("3.登记支出");
            break;
        case 4:
            // 用户选择退出,设置循环标志为 0 以结束循环
            puts("4.退出");
            loop = 0;
            break;
        default:
            // 如果用户输入了无效的选项,则不执行任何操作
            puts("无效的选择,请重新选择!");
            break;
        }

    } while (loop); // 当 loop 为 1 时,继续循环

    // 程序正常结束
    return 0;
}

3.4 登记支出功能

        “登记支出” 功能允许用户记录一笔支出,包括支出的金额和说明。程序会检查账户余额是否足够支付这笔支出,如果余额充足,程序会更新账户余额,并将这条支出记录追加到收支记录中。标记 hasRecord 为 1,表示已经有记录。

#include <stdio.h>
#include <string.h> // 包含字符串处理函数的头文件

int main()
{
    int loop = 1;      // 循环标志,用于控制程序是否继续运行
    int hasRecord = 0; // 标记是否有记录,0 表示没有记录,1 表示有记录
    // 初始化收支记录信息,包含标题
    // 注意:字符串如果太长,可以使用双引号进行换行隔开,双引号之间没有任何字符
    char details[3000] = "-----------------当前收支明细记录-----------------\n"
                         "收支\t收支金额\t\t账户金额\t\t说明\n"; // 存储收支的记录信息
    double initBalance = 1000;  // 初始账户余额

    // 主循环,直到用户选择退出
    do
    {
        // 打印菜单选项
        printf("\n\n---------家庭收支记账软件-----------------");
        printf("\n1     收支明细");
        printf("\n2     登记收入");
        printf("\n3     登记支出");
        printf("\n4     退出");
        printf("\n选择(1-4):");

        int selection;           // 存储用户的选择:1:收支明细  2:登记收入 3:登记支出 4:退出
        scanf("%d", &selection); // 读取用户的选择

        // 根据用户的选择执行相应的操作
        switch (selection)
        {
        case 1:
            // 显示收支明细
            if (!hasRecord)
            {
                puts("当前没有收支明细…来一笔吧!"); // 如果没有记录,提示用户
            }
            else
            {
                puts(details); // 显示收支记录
            }

            break;
        case 2:
            // 登记收入的功能
            printf("%s", "本次收入金额:");
            double addMoney;
            scanf("%lf", &addMoney); // 读取收入金额

            printf("%s", "本次收入说明:");
            char addDetail[100];
            scanf("%s", addDetail); // 读取收入说明

            // 修改账户余额
            initBalance += addMoney;

            char addDetails[300]; // 临时存储用户输入的信息
            // 将用户输入的信息拼接成一个字符串,并追加到收支记录中
            sprintf(addDetails, "收入\t+%lf\t\t%lf\t\t%s\n", addMoney, initBalance, addDetail);
            strcat(details, addDetails);

            // 标记已有记录
            hasRecord = 1;
            puts("登记收入完成");

            break;
         case 3:
            // 登记支出的功能
            printf("%s", "本次支出金额:");
            double minusMoney;
            scanf("%lf", &minusMoney); // 读取支出金额

            if (minusMoney > initBalance)
            {
                puts("余额不足…"); // 如果余额不足,提示用户并中断支出操作
                break;
            }
            else
            {
                // 修改账户余额
                initBalance -= minusMoney;

                printf("%s", "本次支出说明:");
                char minusDetail[100];
                scanf("%s", minusDetail); // 读取支出说明

                char minusDetails[300]; // 临时存储用户输入的信息
                // 将用户输入的信息拼接成一个字符串,并追加到收支记录中
                sprintf(minusDetails, "支出\t-%lf\t\t%lf\t\t%s\n", minusMoney, initBalance, minusDetail);
                strcat(details, minusDetails);

                // 标记已有记录
                hasRecord = 1;
                puts("登记支出完成");
            }
            break;
        case 4:
            // 用户选择退出,设置循环标志为 0 以结束循环
            puts("4.退出");
            loop = 0;
            break;
        default:
            // 如果用户输入了无效的选项,则不执行任何操作
            puts("无效的选择,请重新选择!");
            break;
        }

    } while (loop); // 当 loop 为 1 时,继续循环

    // 程序正常结束
    return 0;
}

3.5 退出功能

        “退出功能” 允许用户确认是否要退出程序。当用户选择退出时,程序会提示用户确认是否真的要退出。如果用户确认退出,程序会设置循环标志 loop 为 0,结束主循环并退出程序。如果用户取消退出,程序将继续运行。

#include <stdio.h>
#include <string.h> // 包含字符串处理函数的头文件

int main()
{
    int loop = 1;      // 循环标志,用于控制程序是否继续运行
    int hasRecord = 0; // 标记是否有记录,0 表示没有记录,1 表示有记录
    // 初始化收支记录信息,包含标题
    // 注意:字符串如果太长,可以使用双引号进行换行隔开,双引号之间没有任何字符
    char details[3000] = "-----------------当前收支明细记录-----------------\n"
                         "收支\t收支金额\t\t账户金额\t\t说明\n"; // 存储收支的记录信息
    double initBalance = 1000;                                   // 初始账户余额

    // 主循环,直到用户选择退出
    do
    {
        // 打印菜单选项
        printf("\n\n---------家庭收支记账软件-----------------");
        printf("\n1     收支明细");
        printf("\n2     登记收入");
        printf("\n3     登记支出");
        printf("\n4     退出");
        printf("\n选择(1-4):");

        int selection;           // 存储用户的选择:1:收支明细  2:登记收入 3:登记支出 4:退出
        scanf("%d", &selection); // 读取用户的选择

        // 根据用户的选择执行相应的操作
        switch (selection)
        {
        case 1:
            // 显示收支明细
            if (!hasRecord)
            {
                puts("当前没有收支明细…来一笔吧!"); // 如果没有记录,提示用户
            }
            else
            {
                puts(details); // 显示收支记录
            }

            break;
        case 2:
            // 登记收入的功能
            printf("%s", "本次收入金额:");
            double addMoney;
            scanf("%lf", &addMoney); // 读取收入金额

            printf("%s", "本次收入说明:");
            char addDetail[100];
            scanf("%s", addDetail); // 读取收入说明

            // 修改账户余额
            initBalance += addMoney;

            char addDetails[300]; // 临时存储用户输入的信息
            // 将用户输入的信息拼接成一个字符串,并追加到收支记录中
            sprintf(addDetails, "收入\t+%lf\t\t%lf\t\t%s\n", addMoney, initBalance, addDetail);
            strcat(details, addDetails);

            // 标记已有记录
            hasRecord = 1;
            puts("登记收入完成");

            break;
        case 3:
            // 登记支出的功能
            printf("%s", "本次支出金额:");
            double minusMoney;
            scanf("%lf", &minusMoney); // 读取支出金额

            if (minusMoney > initBalance)
            {
                puts("余额不足…"); // 如果余额不足,提示用户并中断支出操作
                break;
            }
            else
            {
                // 修改账户余额
                initBalance -= minusMoney;

                printf("%s", "本次支出说明:");
                char minusDetail[100];
                scanf("%s", minusDetail); // 读取支出说明

                char minusDetails[300]; // 临时存储用户输入的信息
                // 将用户输入的信息拼接成一个字符串,并追加到收支记录中
                sprintf(minusDetails, "支出\t-%lf\t\t%lf\t\t%s\n", minusMoney, initBalance, minusDetail);
                strcat(details, minusDetails);

                // 标记已有记录
                hasRecord = 1;
                puts("登记支出完成");
            }
            break;
        case 4:
            // 退出功能
            puts("你确定要退出吗(y/n):");
            char isExit;
            while (1)
            {
                // getchar(); // 清除缓冲区(因为输入 4 之后,还按了一个回车)

                // 读取用户输入,确认是否退出
                scanf(" %c", &isExit); // 注意 %c 前面的空格,用于跳过前面的空白符,或者在前面使用 getchar 读取多余的换行符

                if (isExit == 'y' || isExit == 'n')
                {
                    break; // 如果输入有效,结束循环
                }
                else
                {
                    puts("你输入有误,请从新输入(y/n):"); // 提示用户重新输入
                }
            }
            if (isExit == 'y')
            {
                // 设置循环标志为 0,准备退出程序
                loop = 0;
                puts("欢迎再次来到家庭记账软件^_^");
            }

            break;
        default:
            // 如果用户输入了无效的选项,则不执行任何操作
            puts("无效的选择,请重新选择!");
            break;
        }

    } while (loop); // 当 loop 为 1 时,继续循环

    // 退出程序时的提示信息
    puts("你退出了家庭财务软件~");

    // 程序正常结束
    return 0;
}

        自此,本项目已成功实现,但仍存在需要进一步优化的地方。具体详情,请见后文。


4 安全升级

        scanf 函数在读取输入时,如果输入不符合预期格式,会留下未读取的数据在输入缓冲区中,这会导致后续的 scanf 调用继续读取这些未处理的数据,从而形成无限循环。为了防止这种情况,可以在读取用户输入时检查 scanf 的返回值,并在必要时清理输入缓冲区。

#include <stdio.h>
#include <string.h> // 包含字符串处理函数的头文件

int main()
{
    int loop = 1;      // 循环标志,用于控制程序是否继续运行
    int hasRecord = 0; // 标记是否有记录,0 表示没有记录,1 表示有记录
    // 初始化收支记录信息,包含标题
    // 注意:字符串如果太长,可以使用双引号进行换行隔开,双引号之间没有任何字符
    char details[3000] = "-----------------当前收支明细记录-----------------\n"
                         "收支\t收支金额\t\t账户金额\t\t说明\n"; // 存储收支的记录信息
    double initBalance = 1000;                                   // 初始账户余额

    // 主循环,直到用户选择退出
    do
    {
        // 打印菜单选项
        printf("\n\n---------家庭收支记账软件-----------------");
        printf("\n1     收支明细");
        printf("\n2     登记收入");
        printf("\n3     登记支出");
        printf("\n4     退出");
        printf("\n选择(1-4):");

        int selection;                    // 存储用户的选择:1:收支明细  2:登记收入 3:登记支出 4:退出
        if (scanf("%d", &selection) != 1) // 检查 scanf 是否成功读取一个整数
        {
            puts("无效的输入,请输入一个数字 (1-4)!");
            // 清理输入缓冲区
            while (getchar() != '\n')
                ;
            continue; // 重新显示菜单
        }

        // 根据用户的选择执行相应的操作
        switch (selection)
        {
        case 1:
            // 显示收支明细
            if (!hasRecord)
            {
                puts("当前没有收支明细…来一笔吧!"); // 如果没有记录,提示用户
            }
            else
            {
                puts(details); // 显示收支记录
            }

            break;
        case 2:
            // 登记收入的功能
            printf("%s", "本次收入金额:");
            double addMoney;
            if (scanf("%lf", &addMoney) != 1) // 检查 scanf 是否成功读取一个浮点数
            {
                puts("无效的输入,请输入一个数字!");
                while (getchar() != '\n')
                    ;     // 清理输入缓冲区
                continue; // 重新显示菜单
            }

            printf("%s", "本次收入说明:");
            char addDetail[100];
            if (scanf("%99s", addDetail) != 1) // 检查 scanf 是否成功读取一个字符串
            {
                puts("无效的输入,请输入一个有效的说明!");
                while (getchar() != '\n')
                    ;     // 清理输入缓冲区
                continue; // 重新显示菜单
            }

            // 修改账户余额
            initBalance += addMoney;

            char addDetails[300]; // 临时存储用户输入的信息
            // 将用户输入的信息拼接成一个字符串,并追加到收支记录中
            sprintf(addDetails, "收入\t+%lf\t\t%lf\t\t%s\n", addMoney, initBalance, addDetail);
            strcat(details, addDetails);

            // 标记已有记录
            hasRecord = 1;
            puts("登记收入完成");

            break;
        case 3:
            // 登记支出的功能
            printf("%s", "本次支出金额:");
            double minusMoney;
            if (scanf("%lf", &minusMoney) != 1) // 检查 scanf 是否成功读取一个浮点数
            {
                puts("无效的输入,请输入一个数字!");
                while (getchar() != '\n')
                    ;     // 清理输入缓冲区
                continue; // 重新显示菜单
            }

            if (minusMoney > initBalance)
            {
                puts("余额不足…"); // 如果余额不足,提示用户并中断支出操作
                break;
            }
            else
            {
                // 修改账户余额
                initBalance -= minusMoney;

                printf("%s", "本次支出说明:");
                char minusDetail[100];
                if (scanf("%99s", minusDetail) != 1) // 检查 scanf 是否成功读取一个字符串
                {
                    puts("无效的输入,请输入一个有效的说明!");
                    while (getchar() != '\n')
                        ;     // 清理输入缓冲区
                    continue; // 重新显示菜单
                }

                char minusDetails[300]; // 临时存储用户输入的信息
                // 将用户输入的信息拼接成一个字符串,并追加到收支记录中
                sprintf(minusDetails, "支出\t-%lf\t\t%lf\t\t%s\n", minusMoney, initBalance, minusDetail);
                strcat(details, minusDetails);

                // 标记已有记录
                hasRecord = 1;
                puts("登记支出完成");
            }
            break;
        case 4:
            // 退出功能
            puts("你确定要退出吗(y/n):");
            char isExit;
            while (1)
            {
                // 读取用户输入,确认是否退出
                if (scanf(" %c", &isExit) != 1) // 检查 scanf 是否成功读取一个字符
                {
                    puts("无效的输入,请输入 y 或 n!");
                    while (getchar() != '\n')
                        ;     // 清理输入缓冲区
                    continue; // 重新读取输入
                }

                if (isExit == 'y' || isExit == 'n')
                {
                    break; // 如果输入有效,结束循环
                }
                else
                {
                    puts("你输入有误,请从新输入(y/n):"); // 提示用户重新输入
                }
            }
            if (isExit == 'y')
            {
                // 设置循环标志为 0,准备退出程序
                loop = 0;
                puts("欢迎再次来到家庭记账软件^_^");
            }

            break;
        default:
            // 如果用户输入了无效的选项,则不执行任何操作
            puts("无效的选择,请重新选择!");
            break;
        }

    } while (loop); // 当 loop 为 1 时,继续循环

    // 退出程序时的提示信息
    puts("你退出了家庭财务软件~");

    // 程序正常结束
    return 0;
}

5 代码升级

5.1 抽取结构体

#include <stdio.h>
#include <string.h> // 包含字符串处理函数的头文件

// 声明一个结构体,用于存储家庭账户信息
typedef struct FamilyAccount
{
    // 结构体成员声明
    int hasRecord;      // 是否有记录标志,0 表示没有记录,1 表示有记录
    char details[3000]; // 用于存储收支的记录信息
    double initBalance; // 用于存储初始账户余额

} familyAccount;

/*
    菜单函数,用于显示和处理用户的选择
    注意,这里 familyAccount 等价于 struct FamilyAccount
    如若没有使用 typedef,请使用 void menu(struct FamilyAccount *fAccount)
*/
void menu(familyAccount *fAccount)
{
    int loop = 1; // 循环标志,用于控制程序是否继续运行

    // 主循环,直到用户选择退出
    do
    {
        // 打印菜单选项
        printf("\n\n---------家庭收支记账软件-----------------");
        printf("\n1     收支明细");
        printf("\n2     登记收入");
        printf("\n3     登记支出");
        printf("\n4     退出");
        printf("\n选择(1-4):");

        int selection;                    // 存储用户的选择:1:收支明细  2:登记收入 3:登记支出 4:退出
        if (scanf("%d", &selection) != 1) // 检查 scanf 是否成功读取一个整数
        {
            puts("无效的输入,请输入一个数字 (1-4)!");
            // 清理输入缓冲区
            while (getchar() != '\n')
                ;
            continue; // 重新显示菜单
        }

        // 根据用户的选择执行相应的操作
        switch (selection)
        {
        case 1:
            // 显示收支明细
            // if (!(*fAccount).hasRecord)// 写法 1
            if (!fAccount->hasRecord) // 写法 2
            {
                puts("当前没有收支明细…来一笔吧!"); // 如果没有记录,提示用户
            }
            else
            {
                puts(fAccount->details); // 显示收支记录
            }

            break;
        case 2:
            // 登记收入的功能
            printf("%s", "本次收入金额:");
            double addMoney;
            if (scanf("%lf", &addMoney) != 1) // 检查 scanf 是否成功读取一个浮点数
            {
                puts("无效的输入,请输入一个数字!");
                while (getchar() != '\n')
                    ;     // 清理输入缓冲区
                continue; // 重新显示菜单
            }

            printf("%s", "本次收入说明:");
            char addDetail[100];
            if (scanf("%99s", addDetail) != 1) // 检查 scanf 是否成功读取一个字符串
            {
                puts("无效的输入,请输入一个有效的说明!");
                while (getchar() != '\n')
                    ;     // 清理输入缓冲区
                continue; // 重新显示菜单
            }

            // 修改账户余额
            fAccount->initBalance += addMoney;

            char addDetails[300]; // 临时存储用户输入的信息
            // 将用户输入的信息拼接成一个字符串,并追加到收支记录中
            sprintf(addDetails, "收入\t+%lf\t\t%lf\t\t%s\n", addMoney, fAccount->initBalance, addDetail);
            strcat(fAccount->details, addDetails);

            // 标记已有记录
            fAccount->hasRecord = 1;
            puts("登记收入完成");

            break;
        case 3:
            // 登记支出的功能
            printf("%s", "本次支出金额:");
            double minusMoney;
            if (scanf("%lf", &minusMoney) != 1) // 检查 scanf 是否成功读取一个浮点数
            {
                puts("无效的输入,请输入一个数字!");
                while (getchar() != '\n')
                    ;     // 清理输入缓冲区
                continue; // 重新显示菜单
            }

            if (minusMoney > fAccount->initBalance)
            {
                puts("余额不足…"); // 如果余额不足,提示用户并中断支出操作
                break;
            }
            else
            {
                // 修改账户余额
                fAccount->initBalance -= minusMoney;

                printf("%s", "本次支出说明:");
                char minusDetail[100];
                if (scanf("%99s", minusDetail) != 1) // 检查 scanf 是否成功读取一个字符串
                {
                    puts("无效的输入,请输入一个有效的说明!");
                    while (getchar() != '\n')
                        ;     // 清理输入缓冲区
                    continue; // 重新显示菜单
                }

                char minusDetails[300]; // 临时存储用户输入的信息
                // 将用户输入的信息拼接成一个字符串,并追加到收支记录中
                sprintf(minusDetails, "支出\t-%lf\t\t%lf\t\t%s\n", minusMoney, fAccount->initBalance, minusDetail);
                strcat(fAccount->details, minusDetails);

                // 标记已有记录
                fAccount->hasRecord = 1;
                puts("登记支出完成");
            }
            break;
        case 4:
            // 退出功能
            puts("你确定要退出吗(y/n):");
            char isExit;
            while (1)
            {
                // 读取用户输入,确认是否退出
                if (scanf(" %c", &isExit) != 1) // 检查 scanf 是否成功读取一个字符
                {
                    puts("无效的输入,请输入 y 或 n!");
                    while (getchar() != '\n')
                        ;     // 清理输入缓冲区
                    continue; // 重新读取输入
                }

                if (isExit == 'y' || isExit == 'n')
                {
                    break; // 如果输入有效,结束循环
                }
                else
                {
                    puts("你输入有误,请从新输入(y/n):"); // 提示用户重新输入
                }
            }
            if (isExit == 'y')
            {
                // 设置循环标志为 0,准备退出程序
                loop = 0;
                puts("欢迎再次来到家庭记账软件^_^");
            }

            break;
        default:
            // 如果用户输入了无效的选项,则不执行任何操作
            puts("无效的选择,请重新选择!");
            break;
        }

    } while (loop); // 当 loop 为 1 时,继续循环

    // 退出程序时的提示信息
    puts("你退出了家庭财务软件~");
}

int main()
{
    // 声明结构体变量,并为结构体成员赋值
    // familyAccount fAccount;
    // fAccount.hasRecord = 0;
    // 初始化收支记录信息,包含标题
    // 注意:字符串如果太长,可以使用双引号进行换行隔开,双引号之间没有任何字符
    // fAccount.details = "-----------------当前收支明细记录-----------------\n"
    //                    "收支\t收支金额\t\t账户金额\t\t说明\n";
    // 可以使用 strcpy
    // strcpoy(fAccount.details, "-----------------当前收支明细记录-----------------\n收支\t收支金额\t\t账户金额\t\t说明\n");
    // fAccount.initBalance = 1000;

    // 也可以使用大括号一次性赋值,但必须在声明结构体时
    familyAccount fAccount = {0,
                              "-----------------当前收支明细记录-----------------\n收支\t收支金额\t\t账户金额\t\t说明\n",
                              1000};

    // 显示菜单界面
    menu(&fAccount);

    // 程序正常结束
    return 0;
}

5.2 使用函数封装相应功能

#include <stdio.h>
#include <string.h>

// 定义家庭账户结构体
// 使用 typedef 定义别名,在后续操作中可以使用别名 FamilyAccount 代替 struct FamilyAccount,以此简化操作
typedef struct FamilyAccount
{
    int hasRecord;      // 是否有记录标志,0 表示没有记录,1 表示有记录
    char details[3000]; // 用于存储收支的记录信息
    double balance;     // 用于存储账户余额
} FamilyAccount;

// 全局变量,循环标志,用于控制主循环的继续或退出
int loop = 1;

// 声明函数原型
void menu(FamilyAccount *fAccount);        // 菜单函数
void showDetails(FamilyAccount *fAccount); // 显示收支明细
void addIncome(FamilyAccount *fAccount);   // 登记收入
void addExpense(FamilyAccount *fAccount);  // 登记支出
void confirmExit();                        // 确认退出

int main()
{
    // 定义结构体变量,并使用 {} 一次性为结构体成员赋值
    // 使用 {} 赋值 必须在结构体变量定义时才行
    FamilyAccount fAccount = {0, "-----------------当前收支明细记录-----------------\n收支\t收支金额\t\t账户金额\t\t说明\n", 1000};

    // 传递结构体变量的地址(即结构体指针),方便后续对结构体成员的数据进行操作
    menu(&fAccount);

    return 0;
}

// 显示收支明细
void showDetails(FamilyAccount *fAccount)
{
    // 检查是否有记录
    if (!fAccount->hasRecord)
    {
        puts("当前没有收支明细…来一笔吧!");
    }
    else
    {
        // 输出收支记录信息
        puts(fAccount->details);
    }
}

// 登记收入
void addIncome(FamilyAccount *fAccount)
{
    printf("本次收入金额:");
    double addMoney;
    // 读取用户输入的收入金额
    if (scanf("%lf", &addMoney) != 1)
    {
        puts("无效的输入,请输入一个数字!");
        // 清理输入缓冲区
        while (getchar() != '\n')
            ;
        return; // 退出函数
    }

    printf("本次收入说明:");
    char addDetail[100];
    // 读取用户输入的收入说明
    if (scanf("%99s", addDetail) != 1)
    {
        puts("无效的输入,请输入一个有效的说明!");
        // 清理输入缓冲区
        while (getchar() != '\n')
            ;
        return; // 退出函数
    }

    // 更新账户余额
    fAccount->balance += addMoney;

    // 创建一个临时字符串,用于存储本次收入的详细信息
    char addDetails[300];
    sprintf(addDetails, "收入\t+%lf\t\t%lf\t\t%s\n", addMoney, fAccount->balance, addDetail);

    // 将本次收入的详细信息追加到记录中
    strcat(fAccount->details, addDetails);

    // 标记已有记录
    fAccount->hasRecord = 1;

    // 提示用户收入登记完成
    puts("登记收入完成");
}

// 登记支出
void addExpense(FamilyAccount *fAccount)
{
    printf("本次支出金额:");
    double minusMoney;
    // 读取用户输入的支出金额
    if (scanf("%lf", &minusMoney) != 1)
    {
        puts("无效的输入,请输入一个数字!");
        // 清理输入缓冲区
        while (getchar() != '\n')
            ;
        return; // 退出函数
    }

    // 检查余额是否足够
    if (minusMoney > fAccount->balance)
    {
        puts("余额不足…");
        return; // 退出函数
    }

    // 更新账户余额
    fAccount->balance -= minusMoney;

    printf("本次支出说明:");
    char minusDetail[100];
    // 读取用户输入的支出说明
    if (scanf("%99s", minusDetail) != 1)
    {
        puts("无效的输入,请输入一个有效的说明!");
        // 清理输入缓冲区
        while (getchar() != '\n')
            ;
        return; // 退出函数
    }

    // 创建一个临时字符串,用于存储本次支出的详细信息
    char minusDetails[300];
    sprintf(minusDetails, "支出\t-%lf\t\t%lf\t\t%s\n", minusMoney, fAccount->balance, minusDetail);

    // 将本次支出的详细信息追加到记录中
    strcat(fAccount->details, minusDetails);

    // 标记已有记录
    fAccount->hasRecord = 1;

    // 提示用户支出登记完成
    puts("登记支出完成");
}

// 确认退出
void confirmExit()
{
    puts("你确定要退出吗(y/n):");
    char isExit;
    // 读取用户输入的确认信息
    while (1)
    {
        if (scanf(" %c", &isExit) != 1) // %c 前面有空格,用于跳过前面的空白字符
        {
            puts("无效的输入,请输入 y 或 n!");
            // 清理输入缓冲区
            while (getchar() != '\n')
                ;
            continue; // 跳过本次循环
        }

        // 检查用户输入是否有效
        if (isExit == 'y' || isExit == 'n')
        {
            break; // 结束循环
        }
        else
        {
            puts("你输入有误,请从新输入(y/n):");
        }
    }

    // 如果用户选择退出,设置循环标志为 0
    if (isExit == 'y')
    {
        loop = 0;
        puts("欢迎再次来到家庭记账软件^_^");
    }
}

// 菜单函数
void menu(FamilyAccount *fAccount)
{
    do
    {
        // 打印菜单选项
        printf("\n\n---------家庭收支记账软件-----------------\n");
        printf("1     收支明细\n");
        printf("2     登记收入\n");
        printf("3     登记支出\n");
        printf("4     退出\n");
        printf("选择(1-4):");

        int selection;
        // 读取用户选择
        if (scanf("%d", &selection) != 1)
        {
            puts("无效的输入,请输入一个数字 (1-4)!");
            // 清理输入缓冲区
            while (getchar() != '\n')
                ;
            continue;
        }

        // 根据用户选择执行相应操作
        switch (selection)
        {
        case 1:
            showDetails(fAccount); // fAccount 就是结构体指针类型,不需要在前面再加上取地址符 &
            break;
        case 2:
            addIncome(fAccount);
            break;
        case 3:
            addExpense(fAccount);
            break;
        case 4:
            confirmExit();
            break;
        default:
            puts("无效的选择,请重新选择!");
            break;
        }
    } while (loop); // 当 loop 为 1 时,继续循环

    // 提示用户已退出软件
    puts("你退出了家庭财务软件~");
}

        至此,该项目的代码已经进行了充分的优化。如果您有任何更好的优化建议或改进思路,欢迎在评论区踊跃留言!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/889640.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

23年408数据结构

第一题&#xff1a; 解析&#xff1a; 第一点&#xff0c;我们要知道顺序存储的特点&#xff1a;优点就是随用随取&#xff0c;就是你想要查询第几个元素可以直接查询出来&#xff0c;时间复杂度就是O(1)&#xff0c;缺点就是不适合删除和插入&#xff0c;因为每次删除和插入一…

【数据分享】2000—2023年我国省市县三级逐年植被覆盖度(FVC)数据(Shp/Excel格式)

之前我们分享过2000—2023年逐月植被覆盖度&#xff08;FVC&#xff09;栅格数据&#xff08;可查看之前的文章获悉详情&#xff09;和Excel和Shp格式的省市县三级逐月FVC数据&#xff08;可查看之前的文章获悉详情&#xff09;&#xff0c;原始的逐月栅格数据来源于高吉喜学者…

青云AI算力创新:直击AI落地痛点 打造企业数智化基石

在当今这个数字化、智能化的时代&#xff0c;企业数字化转型、智能化升级应用实践在加速&#xff0c;AI算力已经成为企业数字化转型和智能化升级的重要基石&#xff0c;而AI算力在推动技术创新和业务增长中起到了关键作用。青云科技近日举办的AI算力发布会&#xff0c;标志着AI…

知识图谱入门——5:Neo4j Desktop安装和使用手册(小白向:Cypher 查询语言:逐步教程!Neo4j 优缺点分析)

Neo4j简介 Neo4j 是一个基于图结构的 NoSQL 数据库&#xff0c;专门用于存储、查询和管理图形数据。它的核心思想是使用节点、关系和属性来描述数据。图数据库非常适合那些需要处理复杂关系的数据集&#xff0c;如社交网络、推荐系统、知识图谱等领域。 与传统的关系型数据库…

如何快速给word文件加拼音?请跟着步骤完成吧

如何快速给word文件加拼音&#xff1f;在日常工作中&#xff0c;我们时常会遇到需要为Word文件中的文字添加拼音的情况&#xff0c;这尤其在教育、出版或国际交流等领域显得尤为重要。为文字配上拼音&#xff0c;不仅能帮助学习者准确发音&#xff0c;还能提升文档的可读性和普…

【JavaEE初阶】深入理解不同锁的意义,synchronized的加锁过程理解以及CAS的原子性实现(面试经典题);

前言 &#x1f31f;&#x1f31f;本期讲解关于锁的相关知识了解&#xff0c;这里涉及到高频面试题哦~~~ &#x1f308;上期博客在这里&#xff1a;【JavaEE初阶】深入理解线程池的概念以及Java标准库提供的方法参数分析-CSDN博客 &#x1f308;感兴趣的小伙伴看一看小编主页&am…

【STM32单片机_(HAL库)】4-2-1【定时器TIM】定时器输出PWM实现呼吸灯实验

1.硬件 STM32单片机最小系统LED灯模块 2.软件 pwm驱动文件添加定时器HAL驱动层文件添加GPIO常用函数定时器输出PWM配置步骤main.c程序 #include "sys.h" #include "delay.h" #include "led.h" #include "pwm.h"int main(void) {HA…

【瑞萨RA8D1 CPK开发板】串口的使用和STDOUT输出重定向

串口 本次串口的使用关于时钟导致串口的波特率不对&#xff0c;坑了我很久的时间 使能时钟 串口发现一个问题就是&#xff0c;只能使用下边的时钟配置&#xff0c;修改时钟源和分频系数都会导致串口波特率不正常&#xff0c;这种问题出现在mdkrasc的使用场景之下&#xff1b…

adaptor lora基础

https://www.zhihu.com/question/508658141/answer/3340979311 adaptor和PEFT的区别&#xff1a;前者在模型子层后加一个小型的dense&#xff1b;后者直接稀疏化模型本身&#xff1b; Loading Pre-Trained Adapters — AdapterHub documentation CVPR 2024 | SD-DiT&#xff…

Python | Leetcode Python题解之第468题验证IP地址

题目&#xff1a; 题解&#xff1a; class Solution:def validIPAddress(self, queryIP: str) -> str:if queryIP.find(".") ! -1:# IPv4last -1for i in range(4):cur (len(queryIP) if i 3 else queryIP.find(".", last 1))if cur -1:return &q…

每日OJ题_牛客_小乐乐改数字_模拟_C++_Java

目录 牛客_小乐乐改数字_模拟 题目解析 C代码 Java代码 牛客_小乐乐改数字_模拟 小乐乐改数字_牛客题霸_牛客网 (nowcoder.com) 描述&#xff1a; 小乐乐喜欢数字&#xff0c;尤其喜欢0和1。他现在得到了一个数&#xff0c;想把每位的数变成0或1。如果某一位是奇数&#…

【网络安全】CVE-2024-46990: Directus环回IP过滤器绕过实现SSRF

未经许可,不得转载。 文章目录 背景漏洞详情受影响版本解决方案背景 Directus 是一款开源 CMS,提供强大的内容管理 API,使开发人员能够轻松创建自定义应用程序,凭借其灵活的数据模型和用户友好的界面备受欢迎。然而,Directus 存在一个漏洞,允许攻击者绕过默认的环回 IP …

大数据之——VWare、Ubuntu、CentOs、Hadoop安装配置

前言&#xff1a;这里很抱歉前几期考研专题以及PyTorch这些内容都没有更新&#xff0c;并不是没有在学了&#xff0c;而是事太鸡儿多了&#xff0c;前不久刚刚打完华为开发者比赛&#xff0c;然后有紧接着高数比赛、考研复习&#xff0c;因此这些后续文章都在草稿状态中&#x…

Allegro在PCB上开槽的三种方法操作指导

Allegro如何在PCB上开槽的三种方法操作指导 当PCB有特殊设计要求的时候&#xff0c;需要在PCB上开槽&#xff0c;Allegro支持在PCB上开槽操作&#xff0c;具体操作如下 以下图为例&#xff0c;需要在这个板框中间开槽 开方形槽 选择shape add rect命令 画在Board Geometry-o…

【技术详解】SpringMVC框架全面解析:从入门到精通(SpringMVC)

文章目录 【技术详解】SpringMVC框架全面解析&#xff1a;从入门到精通(SpringMVC)SpringMVC概述1. 三层架构与MVC架构区别1.1 三层架构1.2 MVC架构1.3前后端分离开发模式 2. SpringMVC环境搭建2.1 注解启动方式2.2 xml启动方式2.3 SpringMVC PostMan工具使用 3. SpringMVC 请求…

MySQL 实验1:Windows 环境下 MySQL5.5 安装与配置

MySQL 实验1&#xff1a;Windows 环境下 MySQL5.5 安装与配置 目录 MySQL 实验1&#xff1a;Windows 环境下 MySQL5.5 安装与配置一、MySQL 软件的下载二、安装 MySQL三、配置 MySQL1、配置环境变量2、安装并启动 MySQL 服务3、设置 MySQL 字符集4、为 root 用户设置登录密码 一…

【华为欧拉】国产OpenEuler服务器系统安装以及图形界面

openEuler下载 | openEuler ISO镜像 | openEuler社区官网 下载安装iso 本次选择4G的社区版本 安装&#xff0c;复制到光盘&#xff0c;光盘引导安装。虚拟机安装&#xff0c;准备好iso文件引用&#xff0c;指定好安装源&#xff0c;安装界面和centOS基本一样。选择最小安装就…

代码随想录算法训练营第三十天|491. 非递减子序列,46. 全排列,47. 全排列 II,332. 重新安排行程,51. N 皇后,37. 解数独

491. 非递减子序列&#xff0c;46. 全排列&#xff0c;47. 全排列 II&#xff0c;332. 重新安排行程&#xff0c;51. N 皇后&#xff0c;37. 解数独 491. 非递减子序列46. 全排列47. 全排列 II332. 重新安排行程51. N 皇后37. 解数独 491. 非递减子序列 给你一个整数数组 nums…

友思特方案 | FantoVision边缘计算:嵌入式视觉系统如何实现“更快 更高 更强”?

导读 便于集成的嵌入式视觉系统一直以来面临着带宽、内存、算力三个方面的挑战。友思特 FantoVision 边缘计算设备拥有更快的处理速度和更高的带宽选择&#xff0c;其开放式架构有效突破了上述三重阻碍。 嵌入式视觉 嵌入式视觉是传统机器视觉衍生出来的子集&#xff0c;嵌入…

k8s 中存储之 PV 持久卷 与 PVC 持久卷申请

目录 1 PV 与 PVC 介绍 1.1 PersistentVolume&#xff08;持久卷&#xff0c;简称PV&#xff09; 1.2 PersistentVolumeClaim&#xff08;持久卷声明&#xff0c;简称PVC&#xff09; 1.3 使用了PV和PVC之后&#xff0c;工作可以得到进一步的细分&#xff1a; 2 持久卷实验配置…