写在前面
本文是对 《csapp》 第二章 “信息的表示和处理” 做的笔记,读者有兴趣可以去读原文,以下内容全来自对原文的总结。
承接上文 《信息的表示和处理——信息存储》。
ps:本文含有大量文字以及示例,可能造成不适。
整数
整型的大小在 32 位和 64 位平台上可能不一样,且 posix 为了统一差异,又引入了新的类型。
以下表格均为在 64 位平台的大小。
自带类型 | posix 类型 | 大小(字节) | 最大值 | 最小值 |
---|---|---|---|---|
char | int8_t | 1 | -128 | 127 |
unsigned char | uint_8t | 1 | 0 | 255 |
short | int_16t | 2 | -32 768 | 32 767 |
unsigned short | uint_16t | 2 | 0 | 65535 |
int | int_32t | 4 | -2 147 483 648 | 2 147 483 647 |
unsigned int | uint_32t | 4 | 0 | 4 294 967 295 |
long | int64_t | 8 | -9 223 372 036 854 775 808 | 9 223 372 036 854 775 807 |
unsigned long | uint64_t | 8 | 0 | 18 446 744 073 709 551 615 |
32 位平台上,long 和 unsigned long 以及指针的大小为 32 位。
以上所说均为 c/c++,其默认为有符号类型的,支持无符号类型,当有符号与无符号一同计算时会统一为无符号计算。
在 Java 中所有类型大小都是固定的,且支持有符号类型。
观察上表发现,正数与负数具有不对称性,负数所表示的数永远比正数多一个,因为 0 占用了正数所表示的一个位置。
编码
现在我们需要建立一个映射关系,使一个二进制数和十进制数一一映射。
-
无符号数编码:直接将二进制数转为十进制数即可。
-
有符号数编码:因为要表示负数,所以需要补一个偏置 − x w − 1 2 w − 1 {-\mathop{{x}}\nolimits_{{w-1}}\mathop{{2}}\nolimits^{{w-1}}} −xw−12w−1,使多出的正数部分映射为负数。 − x w − 1 -\mathop{{x}}\nolimits_{{w-1}} −xw−1 又称为符号位(也就是最左边的那一位)。( w w w 为数据宽度)
如以下 8 bit 数据
二进制:1111 1111
无符号:128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255
二进制:1 111 1111
有符号:-2^7 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = -1
再次印证了,底层存储的是一串毫无意义的二进制序列,到底是什么意思完全取决于如何解读。
此处表明了,全 1 的串有两种解读方式:
- 无符号数所能表示的最大值。
- 有符号数的 -1。
引入原码、反码、补码
在很多书上,没有告知有符号数是如何编码的,而是告知有符号数采用补码编码,我觉得是一种本末倒置。所以我在此之前先引入了有符号数如何编码。
原码:将原来是的十进制直接转为二进制,其对应的就是上文的无符号数编码(如果是有符号数,则最高位为符号位,1 负 0 正)。
反码:除符号位以外的所有位置取反。
补码:将反码 + 1,其对应的就是上文中的有符号数编码。
有符号: -28
原码: 1001 1100
反码: 1110 0011
补码: 1110 0100
类型转换
在此前我们一直强调,计算机底层存储的是一串毫无意义的二进制序列。对于几乎一切的编程语言都遵循的这样的规定。
有符号与无符号转换
有无符号转换只改变解读方式,不改变底层序列。
有符号数 -> 无符号数
有符号正数的符号位是 0,所以有符号数正数转为无符号数值不变。
有符号负数的符号位是 1,所以解读时符号位要改变解读方式。
关注下列有符号负数:
有符号: -28
二进制: 1110 0100
无符号: 128 + 64 + 32 + 4 = 228 = 256 + (-28)
所以我们可以归纳出公式:
- 正数:无符号数 = 有符号数
- 负数:无符号数 = 有符号数 + 2 w 2^w 2w
有符号数的补码表示
在此之前,我们得到的补码表示采用的是原码 -> 反码 -> 补码,逐渐转换的方式。
因为类型转换只改变解读方式,所以我们可以直接将其转换为无符号数对其做十进制转二进制。该方式转换比之前的方式要快得多。
看个例子:
有符号: -28 原码: 1001 1100 反码: 1110 0011 补码: 1110 0100 有符号: -28 -> 无符号: 256 + (-28) = 228 补码: 1110 0100
无符号数 -> 有符号数
在此之间我们介绍了,整数的编码方式。而无符号数转有符号数,也就无需多说。直接使用有符号编码即可,可以整理为以下公式:
- x ≥ 2 w − 1 x \ge 2^{w-1} x≥2w−1: x − 2 w x - 2^{w} x−2w
- x < 2 w − 1 x < 2^{w-1} x<2w−1: x x x
看个例子:
无符号: 228 > 128
有符号: 228 - 256 = -28
无符号: 127 < 128
有符号: 127
不对称性的坑
因为不对称性的存在,所以会存在一个坑,这个坑从本质来说不是不对称性引起的,而是负数值引起的。看示例:
printf("%d\n", sizeof(INT_MAX));
printf("%d\n", sizeof(INT_MIN));
printf("%d\n", sizeof(-2147483648));
这段代码,很多人想当然的以为输出都是 4。实际上是 4、4、8。
原因是这个负数,其实是一个单元运算符 -
和 2147483648
构成的表达式。我们都知道 int
最大值是 2147483647
显然溢出,这是一个未定义行为,每个编译器的结果都不一样,所以就不具备可移植性。
gcc 的处理方式很简单,就是将其转化为一个 long
然后在取负号,这时的值就能被 int
装下了。
那如果是 -9223372036854775808
呢?直接将把 9223372036854775808
当转为 unsigned long
,
这时的值就能被 unsigned long
装下,然后强制转换为 long
。有无符号的转换只会改变解释,不会改变二进制序列。9223372036854775808
按照有符号解析时就会变成 -9223372036854775808
。
实际上根本不需要类型转换就能容纳得下,也就是说可以避免为定义行为,只需要 -9223372036854775807 - 1
就可以了。
所以这就是为什么 limits.h
中 INT_MIN = -INT_MAX - 1
的原因了。
自动转换的坑
无符号数与有符号数参与运算,会默认自动转换为无符号数。
以下代码,一运行就是段错误。
int sum(const int arr[], unsigned int n) {
int res = 0;
for (int i = 0; i <= n - 1; i++) {
res += arr[i];
}
return res;
}
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int res = sum(arr, 0);
printf("%d\n", res);
}
数组中前 0
个元素之和(也就是不进 for
循环),数组都没有访问,为什么会报段错误呢?
真的吗?这其实就是自动转换的坑,这里的 n - 1
并不是等于 -1
而是等于 4294967295
。
这种坑很多,常用的很多函数,如 strlen
返回的 size_t
就是 unsiged long
,一不留神就会掉入坑中。
整型间的转换
当一个小的整型转换为大的整型时,我们就需要扩展原先的整型。同样的当一个大的整型转换小的整型,我们需要截断原先的整型。
不同大小间的转换,宗旨是尽量保证转换后的数与转换前的数相同。大转小不一定能保证,但是小转大一定能保证。
整型的扩充
小转大一定要保证转换前后的数相同,基于这个想法,就有两种扩展方式:
- 无符号的零扩展:扩展前面补 0。
- 有符号的符号扩展:扩展前面补符号位。
如以下 8 位扩展为 16 位:
无符号: 87
8 位: 0101 0111 -> 16 位: 0000 0000 0101 0111
有符号: -87
8 位: 1010 1001 -> 16 位: 1111 1111 1010 1001
整型的截断
尽量保证转换前后一致,就算不能保证全部,但是起码一个大整型的变量的数落在小整型变量的范围内还是能够转换的。
如 long a = 123L; int b = a;
起码这种要保证前后一致。基于这个想法,其实只需要丢弃前面的
w
−
k
w - k
w−k 位即可。
w
w
w 为大类型的宽度,
k
k
k 为小类型宽度。
对于无符号来说,丢弃 w − k w-k w−k 位,就相当于 m o d 2 k mod\ 2^k mod 2k。而对于有符号来说,相当于解释为无符号先做 m o d 2 k mod\ 2^k mod 2k,再解释为有符号。
如以下 16 位截断为 8 位:
16 位无符号: 3000
16 位无符号: 3000 -> 8 位无符号: 3000 % 256 = 184
16 位无符号: 0000 1011 1011 1000 -> 8 位无符号: 1011 1000 = 184
16 位有符号: -3000
16 位有符号: -3000 -> 16 位无符号: -3000 + 65536 = 62536 -> 8 位无符号: 35536 % 256 = 72 -> 8 位有符号: 72
16 位有符号: 1111 0100 0100 1000 -> 8 位有符号: 0100 1000 = 72
转换优先级
c 中是先改变大小,在改变是否符号。
short a = -12345;
unsigned int b = a;
printf("%u\n", b); // 输出是 4294954951
-12345
short: 0xcfc7 -> int: 0xffff cfc7
有无符号二进制序列相同,以无符号的方式解释 0xffff cfc7 = 4294954951
整数运算
字节膨胀
每个数都能表示为 w w w 位数,然而要表示他们的和至少需要 w + 1 w+1 w+1 位。如果要完整的表示所有结果,那么 w w w 将会无限膨胀下去。
然而大部分语言(如:c/c++、Java)都不会允许字节膨胀,采取的方式也很简单,将最高位丢弃。那么这样一来,对于两个有符号正数相加就有可能变为了负数,这就是我们所说的“溢出”。
无符号数
无符号数加法
对满足 0 ≤ x , y < 2 w 0\le x,y < 2^w 0≤x,y<2w ( x , y x,y x,y 均无溢出),有:
- x + y < 2 w x + y < 2^w x+y<2w ,正常,那么 x + y = x + y x + y = x + y x+y=x+y。
- 2 w ≤ x + y < 2 w + 1 2^w\le x + y < 2^w+1 2w≤x+y<2w+1 ,则发生溢出,那么 x + y = x + y − 2 w x + y = x + y - 2^w x+y=x+y−2w 。
计算机处理溢出的方式很简单,就是将高位直接丢弃,这些公式都是给人“看”的。
所以无符号加法又称为模数加法,因为当溢出时会丢弃最高位,所以 s = x + y s = x + y s=x+y 相当于 s = ( x + y ) m o d 2 w s = (x + y)\ mod\ 2^w s=(x+y) mod 2w。
如以下两个 8 位无符号数相加:
1111 1111 -> 对应十进制 255
0000 0001 -> 对应十进制 1
1 0000 0000
最高位舍弃后,结果为0,与 (255 + 1) mod 256 = 0 结果一致。
无符号溢出检测
设 s = x + y s = x + y s=x+y ,当且仅当 s < x s < x s<x 或 s < y s < y s<y 是发生溢出。
无符号数求逆元
补充一些离散数学的基本知识
无符号数加法满足阿贝尔群(具有交换律、结合律、以及逆元、单位元)。
- a o p b = a a\ op\ b = a a op b=a ,则 b b b 为单位元。如: 1 + 0 = 1 1 + 0 = 1 1+0=1 , 0 0 0 为单位元。
- a o p b = 0 a\ op\ b = 0 a op b=0,则 a a a 与 b b b 互为逆元。如: 1 + ( − 1 ) = 0 1 + (-1) = 0 1+(−1)=0, 1 1 1 与 − 1 -1 −1 互为逆元
w w w 位的无符号 x x x 逆元记为 − x -x −x:
- x = 0 x = 0 x=0 时, − x = x -x = x −x=x。
- x > 0 x > 0 x>0 时, − x = 2 w − x -x = 2^w - x −x=2w−x。
( x + 2 w − x ) m o d 2 w = 0 (x + 2^w - x)\ mod\ 2^w = 0 (x+2w−x) mod 2w=0
计算机中取逆元是通过取反+1 得到的。如以下 8 位无符号数 223 223 223 求逆为例:
1101 1111 -> 取反 0010 0000 -> 加一 0010 0001 -> 十进制数 33
与 256 - 223 = 33 一致
无符号数减法
计算机中的减法都是统一为加法进行运算的,所以 x − y x - y x−y 其实是 x + ( − y ) x + (-y) x+(−y)。而这里的 − y -y −y 可不是取负,而是 y y y 的逆元,毕竟无符号数可不能表示负数。
如以下两个 8 位无符号数相减:
253 - 223
253 + (-223)
223 取逆元 256 - 223 = 33
253 + 33 = 286 溢出 286 - 256 = 30,与 253 - 223 = 30 结果一致
再次强调,计算机中没有减法,都是统一成加法进行运算的,包括下面将要讲到的有符号数。
有符号数
有符号数加法
对满足 − 2 w − 1 ≤ x , y < 2 w − 1 − 1 -2^{w-1}\le x,y < 2^{w-1} -1 −2w−1≤x,y<2w−1−1 ( x , y x,y x,y 均无溢出),有:
- 2 w − 1 ≤ x + y 2^{w-1} \le x +y 2w−1≤x+y ,正溢出, x + y = x + y − 2 w x + y = x + y - 2^w x+y=x+y−2w
- − 2 w − 1 ≤ x , y < 2 w − 1 -2^{w-1}\le x,y < 2^{w-1} −2w−1≤x,y<2w−1 ,正常, x + y = x + y x + y = x + y x+y=x+y
- x + y < − 2 w − 1 x + y < -2^{w-1} x+y<−2w−1,负溢出, x + y = x + y + 2 w x + y = x + y + 2^w x+y=x+y+2w
有符号数溢出检测
对满足 − 2 w − 1 ≤ x , y < 2 w − 1 − 1 -2^{w-1}\le x,y < 2^{w-1} -1 −2w−1≤x,y<2w−1−1 ( x , y x,y x,y 均无溢出),设 s = x + y s = x +y s=x+y :
- x > 0 , y > 0 x>0,y>0 x>0,y>0 时,当且仅当 s ≤ 0 s \le 0 s≤0 时发生溢出。
- x < 0 , y < 0 x<0,y<0 x<0,y<0 时,当且仅当 s ≥ 0 s \ge 0 s≥0 时发生溢出。
只有当同号时相加才会发生溢出,这是一个显然的结论。
#define Tmax 0x7FFFFFFF
#define Tmin 0x80000000
/* 异号相加一定不会溢出
同号相加才可能会溢出
1. 当两个正数相加结果为负数时,出现正溢出
2. 当两个负数相加结果为正数时,出现负溢出
3. 当两个数相加符号位没改变时,没出现溢出
*/
int saturate_add(int x, int y) {
int sx = x >> 31;
int sy = y >> 31;
int ss = (x + y) >> 31;
/* 检测是否溢出 */
int fl = !(sx ^ sy) && (sx ^ ss);
/* INT_MAX 与 INT_MIN 差 1,正溢出偏置置 0,负溢出偏置置 1 */
int offset = fl & sx;
return INT_MAX + offset;
}
有符号数求逆元
w w w 位的有符号 x x x 逆元记为 − x -x −x:
- x = 最 小 值 x = 最小值 x=最小值 时, − x = 最 小 值 -x = 最小值 −x=最小值。
- x > 最 小 值 x > 最小值 x>最小值 时, − x = − x -x = -x −x=−x。
有符号数的逆元就是取相反数,因为有符号数可以有负数。
计算机中取逆元是通过取反+1 得到的,如 123 123 123 取反:
0111 1011 -> 取反 1000 0100 -> 加一 1000 0101 -> 十进制有符号数 -123
整数乘法
关于乘法,两个 w w w 位数相乘最少需要 2 w 2w 2w 位,所以为了避免“字节膨胀”也是直接截断。造成的后果就是相当于对结果 m o d 2 w mod\ 2^w mod 2w,所以也称为模数乘法。
不管有无符号数乘法运算的位级表示是一样的(位级具有等价性),只是在解释时不一样。