在计算机中,整数的算术运算往往不符合日常直觉。例如,两个正数 2147483647 和 1 相加,结果却是一个负数 -2147483648。这是因为运算结果超出了整数类型的表示范围,这种现象被称为 溢出 。本教程将系统介绍整数类型常见的算术运算,包括加法、取反、乘法、除法,并详细解释溢出时的处理方式,帮助您理解计算机底层的运算规则。
算术运算概述
先看一个典型的溢出例子(Java 语言):
// Java语言
public static void main(String[] args) {
int i1 = 2147483647;
int i2 = 1;
System.out.printf("2147483647+1=%d\n", i1+i2);
}
// 输出结果:2147483647+1=-2147483648
正数相加得到负数,根本原因是 结果超出了 int 类型(32 位有符号)的表示范围,系统对结果进行了截断。下面我们逐个讲解各种运算的溢出规则。
整数加法
(1)无符号整数加法运算
对两个 w 位的无符号整数,有 0 ≤ x, y < 2^w,真实和为 x + y。如果用 w 位表示结果,当 x + y ≥ 2^w 时,运算产生溢出。系统会对结果进行截断,只保留低 w 位。
例如 w = 4 的场景:

用 +^u_w 表示 w 位无符号整数加法,规则为:
- 正常:
x + y(当x + y < 2^w) - 溢出:
x + y - 2^w(因为截断时舍去了最高位的2^w)
(2)有符号整数加法运算
对两个 w 位的有符号整数(补码表示),范围是 -2^(w-1) ~ 2^(w-1)-1。当真实结果 x + y 超出此范围时,就会产生溢出。有两种溢出情况:
- 正溢出:
x + y ≥ 2^(w-1),截断后结果变为负数。 - 负溢出:
x + y < -2^(w-1),截断后结果变为正数。
例如 w = 4 的场景:

注意:两个 w 位的负数相加,即使结果需要 w+1 位表示,也可能是正常场景。这取决于截断后 w 位的最高位(符号位)——若为 1 则结果正常(负),若为 0 则产生负溢出(正)。
规则如下:
- 正溢出:真实值
x + y,实际补码结果为x + y - 2^w。 - 负溢出:真实值
x + y,实际补码结果为x + y + 2^w。
为什么负溢出场景下,w 位一定是 0?
负溢出条件为
x + y < -2^(w-1),即x + y + 2^w < 2^(w-1)。由于x, y均为负数,它们的 w 位二进制表示最高位都是 1,相加后第 w 位(进位)必然为 0,否则无法满足不等式。
小提示:无符号整数和有符号整数的加法运算本质一样,都可以拆解为 4 步:
- 先计算出真实加法结果值。
- 对结果用
w+1位二进制表示。 - 截断,保留低 w 位。
- 将截断后的 w 位二进制值转换回十进制(无符号用无符号编码,有符号用补码编码)。
整数取反(求相反数)
取反即求相反数,给定整数 x,它的相反数 -x 满足 x + (-x) = 0。
(1)无符号整数的取反
对 w 位无符号整数 x:
- 当
x = 0时,-x = 0。 - 当
x > 0时,只有在溢出场景下才能得到 0,即x + (-x) = 2^w,因此-x = 2^w - x。
规则:-^u_w = (2^w - x) mod 2^w,即 2^w - x(当 x ≠ 0)。
例如 w = 8:
// C++
int main() {
uint8_t i1 = 0;
uint8_t i2 = -i1;
printf("i1=%u\n", i1);
printf("-i1=%u\n", i2);
uint8_t i3 = 100;
uint8_t i4 = -i3;
printf("i3=%u\n", i3);
printf("-i3=%u\n", i4);
printf("2^8-i3=256-%u=%u\n", i3, 256-i3);
return 0;
}
// 输出结果
i1=0
-i1=0
i3=100
-i3=156
2^8-i3=256-100=156
(2)有符号整数的取反
对 w 位有符号整数 x:
- 当
x ≠ TMin_w(最小负数-2^(w-1))时,-x = -x(正常范围)。 - 当
x = TMin_w时,-x = TMin_w(因为2^(w-1)不在范围内,溢出后自身等于自身)。
规则:-^s_w = -x(正常),-^s_w = TMin_w(当 x = TMin_w)。
例如 w = 8:
// C++
int main() {
int8_t i1 = -128;
int8_t i2 = -i1;
printf("i1=%d\n", i1);
printf("-i1=%d\n", i2);
int8_t i3 = 100;
int8_t i4 = -i3;
printf("i3=%d\n", i3);
printf("-i3=%d\n", i4);
return 0;
}
// 输出结果
i1=-128
-i1=-128
i3=100
-i3=-100
整数乘法
基本乘法与溢出
乘法与加法类似:无符号和有符号整数的乘法运算也可以拆成“计算真实值→二进制表示→截断→转换”这 4 步。结果最多需要 2w 位表示。
无符号整数例子(w=8):
// C++
int main() {
uint8_t i1 = 100;
uint8_t i2 = 2;
uint8_t i3 = i1 * i2;
printf("normal: i1 * i2 = %d * %d = %d\n", i1, i2, i3);
uint8_t i4 = 100;
uint8_t i5 = 3;
uint8_t i6 = i4 * i5;
printf("overflow: i4 * i5 = %d * %d = %d\n", i4, i5, i6);
return 0;
}
// 输出结果
normal: i1 * i2 = 100 * 2 = 200
overflow: i4 * i5 = 100 * 3 = 44

