🛡️

身份验证

答对题目即可进入,验证你是谁

剩余尝试次数:3

实战项目 — 命令行计算器

C语言学习教程

实战项目 — 命令行计算器

热身类比:从工具箱到工作台

想象你面前有一个工具箱,里面散落着各种工具:扳手、螺丝刀、锤子、卷尺……单独每个工具都很有用,但如果你只是把它们摊在地上,很难完成一个完整的家具组装任务。

真正有用的是把工具放在工作台上,按照步骤来

  1. 拿出设计图(程序结构)
  2. 按顺序取用工具(调用函数)
  3. 一步一步组装(程序流程)
  4. 检查成品(测试)

本章就是把前面学的所有”工具”(数据类型、运算符、控制流、函数)放在一个工作台上,组装出一个真正可用的命令行计算器

工具箱(你学过的知识)          工作台(本章项目)
┌────────────────────┐      ┌─────────────────────┐
│ 变量 & 数据类型      │      │                     │
│ 输入输出 (scanf)    │  ──→ │  命令行计算器        │
│ 运算符 (+, -, *, /)│      │  main() 组装一切     │
│ 条件判断 (if/switch)│      │                     │
│ 循环 (while)       │      └─────────────────────┘
│ 函数 (模块化)       │
└────────────────────┘

项目需求分析

在动手写代码之前,先搞清楚我们要做什么:

功能列表

功能描述示例
四种运算+ - * / 都能算10 + 5 → 15
连续计算用上一次的结果继续算10 + 5 = 15,接着 15 * 3 = 45
退出 q输入 q 退出程序
清零 c输入 c 把当前结果重置为 0
除零保护除以 0 时报错提示,不崩溃10 / 0 → 错误提示

程序流程图

        ┌─────────────┐
        │   开始       │
        └──────┬──────┘

        ┌──────▼──────┐
        │ 输入初始数字  │
        └──────┬──────┘

        ┌──────▼──────┐
    ┌──→│ 显示当前值    │
    │   └──────┬──────┘
    │          │
    │   ┌──────▼──────┐
    │   │ 读取操作符    │
    │   └──────┬──────┘
    │          │
    │     ┌────┴────┐
    │     │ 是什么?  │
    │     └────┬────┘
    │   ┌──────┼──────┬──────┐
    │   │      │      │      │
    │   ▼      ▼      ▼      ▼
    │  'q'   'c'   '+/-  '*'/'
    │   │      │     */'    │
    │   ▼      ▼      │      │
    │  退出  清零     │      │
    │          │      │      │
    │          └──┬───┘      │
    │             │          │
    │      ┌──────▼──────┐   │
    │      │ 读取操作数    │   │
    │      └──────┬──────┘   │
    │             │          │
    │      ┌──────▼──────┐   │
    │      │ switch 分发  │   │
    │      │ 调用对应函数  │   │
    │      └──────┬──────┘   │
    │             │          │
    └─────────────┘          │

                    ┌────────▼───┐
                    │  打印结果    │
                    │  程序结束    │
                    └────────────┘

完整代码(逐块详解)

第一块:头文件与运算函数

#include <stdio.h>

// ============================================
// 第一块:四个纯函数 —— 只做计算,不碰输入输出
// ============================================

/**
 * 加法
 * 纯函数:同样的输入永远产生同样的输出,没有副作用
 */
double add(double a, double b) {
    return a + b;
}

/**
 * 减法
 */
double sub(double a, double b) {
    return a - b;
}

/**
 * 乘法
 */
double mul(double a, double b) {
    return a * b;
}

/**
 * 除法(带除零保护)
 * 注意:这里加了除零检查,避免程序崩溃
 */
double divide(double a, double b) {
    if (b == 0.0) {
        printf("错误:不能除以零!操作被忽略。\n");
        return 0.0;           // 返回 0 表示操作无效
    }
    return a / b;
}

设计要点:每个函数只做计算,不涉及 printf(除了除零提示)和 scanf。这叫做关注点分离——计算归计算,界面归界面。


第二块:菜单显示函数

// ============================================
// 第二块:界面函数 —— 负责和用户交互
// ============================================

/**
 * 显示当前状态和操作提示
 * @param current  当前结果值
 */
void show_status(double current) {
    printf("\n┌─────────────────────────────┐\n");
    printf("│  当前结果: %12.2f\n", current);
    printf("├─────────────────────────────┤\n");
    printf("│ 操作: +  -  *  /            │\n");
    printf("│ 命令: c=清零  q=退出         │\n");
    printf("└─────────────────────────────┘\n");
    printf("请输入操作/命令: ");
}

设计要点:界面展示独立成一个函数,以后想改成图形界面,只换这个函数就行。


第三块:主函数 —— 组装一切

// ============================================
// 第三块:主函数 —— 把所有零件组装起来
// ============================================

