C 语言简单加减乘除运算 前言加减乘除看起来是 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结果是doubleint与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.hint 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.hdouble 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里写%lfint用%dlong long用%lldunsigned用%u。类型与说明符不匹配是未定义行为常见现象是打印出乱七八糟的数字。宽度与精度可以控制输出%.6f表示保留 6 位小数%8.2f表示总宽 8、保留 2 位、右对齐。做计算器时固定小数位能让结果更整齐。四、完整示例四则运算计算器下面是一个读取数 运算符 数并输出结果的完整程序。用double统一处理除法前检查零非法运算符给出提示。/* 适用于 C11 */#include stdio.hint 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.hint 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.hif (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 计算器才算写完整否则它只是在恰好能算对的输入上看起来正确。