信息的表示和处理——整型表示及其运算

写在前面

本文是对 《csapp》 第二章 “信息的表示和处理” 做的笔记,读者有兴趣可以去读原文,以下内容全来自对原文的总结。
承接上文 《信息的表示和处理——信息存储》
ps:本文含有大量文字以及示例,可能造成不适。

整数

整型的大小在 32 位和 64 位平台上可能不一样,且 posix 为了统一差异,又引入了新的类型。

以下表格均为在 64 位平台的大小。

自带类型posix 类型大小(字节)最大值最小值
charint8_t1-128127
unsigned charuint_8t10255
shortint_16t2-32 76832 767
unsigned shortuint_16t2065535
intint_32t4-2 147 483 6482 147 483 647
unsigned intuint_32t404 294 967 295
longint64_t8-9 223 372 036 854 775 8089 223 372 036 854 775 807
unsigned longuint64_t8018 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}}} xw12w1,使多出的正数部分映射为负数。 − x w − 1 -\mathop{{x}}\nolimits_{{w-1}} xw1 又称为符号位(也就是最左边的那一位)。( 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. 无符号数所能表示的最大值。
  2. 有符号数的 -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} x2w1 x − 2 w x - 2^{w} x2w
  • x < 2 w − 1 x < 2^{w-1} x<2w1 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.hINT_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 wk 位即可。 w w w 为大类型的宽度, k k k 为小类型宽度。

对于无符号来说,丢弃 w − k w-k wk 位,就相当于 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 0x,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 2wx+y<2w+1 ,则发生溢出,那么 x + y = x + y − 2 w x + y = x + y - 2^w x+y=x+y2w

计算机处理溢出的方式很简单,就是将高位直接丢弃,这些公式都是给人“看”的。

所以无符号加法又称为模数加法,因为当溢出时会丢弃最高位,所以 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=2wx

( x + 2 w − x )   m o d   2 w = 0 (x + 2^w - x)\ mod\ 2^w = 0 (x+2wx) mod 2w=0

计算机中取逆元是通过取反+1 得到的。如以下 8 位无符号数 223 223 223 求逆为例:

1101 1111 -> 取反 0010 0000 -> 加一 0010 0001 -> 十进制数 33
与 256 - 223 = 33 一致
无符号数减法

计算机中的减法都是统一为加法进行运算的,所以 x − y x - y xy 其实是 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 2w1x,y<2w11 x , y x,y x,y 均无溢出),有:

  • 2 w − 1 ≤ x + y 2^{w-1} \le x +y 2w1x+y ,正溢出, x + y = x + y − 2 w x + y = x + y - 2^w x+y=x+y2w
  • − 2 w − 1 ≤ x , y < 2 w − 1 -2^{w-1}\le x,y < 2^{w-1} 2w1x,y<2w1 ,正常, x + y = x + y x + y = x + y x+y=x+y
  • x + y < − 2 w − 1 x + y < -2^{w-1} x+y<2w1,负溢出, 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 2w1x,y<2w11 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 s0 时发生溢出。
  • x < 0 , y < 0 x<0,y<0 x<0,y<0 时,当且仅当 s ≥ 0 s \ge 0 s0 时发生溢出。

只有当同号时相加才会发生溢出,这是一个显然的结论。

#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,所以也称为模数乘法。

不管有无符号数乘法运算的位级表示是一样的(位级具有等价性),只是在解释时不一样。

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值