int main() {
    double result = 0.0;       // 计算结果(初始值)
    char op;                   // 操作符或命令
    int running = 1;           // 程序运行标志(1=运行中, 0=停止)

    // ===== 步骤 1:显示欢迎界面 =====
    printf("╔══════════════════════════════════╗\n");
    printf("║     命令行计算器 v1.0            ║\n");
    printf("║     支持 + - * / 四种运算        ║\n");
    printf("╚══════════════════════════════════╝\n\n");

    // ===== 步骤 2:获取初始数字 =====
    printf("请输入初始数字: ");
    scanf("%lf", &result);
    getchar();                  // 吃掉 scanf 留下的换行符
                                // 注意:没有这行,下次 scanf("%c") 会读到换行符!

    // ===== 步骤 3:主循环 =====
    while (running) {
        // 3a:显示当前状态
        show_status(result);

        // 3b:读取操作符
        scanf("%c", &op);
        getchar();              // 再次吃掉换行符

        // 3c:处理命令
        if (op == 'q' || op == 'Q') {
            // 用户要退出
            running = 0;
            continue;           // 跳到循环开头,发现 running=0,退出循环
        }

        if (op == 'c' || op == 'C') {
            // 用户要清零
            result = 0.0;
            printf("结果已清零!\n");
            continue;           // 跳过后续计算,直接下一轮
        }

        // 3d:检查是否是有效运算符
        if (op != '+' && op != '-' && op != '*' && op != '/') {
            printf("未知操作: '%c',请重新输入。\n", op);
            continue;
        }

        // 3e:读取操作数
        double num;
        printf("请输入操作数: ");
        scanf("%lf", &num);
        getchar();              // 又一次吃掉换行符

        // 3f:根据操作符调用对应的函数
        switch (op) {
            case '+':
                result = add(result, num);
                break;
            case '-':
                result = sub(result, num);
                break;
            case '*':
                result = mul(result, num);
                break;
            case '/':
                result = divide(result, num);
                break;
            default:
                // 理论上不会到这里(前面已经过滤了),但安全起见保留
                printf("内部错误:未知操作符。\n");
                break;
        }
    }

    // ===== 步骤 4:退出 =====
    printf("\n╔══════════════════════════════════╗\n");
    printf("║  最终结果: %14.2f\n", result);
    printf("║  感谢使用,再见!                ║\n");
    printf("╚══════════════════════════════════╝\n");

    return 0;
}

代码结构全景图

main()

├── 初始化 (result = 0, running = 1)
├── 输入初始数字

├── while (running) 主循环 ──────────────────────┐
│   │                                             │
│   ├── show_status(result)  ← 显示界面           │
│   ├── scanf 读取操作符                           │
│   │                                             │
│   ├── [q/Q] → running = 0, continue ──────────┐ │
│   ├── [c/C] → result = 0, continue            │ │
│   ├── [无效] → 提示错误, continue               │ │
│   │                                             │
│   ├── scanf 读取操作数                           │
│   │                                             │
│   └── switch(op)                                │
│       ├── '+' → result = add(result, num)      │
│       ├── '-' → result = sub(result, num)      │
│       ├── '*' → result = mul(result, num)      │
│       └── '/' → result = divide(result, num)   │
│                                                 │
└── 退出                                          │

底层函数(被 switch 调用):
  add()   sub()   mul()   divide()
   ↑        ↑       ↑        ↑
   └────────┴───────┴────────┘
        独立、可测试、可复用

运行示例

╔══════════════════════════════════╗
║     命令行计算器 v1.0            ║
║     支持 + - * / 四种运算        ║
╚══════════════════════════════════╝

请输入初始数字: 100

┌─────────────────────────────┐
│  当前结果:        100.00    │
├─────────────────────────────┤
│  操作: +  -  *  /            │
│  命令: c=清零  q=退出         │
└─────────────────────────────┘
请输入操作/命令: +
请输入操作数: 50

┌─────────────────────────────┐
│  当前结果:        150.00    │
├─────────────────────────────┤
│  操作: +  -  *  /            │
│  命令: c=清零  q=退出         │
└─────────────────────────────┘
请输入操作/命令: *
请输入操作数: 3

┌─────────────────────────────┐
│  当前结果:        450.00    │
├─────────────────────────────┤
│  操作: +  -  *  /            │
│  命令: c=清零  q=退出         │
└─────────────────────────────┘
请输入操作/命令: /
请输入操作数: 9

┌─────────────────────────────┐
│  当前结果:         50.00    │
├─────────────────────────────┤
│  操作: +  -  *  /            │
│  命令: c=清零  q=退出         │
└─────────────────────────────┘
请输入操作/命令: q

╔══════════════════════════════════╗
║  最终结果:           50.00       ║
║  感谢使用,再见!                ║
╚══════════════════════════════════╝

常见错误诊所

错误 1:忘记 getchar() 吃掉换行符

// ❌ 错误:scanf("%lf") 后残留的换行符被 scanf("%c") 读走
int main() {
    double result;
    char op;
    printf("请输入: ");
    scanf("%lf", &result);
    // 用户输入 100 然后按回车
    // scanf 读走了 100,但换行符 '\n' 还留在输入缓冲区

    printf("操作: ");
    scanf("%c", &op);       // op 读到了 '\n',而不是用户想输入的 '+'!
    return 0;
}

