实战项目 — 命令行计算器
C语言学习教程
实战项目 — 命令行计算器
热身类比:从工具箱到工作台
想象你面前有一个工具箱,里面散落着各种工具:扳手、螺丝刀、锤子、卷尺……单独每个工具都很有用,但如果你只是把它们摊在地上,很难完成一个完整的家具组装任务。
真正有用的是把工具放在工作台上,按照步骤来:
- 拿出设计图(程序结构)
- 按顺序取用工具(调用函数)
- 一步一步组装(程序流程)
- 检查成品(测试)
本章就是把前面学的所有”工具”(数据类型、运算符、控制流、函数)放在一个工作台上,组装出一个真正可用的命令行计算器。
工具箱(你学过的知识) 工作台(本章项目)
┌────────────────────┐ ┌─────────────────────┐
│ 变量 & 数据类型 │ │ │
│ 输入输出 (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");
// ...
}
实践建议
- 先不看书,自己写一遍:即使写得很烂,也比照着抄强
- 故意引入 bug 然后修复:比如删掉一个
getchar(),看看会发生什么 - 加注释:用自己的话解释每一段代码在干嘛
- 换个界面风格:把菜单改成你的风格,比如改成中文菜单
- 让别人测试:把你的程序给同学用,看看他们会不会发现 bug
要点速查
| 知识点 | 在项目中的体现 | 对应代码 |
|---|---|---|
| 函数定义 | 四个运算函数各司其职 | add(), sub(), mul(), divide() |
| 变量作用域 | result 在 main 内,函数内看不到 | 局部变量 |
| 循环控制 | while(running) 主循环 + continue 跳转 | 程序骨架 |
| switch-case | 根据操作符分发到不同函数 | 核心分发逻辑 |
| scanf/printf | 用户输入输出,注意 getchar() 清缓冲 | I/O 交互 |
| 条件判断 | 除零检查、退出判断、无效输入过滤 | 防御性编程 |
| 浮点精度 | 用 double 而不是 float 保存结果 | 更高精度 |
项目心法:写程序不是从上往下一行行敲,而是先画结构(函数分成几块),再实现细节(每个函数内部怎么写),最后组装(main 里怎么调用)。这个思维习惯比任何语法知识都重要。