有符号整数例子(w=8):
// C++
int main() {
int8_t i1 = -50;
int8_t i2 = 2;
int8_t i3 = i1 * i2;
printf("normal: i1 * i2 = %d * %d = %d\n", i1, i2, i3);
int8_t i4 = -128;
int8_t i5 = 127;
int8_t i6 = i4 * i5;
printf("overflow: i4 * i5 = %d * %d = %d\n", i4, i5, i6);
return 0;
}
// 输出结果
normal: i1 * i2 = -50 * 2 = -100
overflow: i4 * i5 = -128 * 127 = -128

移位优化乘法
大部分机器上,乘法运算消耗 3~10 个时钟周期,而加法或位运算仅需 1 个时钟周期。因此,可以用左移操作替代乘以 2 的幂。
对 x 左移 k 位,相当于乘以 2^k(不考虑截断)。考虑截断时,左移 k 位等同于 x * 2^k mod 2^w 或 (x * 2^k) mod 2^w(有符号按补码)。
例如 w=8 的无符号数:
// C++
int main() {
uint8_t i1 = 100;
uint8_t i2 = 4;
uint8_t i3 = i1 * i2;
printf("i1 * i2 = %d * %d = %d\n", i1, i2, i3);
uint8_t k = 2;
uint8_t i4 = i1 << k;
printf("i1 << k = %d << %d = %d\n", i1, k, i4);
return 0;
}
// 输出结果
i1 * i2 = 100 * 4 = 144
i1 << k = 100 << 2 = 144
有符号数例子:
// C++
int main() {
int8_t i1 = -50;
int8_t i2 = 4;
int8_t i3 = i1 * i2;
printf("i1 * i2 = %d * %d = %d\n", i1, i2, i3);
int8_t k = 2;
int8_t i4 = i1 << k;
printf("i1 << k = %d << %d = %d\n", i1, k, i4);
return 0;
}
// 输出结果
i1 * i2 = -50 * 4 = 56
i1 << k = -50 << 2 = 56
问题:如何将乘以任意常数 K 转换为移位操作?
将 K 分解为二进制形式,例如 14 = 1110_2,可以表示为 (x<<3)+(x<<2)+(x<<1) 或 (x<<4)-(x<<1)。通用公式:若 K 的二进制中连续的 1 序列从高位 n 到低位 m,则 x * K = (x << (n+1)) - (x << m)。
例如:
// C++
int main() {
int8_t x = 5;
int8_t K = 14;
printf("x * 14 = %d\n", x*K);
printf("(x<<3) + (x<<2) + (x<<1) = %d\n", (x<<3)+(x<<2)+(x<<1));
printf("(x<<4) - (x<<1) = %d\n", (x<<4)-(x<<1));
return 0;
}
// 输出结果
x * 14 = 70
(x<<3) + (x<<2) + (x<<1) = 70
(x<<4) - (x<<1) = 70
整数除法
除法运算比乘法更慢,通常需要 30 个时钟周期以上。除以 2 的幂可以用右移操作代替,但需要注意取整方式。
- 无符号整数:除以
2^k,右移 k 位相当于向下取整(floor)。 - 有符号整数(补码):右移 k 位默认是算术右移(保留符号),对于负数,直接右移是向下取整(即向负无穷方向),而非向零取整。要实现向零取整,需要先加上偏置
(1<再右移。
例子(w=8):
// C++
int main() {
int8_t x = -50;
int8_t K = 4;
printf("x / 4 = %d\n", x/K);
printf("x >> 2 = %d\n", x>>2);
printf("(x+(1<<2))>>2 = %d\n", (x+(1<<2))>>2);
uint8_t y = 50;
uint8_t Z = 4;
printf("y / 4 = %d\n", y/Z);
printf("y >>> 2 = %d\n", y>>2);
printf("(y+(1<<2))>>>2 = %d\n", (y+(1<<2))>>2);
return 0;
}
// 输出结果
x / 4 = -12
x >> 2 = -13
(x+(1<<2))>>2 = -12
y / 4 = 12
y >>> 2 = 12
(y+(1<<2))>>>2 = 13
注意:有符号负数的直接右移会得到 -13(向下取整),而通过加偏置 (1< 对于无符号数,加偏置反而会得到向上取整,因此不需要。-12。
常见问题(FAQ)
- 问:为什么两个正数相加会变成负数?
答:这是有符号整数加法正溢出。真实结果超过了2^(w-1)-1,截断后最高位(符号位)变为 1,因此解释为负数。 - 问:所有负数相加都会溢出吗?
答:不一定。两个 w 位负数相加,如果结果在-2^(w-1)到-1范围内,则正常;只有当结果小于-2^(w-1)时才会发生负溢出。请看前文的判定规则。 - 问:为什么取反操作中,最小负数取反等于自身?
答:因为-TMin = 2^(w-1),这已经超出了有符号整数的最大正值,按补码规则溢出后重新映射为TMin。 - 问:用移位代替乘法时,需要考虑溢出吗?
答:需要。移位操作本身也会产生截断,效果与乘法完全一致。例如100 << 2得到 144 而非 400,正是因为溢出。 - 问:除法用右移替代时,无符号和有符号有什么区别?
答:无符号右移是逻辑右移,自然向下取整;有符号右移是算术右移,对负数向下取整(向负无穷),若需向零取整,必须加上偏置((1<)后再右移。
总结:本教程详细讲解了整数四种算术运算(加法、取反、乘法、除法)的底层原理,特别强调了溢出时的截断规则。理解这些规则有助于编写健壮的代码,避免因溢出导致的逻辑错误,同时也让您掌握性能优化技巧(如移位代替乘除)。希望这份教程能帮助您扎实地掌握计算机算术运算的本质。
