☰
C 语言简单加减乘除运算
2026/10/31 16:53:58 网站建设 项目流程

前言


加减乘除看起来是 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 计算器才算写完整;否则它只是在"恰好能算对"的输入上看起来正确。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询