1. 原码、反码和补码
-
原码,即二进制定点表示法:最高位为符号位,其余位表示数值的大小。
-
反码,为了解决减法运算错误而设计:正数的反码与其原码相同,负数的反码是对其原码数值部分逐位取反。
-
补码,为了解决正负零问题而设计:正数的补码与其原码相同,负数的补码是其反码加一。
1.1 减法运算错误
原码无法正确进行减法运算,而反码和补码可以,这里以 1 + (-1) = 0
为例说明。
使用原码运算:0000 0001 + 1000 0001 = 1000 0010 = -2
使用反码运算:0000 0001 + 1111 1110 = 1111 1111 = 0
1.2 正负零问题
原码和反码都存在零的两种表示,即正零和负零;补码只存在一种,即 0000 0000
。
原码 | 反码 | 补码 | |
---|---|---|---|
正零 | 0000 0000 | 0000 0000 | 0000 0000 |
负零 | 1000 0000 | 1111 1111 | 0000 0000 |
1.3 计算机内部运算形式
计算机内部以补码的形式进行运算,原因有三:
-
能做到符号位和数值部分一起运算,无需单独考虑符号。
-
能把减法运算转化为加法运算,一并处理。
-
补码没有正零和负零之分,表示范围比原码/反码多一。
2. 整数表示范围
n n n bits 有符号整数的表示范围 [ − 2 n − 1 , 2 n − 1 − 1 ] [-2^{n-1}, 2^{n-1}-1] [−2n−1,2n−1−1],无符号整数的表示范围 [ 0 , 2 n − 1 ] [0, 2^n-1] [0,2n−1],这里以8位整数为例说明。
-
正数在计算机中表示为原码,或者说正数的原/反/补码均相同。最大正数为
0111 1111
,即 2 8 − 1 − 1 = 127 2^{8-1}-1=127 28−1−1=127。 -
负数在计算机中表示为补码。-1 的补码为
1111 1111
,-127 的原码为1111 1111
,补码为1000 0001
。
由于补码中正零和负零均编码为 0000 0000
,补码能够表示的整数应该比原码/反码多一个。经过排列组合,发现补码 1000 0001 ~ 1111 1111 对应负整数 -127 ~ -1,0000 0000 ~ 0111 1111 对应非负整数 0 ~ 127,补码 1000 0000
表示的整数似乎成谜。
解释一: 补码 1000 0000
表示的整数是 -128。这不是强行规定的,其原因与高位截断有关。-128 的原码 1 1000 0000
,计算其补码 1 1000 0000 -> 1 0111 1111 -> 1 1000 0000
,高位截断后恰好是 1000 0000
。因此,-128 的补码就是 1000 0000
。
解释二: 考虑到在有限存储空间内,负零与 -128 的原码相同,可以勉强理解为在负零的位置表示 -128。
错误理解: 不能说 -128 没有原码和反码,只能说在有限存储空间内,-128 只能表示为补码,或者说 1000 0000
的原码和反码在有限存储空间内不存在。
3. 机器相关性
C/C++ 编程的陷阱之一是整数类型的表示范围与机器/编译器相关,突出表现在 int
整型的表示范围上。
和Java等语言不同,C/C++ 本身并没有规定各数据类型的位数,只是限定了一个大小关系,short <= int <= long <= long long。至于具体哪种类型占用多少位,由编译器决定的。
编译器位数 | char | short int | int | long int | long long int |
---|---|---|---|---|---|
16 位编译器 | 1 | 2 | 2 | 4 | 8 |
32 位编译器 | 1 | 2 | 4 | 4 | 8 |
64 位编译器 | 1 | 2 | 4 | 8 | 8 |
K&R C 标准规定长整型不得比短整型短,仅此而已。
ANSI C90 标准规定了数据类型间的大小关系,即 short <= int <= long <= long long。同时,它还规定了各种整型最小允许范围,short/int >= 2 bits,long >= 4 bits。
ANSI C90 标准引入 long long int 类型。
byte | char | short int | int | long int | long long int | |
---|---|---|---|---|---|---|
32 位 C/C++ | 1 | 2 | 4 | 4 | 8 | |
Java | 1 | 2 | 2 | 4 | 8 |