写C的实习生遇到怪事:一个成绩变量减到负数后变成了几千万的正数,打印出来完全不对。这就是有符号整型溢出。搞懂它的表示方式,很多看似灵异的数据错误都能解释。
它和无符号的区别
整型占几个字节决定取值范围。以32位为例,有符号int用最高位当符号位,0表示正、1表示负,范围是负21亿多到正21亿多。无符号int把最高位也算数值,范围是0到42亿多。同样的二进制位,解释方式不同,结果就不同。
补码是怎么表示的
- 正数:原样存二进制。
- 负数:取对应正数的二进制按位取反,再加一。
- 好处:加减法不用单独处理符号,减法统一变成加法,电路更简单。
几个容易混的类型
short存16位,范围约正负3万;int在主流平台是32位;long则随平台变,Windows的long是32位,Linux的long是64位。跨平台代码里最坑的就是long,同样的声明在两个系统上宽度不同,序列化到文件再读回来就会错位。
语言之间表现不同
Java的整型宽度固定,int永远是32位,long永远是64位,好预测。C和C++只规定了下限,不保证具体宽度。PHP遇到整数溢出会自动转成浮点,结果开始不精确。Python的int任意精度,不会溢出但会变慢。
溢出为什么危险
有符号溢出是未定义行为,编译器可以做任何假设,包括直接删掉你的判断。经典案例是判断正数加一后是否大于原数,编译器认定永远成立,于是这段安全检查被优化没了。无符号溢出则是明确定义的,会按2的幂取模回绕。
实际怎么防
做加法前先判断是否会越过上限,比如a大于最大值减去b。用更高位宽类型临时存,比如int相加用long long。开启编译器的溢出检查,GCC的-ftrapv或UBSan能在运行时抓到。涉及金额一律用定点或高精度库,别拿浮点也别硬扛整型边界。