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

news2024/10/12 7:25:06

目录

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("你退出了家庭财务软件~");
}

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决UOS操作系统vim内容鼠标选中后进入可视模式,无法复制问题

现象&#xff1a; 在 vim 插入模式中右键单击出现可视模式&#xff0c;如下图 解决方法&#xff1a; 1. 编辑文件 rootkylin-PC:~# vi /usr/share/vim/vim81/defaults.vim 改为

ANSYS Workbench多边形骨料及界面过渡区混凝土细观模型

混凝土细观模型是一种用来研究混凝土材料内部结构和性能的分析方法。它主要关注于混凝土中不同组分&#xff08;如骨料、水泥浆体等&#xff09;之间的相互作用以及这些相互作用如何影响整体材料的行为。在建立这样的模型时&#xff0c;考虑到多边形骨料及其与周围基质之间形成…

【含文档】基于Springboot+Android的环境保护生活App(含源码+数据库+lw)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 系统定…

Jenkins入门:流水线方式部署多模块Springboot项目

目录 一、环境准备 1. 搭建配置Jenkins (在上一篇基础上进行) 2. 安装mysql 3. 安装redis 4. 配置docker-componse 5. 启动docker-componse 二、脚本准备 1. Jenkinsfile 2. deploy.sh 3. Dockerfile 三、Jenkins流水线配置 新增版本号参数 流水线选择代码里面的Je…

ffmpeg面向对象——rtsp拉流探索(1)

目录 1.tcp创建及链接的流程图及对象图2.解析 标准rtsp协议的基石是tcp&#xff0c;本节探索下ffmpeg的rtsp拉流协议tcp的socket创建及链接。 1.tcp创建及链接的流程图及对象图 tcp创建及链接的流程图&#xff0c;如下&#xff1a; tcp创建及链接的对象图&#xff0c;如下&…

QD1-P15 HTML 文本标签(textarea、label)

本节学习 HTML 常用标签&#xff1a;textarea和label ‍ 本节视频 www.bilibili.com/video/BV1n64y1U7oj?p15 ‍ 知识点1&#xff1a;textarea标签的用途 可输入多行文本的控件 cols属性&#xff1a; 文本的可见宽度 rows属性&#xff1a; 文本的可见行数 HTML示例 &l…

Map父接口

通过API可以详细查看Map接口包含的具体方法。重点的方法包括&#xff1a; 案例一&#xff1a;Map接口的基本使用 package com.map;import java.util.HashMap; import java.util.Map; import java.util.Set;/*** Map接口的使用* 特点&#xff1a;存储键值对&#xff1b;键不能重…

【数据结构】零碎知识点(易忘 / 易错)总结回顾

一、数据结构的概念 数据结构&#xff08;Data Structure&#xff09;是计算机存储、组织数据的方式&#xff0c;指相互之间存在一种或多种特定关系的数据元素的集合。 二、算法 算法&#xff08;Algorithm&#xff09;就是定义良好的计算过程&#xff0c;它取一个或一组的值为…

Python、R语言Lasso、Ridge岭回归、XGBoost分析Airbnb房屋数据:旅游市场差异、价格预测|数据分享...

全文链接&#xff1a;https://tecdat.cn/?p37839 分析师&#xff1a;Kefan Yu 在大众旅游蓬勃发展的背景下&#xff0c;乡村旅游已成为推动乡村经济、社会和文化发展的关键力量。当前&#xff0c;乡村旅游接待设施主要以招待所、小宾馆和农家乐等形式存在。然而&#xff0c;一…

二叉树改良版——AVL树

为什么说是“改良”&#xff0c;其实标题的二叉树指的是搜索二叉树&#xff0c;它虽然可以缩短查找的效率&#xff0c;但如果数据已经有序或接近有序的话二叉树就会退化成单支树&#xff0c;这样查找元素的话反而会效率低下。因此&#xff0c;为了解决这个问题&#xff0c;AVL树…

zynq 添加lwip库

在自己的项目属性中. 就是在上一行的下面加了一行配置. 多了个 -llwip4 -Wl,--start-group,-lxil,-llwip4,-lgcc,-lc,--end-group

第十四届单片机嵌入式蓝桥杯

一、CubeMx配置 &#xff08;1&#xff09;LED配置 &#xff08;1&#xff09;LED灯里面用到了SN74HC573ADWR锁存器&#xff0c;这个锁存器有一个LE引脚,这个是我们芯片的锁存引脚&#xff08;使能引脚&#xff09;&#xff0c;由PD2这个端口来控制的 &#xff08;2&#xff…

Qt初识_通过代码创建hello world

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 Qt初识_通过代码创建hello world 收录于专栏【Qt开发】 本专栏旨在分享学习Qt的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 1.通过按…

魔珐出席INSIGHT金融洞察力峰会,共探AI内容生成新范式

2024年9月27日&#xff0c;2024INSIGHT金融洞察力在北京举行&#xff0c;来自银行、保险、期货、证券、基金等行业的业界翘楚&#xff0c;共商行业热点议题&#xff0c;为金融行业增进互信、扩大合作搭建闭门平台&#xff0c;贡献价值与力量。 魔珐科技AIGC业务负责人杜子航&a…

XUbuntu安装OpenSSH远程连接服务器

目录 打开终端。更新你的包索引安装OpenSSH服务器。在终端中输入以下命令&#xff1a;安装完成后&#xff0c;OpenSSH服务器会自动启动。查看主机 IP测试连接打开 cmd 终端SSH 连接虚拟机确认连接输入连接密码发现问题修改用户&#xff0c;尝试连接 打开终端。 更新你的包索引 …

候机时间计算(数学小题目,练习时间字符串“解析”)

时间字符串的简单处理&#xff0c;可自行解析也可以调库。 (笔记模板由python脚本于2024年10月10日 18:06:42创建&#xff0c;本篇笔记适合有基本编程逻辑的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“…

MinIO 学习订阅服务

MinIO 的入门非常简单 — 只需几个简单的命令和一个 100 MB 的小二进制文件&#xff0c;您就可以立即启动并运行一个功能性开发环境。但是&#xff0c;为了在生产规模上利用 MinIO 的全部功能&#xff0c;我们鼓励专业人士更多地了解 MinIO 的广泛功能。我们推出了 MinIO 学习订…

Spring Boot课程问答:技术难题专家解答

摘要 随着信息互联网信息的飞速发展&#xff0c;无纸化作业变成了一种趋势&#xff0c;针对这个问题开发一个专门适应师生交流形式的网站。本文介绍了课程答疑系统的开发全过程。通过分析企业对于课程答疑系统的需求&#xff0c;创建了一个计算机管理课程答疑系统的方案。文章介…

企业远控私有化部署解决方案-内信互联

内信互联&#xff08;DoLink&#xff09;&#xff0c;是点量软件新推出的企业私有化远程控制系统解决方案。很多朋友对这个产品还不是很了解&#xff0c;今天点量小编就对其基础功能做一些详细说明&#xff0c;如果您想快速拥有自己的企业私有远程控制系统&#xff0c;欢迎联系…

xavier 在tensorflow pytorch中的应用,正太分布和均匀分布的计算公式不一样

Xavier初始化&#xff0c;也被称为Glorot初始化&#xff0c;是一种用于深度神经网络的权重初始化方法。这种方法是由Xavier Glorot和Yoshua Bengio在2010年的论文《Understanding the difficulty of training deep feedforward neural networks》中提出的。Xavier初始化的主要目…