前言
加减乘除看起来是 C 语言里最不需要解释的部分:+ - * /四个符号,加上取模%,再配一个printf就完事。但真正动手写一个计算器时,问题会集中冒出来:整数除法为什么把7/2算成 3?-7 % 2到底是 -1 还是 1?除以零为什么不报错而是崩溃?scanf读失败时变量里是什么?
这些都不是边角料,而是 C 语言算术语义的核心。C 的整数运算有截断规则、类型转换规则和溢出规则,三者共同决定了表达式的值;浮点运算还有精度与格式化的问题。不把这些讲清楚,写出来的计算器在正常输入下能用,换一组数据就出错。
本文按"整数运算 → 类型转换与溢出 → 输入输出 → 完整示例"的顺序讲,代码按 C11 编写,只使用标准库。
一、四个运算符与整数除法
| 运算符 | 含义 | 操作数类型 | 注意 |
|---|
+ | 加 | 整数、浮点 | 可能溢出 |
- | 减 | 整数、浮点 | 可能溢出 |
* | 乘 | 整数、浮点 | 最容易溢出 |
/ | 除 | 整数、浮点 | 整数除法向零截断;除数为零是未定义行为 |
% | 取模 | 仅整数 | 余数符号与被除数相同 |
整数除法的规则从 C99 起被明确规定:商向零截断,余数满足(a/b)*b + a%b == a。由此可以推出:
/* 适用于 C11 */
7 / 2 /* 3 */
-7 / 2 /* -3,不是 -4 */
7 / -2 /* -3 */
-7 % 2 /* -1,不是 1 */
7 % -2 /* 1 */如果预期是"向下取整"(例如把时间戳换算成天数),直接用/会在负数上偏一。需要地板除法时要自己调整:
/* 适用于 C11 */
int floor_div(int a, int b) {
int q = a / b;
int r = a % b;
if (r != 0 && ((r < 0) != (b < 0))) --q;
return q;
}%不能用于float或double,编译器会直接报错。浮点求余要用fmod(需要<math.h>)。
除数为零时,整数除法与取模都是未定义行为(undefined behavior, UB),不是"抛异常"也不是"返回 0",程序可能崩溃、可能得到一个垃圾值,也可能看起来正常。任何来自用户输入的除数都必须先检查。
二、类型转换与溢出
C 的算术运算遵循"寻常算术转换":先把两个操作数转换成共同的类型,再做运算。大致顺序是int→unsigned int→long→unsigned long→long long→unsigned long long→float→double→long double。几个实际影响:
int与double运算,int先转成double,结果是double;int与unsigned int运算,int先转成unsigned int,负数会变成一个很大的正数;- 比
int窄的类型(char、short)先经过整型提升变成int。
第二点是经典陷阱:
/* 适用于 C11 */
int a = -1;
unsigned int b = 1;
if (a < b) { /* 不会进入:a 被转换成 UINT_MAX,比 b 大 */ }有符号整数的溢出是未定义行为。int的最大值加一,标准不保证回绕成最小值,编译器可以假设它不发生并据此优化。无符号整数的溢出则是良定义的:按2^N取模回绕。
/* 适用于 C11 */
int x = 2147483647;
/* x + 1 是未定义行为,不要依赖结果 */
unsigned int u = 4294967295u;
/* u + 1 一定回绕为 0,这是标准保证的 */避免有符号溢出的办法是提前判断,或者改用更宽的类型:
/* 适用于 C11 */
#include <limits.h>
int safe_add(int a, int b, int *out) {
if ((b > 0 && a > INT_MAX - b) || (b < 0 && a < INT_MIN - b)) {
return 0; /* 溢出 */
}
*out = a + b;
return 1;
}浮点运算不会"溢出成 UB",而是得到±HUGE_VAL(无穷)或NaN;除以零的浮点运算是良定义的,结果是无穷或NaN,不会崩溃。这一点与整数除法完全不同。
三、输入与输出
读两个数和一个运算符,最直接的写法是scanf("%lf %c %lf", &a, &op, &b)。问题在于必须检查返回值:scanf返回成功赋值的项数,输入格式不符时可能只读了两个甚至一个,其余变量保持原值(如果是未初始化的局部变量,读它就是未定义行为)。
/* 适用于 C11 */
#include <stdio.h>
double a = 0.0, b = 0.0;
char op = 0;
if (scanf("%lf %c %lf", &a, &op, &b) != 3) {
fprintf(stderr, "input error\n");
return 1;
}格式说明符必须与类型匹配。printf里double用%f(%lf在printf中与%f等价,但习惯上只在scanf里写%lf);int用%d,long long用%lld,unsigned用%u。类型与说明符不匹配是未定义行为,常见现象是打印出乱七八糟的数字。
宽度与精度可以控制输出:%.6f表示保留 6 位小数,%8.2f表示总宽 8、保留 2 位、右对齐。做计算器时固定小数位能让结果更整齐。
四、完整示例:四则运算计算器
下面是一个读取"数 运算符 数"并输出结果的完整程序。用double统一处理,除法前检查零,非法运算符给出提示。
/* 适用于 C11 */
#include <stdio.h>
int main(void) {
double a = 0.0, b = 0.0, result = 0.0;
char op = 0;
if (scanf("%lf %c %lf", &a, &op, &b) != 3) {
fprintf(stderr, "usage: number operator number\n");
return 1;
}
switch (op) {
case '+':
result = a + b;
break;
case '-':
result = a - b;
break;
case '*':
result = a * b;
break;
case '/':
if (b == 0.0) {
fprintf(stderr, "division by zero\n");
return 2;
}
result = a / b;
break;
default:
fprintf(stderr, "unknown operator: %c\n", op);
return 3;
}
printf("%.6f\n", result);
return 0;
}如果要处理整数并同时输出商与余数,应当分开写,并且用更宽的类型承接乘法:
/* 适用于 C11 */
#include <stdio.h>
int main(void) {
int a = 0, b = 0;
if (scanf("%d %d", &a, &b) != 2) return 1;
if (b == 0) {
fprintf(stderr, "division by zero\n");
return 2;
}
printf("sum = %d\n", a + b);
printf("diff = %d\n", a - b);
printf("prod = %lld\n", (long long)a * b); /* 避免 int 乘法溢出 */
printf("quot = %d\n", a / b);
printf("rem = %d\n", a % b);
return 0;
}(long long)a * b先把a提升为long long,乘法就在 64 位上做,结果不会因为int溢出而变成 UB。这是处理乘法时最简单的一道保险。
五、浮点结果的比较
计算器输出的是浮点数,用户可能会拿结果与预期值比较。不要用==:
/* 适用于 C11 */
#include <math.h>
if (fabs(result - expected) < 1e-9) { /* 接近 */ }0.1 + 0.2在二进制浮点下不等于0.3,这是表示误差,不是程序错误。比较时引入容差,容差的大小要结合数值量级;对很大或很小的数,绝对容差不再合适,应改用相对误差。
常见坑点
1. 整数除法未检查除零
/* ❌ 除数为 0 是未定义行为 */
int q = a / b;
/* ✅ 先检查 */
if (b == 0) { /* 处理错误 */ }
int q = a / b;2. 不检查scanf返回值
/* ❌ 输入失败时变量是旧值或未初始化值 */
scanf("%d %d", &a, &b);
/* ✅ 检查成功项数 */
if (scanf("%d %d", &a, &b) != 2) return 1;3. 以为1/2等于 0.5
/* ❌ 两个 int 相除,结果是 0 */
double half = 1 / 2;
/* ✅ 至少一个操作数是浮点 */
double half = 1.0 / 2;4. 用数学取模的直觉理解%
/* ❌ 以为 -7 % 2 等于 1 */
int r = -7 % 2; /* 实际是 -1 */
/* ✅ 需要非负余数时自己调整 */
int r = ((-7 % 2) + 2) % 2;5. 依赖有符号溢出回绕
/* ❌ 有符号溢出是未定义行为 */
int x = INT_MAX;
int y = x + 1;
/* ✅ 运算前判断,或改用无符号/更宽类型 */6.int与unsigned混用比较
/* ❌ 负数被转换成很大的无符号数 */
if (-1 < 1u) { /* 条件为假 */ }
/* ✅ 统一类型,或显式转换并确保非负 */7.printf格式与参数类型不匹配
/* ❌ double 用 %d 输出,行为未定义 */
printf("%d\n", 3.14);
/* ✅ 类型与说明符对应 */
printf("%f\n", 3.14);8. 用==比较浮点结果
/* ❌ 表示误差导致比较失败 */
if (result == 0.3) { /* ... */ }
/* ✅ 容差比较 */
if (fabs(result - 0.3) < 1e-9) { /* ... */ }总结
| 主题 | 规则 | 风险 |
|---|
| 整数除法 | 向零截断 | 负数上偏一 |
| 取模 | 余数符号随被除数 | 与数学定义不同 |
| 除零 | 整数:UB;浮点:无穷/NaN | 必须先检查 |
| 类型转换 | 寻常算术转换 | int与unsigned混用 |
| 溢出 | 有符号:UB;无符号:回绕 | 不要依赖 UB |
| 输入 | scanf返回成功项数 | 不检查会读到垃圾值 |
加减乘除的代码只有几行,真正的功夫在边界:除零、溢出、类型转换、输入失败。把这四点显式处理掉,一个"简单"的 C 计算器才算写完整;否则它只是在"恰好能算对"的输入上看起来正确。