// ✅ 正确:每次 scanf 后用 getchar() 清掉换行符
int main() {
    double result;
    char op;
    printf("请输入: ");
    scanf("%lf", &result);
    getchar();              // 吃掉残留的 '\n'

    printf("操作: ");
    scanf("%c", &op);       // 现在 op 正确读到了用户输入
    getchar();              // 养成习惯:每次 scanf 后都清理
    return 0;
}

错误 2:除零检查不严谨

// ❌ 不够安全:只检查了 b 是否为字面上的 0.0
double bad_divide(double a, double b) {
    if (b == 0.0) return 0.0;  // 浮点数比较可能有精度问题
    return a / b;
}

// ✅ 更稳健:检查绝对值是否足够小
#include <math.h>

double better_divide(double a, double b) {
    if (fabs(b) < 1e-10) {     // 1e-10 = 0.0000000001,几乎等于 0
        printf("错误:除数为零或接近零!\n");
        return 0.0;
    }
    return a / b;
}

错误 3:switch 忘记 break

// ❌ 忘记 break → 会"穿透"到下一个 case
switch (op) {
    case '+': result = add(result, num);   // 执行完后……
    case '-': result = sub(result, num);   // 接着又执行这个!
    case '*': result = mul(result, num);   // 然后又执行这个!
    // 结果:不管输入什么操作符,最终执行的都是最后一个 case
}

// ✅ 每个 case 后面必须跟 break
switch (op) {
    case '+': result = add(result, num); break;  // ← break 不要忘!
    case '-': result = sub(result, num); break;
    case '*': result = mul(result, num); break;
    case '/': result = divide(result, num); break;
}

错误 4:变量未初始化

// ❌ 危险:result 没赋初值,里面是随机垃圾数据
int main() {
    double result;           // 未初始化!
    // ... 如果用户立刻按 'q' 退出 ...
    printf("结果: %.2f\n", result);  // 打印出什么?不知道!随机的!
}

// ✅ 总是给变量一个初始值
int main() {
    double result = 0.0;     // 明确初始化为 0
    // ...
}

错误 5:无限循环写错条件

// ❌ 可能的错误:忘记更新 running,或者条件永远为真
int main() {
    int running = 1;
    while (running) {
        // ... 某些逻辑 ...
        if (op == 'q') {
            // 忘记写 running = 0;
            // 程序永远退不出来!只能 Ctrl+C 强制结束
        }
    }
}

// ✅ 确保有明确的退出路径
int main() {
    int running = 1;
    while (running) {
        // ...
        if (op == 'q' || op == 'Q') {
            running = 0;     // 明确的退出
            continue;
        }
        // ...
    }
}

扩展挑战(巩固所学)

基础版本跑通之后,试试加这些功能:

挑战 1:幂运算 ^

// 提示:用循环实现
double power(double base, int exp) {
    double result = 1.0;
    for (int i = 0; i < exp; i++) {
        result *= base;
    }
    return result;
}
// 别忘了在 switch 里加 case '^'

挑战 2:历史记录

用数组保存最近 10 次结果:

#define HISTORY_SIZE 10
double history[HISTORY_SIZE];
int history_count = 0;

// 每次计算后:
history[history_count % HISTORY_SIZE] = result;
history_count++;

// 按 'h' 显示历史

挑战 3:取余运算 %

int mod(int a, int b) {
    if (b == 0) {
        printf("错误:取余不能除以 0!\n");
        return 0;
    }
    return a % b;
}
// 注意:取余只适用于整数,需要转换

挑战 4:单位换算模式

// 按 'u' 进入单位换算子菜单
void unit_converter() {
    printf("1: cm→inch  2: inch→cm  3: kg→lb  4: lb→kg\n");
    // ...
}

实践建议

  1. 先不看书,自己写一遍:即使写得很烂,也比照着抄强
  2. 故意引入 bug 然后修复:比如删掉一个 getchar(),看看会发生什么
  3. 加注释:用自己的话解释每一段代码在干嘛
  4. 换个界面风格:把菜单改成你的风格,比如改成中文菜单
  5. 让别人测试:把你的程序给同学用,看看他们会不会发现 bug

要点速查

知识点在项目中的体现对应代码
函数定义四个运算函数各司其职add(), sub(), mul(), divide()
变量作用域result 在 main 内,函数内看不到局部变量
循环控制while(running) 主循环 + continue 跳转程序骨架
switch-case根据操作符分发到不同函数核心分发逻辑
scanf/printf用户输入输出,注意 getchar() 清缓冲I/O 交互
条件判断除零检查、退出判断、无效输入过滤防御性编程
浮点精度double 而不是 float 保存结果更高精度

项目心法:写程序不是从上往下一行行敲,而是先画结构(函数分成几块),再实现细节(每个函数内部怎么写),最后组装(main 里怎么调用)。这个思维习惯比任何语法知识都重要。