https://github.com/xxg1413/CSAPP/blob/master/Chapter02/2.0.md
https://zhuanlan.zhihu.com/p/104019655
1 使用二进制进行编码
现代计算机存储和处理的信息是以二值信号表示的,是基于二进制进行编码的,好处在于:
- 可以将低电压表示0,将高电压表示1,如果电路中存在噪音或不完善的地方,只要不超过你设定的阈值,你就会得到一个清晰的信号;
- 对于信息存储而言,存储一位信息或一个数字值比存储一个模拟值更容易。
当我们将若干个二进制数组合在一起,再加上某种解释,就能给这些二进制数赋予特定的含义,这个“解释”就是编码。如:
-
对于文档中的字符和符号,我们可以使用标准的字符码将二进制数与字符和符号对应起来;
-
对于数字表示,我们可以使用无符号编码来表示大于或等于零的数字,可以使用补码来表示有符号整数,可以使用浮点数编码来表示数字的科学计数法。
由于浮点数和整型数使用不同的编码规则,所以即使他们存储相同的数字,可能二进制序列也不相同。
计算机的表示法是用有限的位来表示无穷的数字。
1.1 整数与浮点数的不同
而整数和浮点数对数字处理方式的不同,导致了它们具有不同的性质:
- 整数
- 能够编码一个相对较小的精确数值范围;
- 这就使得整数会发生溢出问题;
- 整数的计算机运算能满足真正整数运算的性质,比如结合律和交换律;
- 浮点数
- 能够编码一个较大的近似数值范围;
- 浮点数虽然能够保证两个整数相乘一定是正的;
- 但是浮点运算有可能不会满足一些运算性质,如结合律: ( x + y ) + z = x + ( y + z ) (x+y)+z = x+(y+z) (x+y)+z=x+(y+z)就不一定满足。
1.2 无符号、有符号、浮点数的编码
- 无符号整数的编码:使用传统的二进制表示法(原码),大于等于零的整数;
- 有符号整数的编码:最常见的是使用补码进行表示,可正可负;
- 浮点数的编码:以2为奇数的实数科学计数法;
下面开始介绍:
- 不同信息如何存储,以及基础运算方法;
- 介绍整数的表示、整数的运算。
2 信息的表示和处理
2.1 信息的存储
计算机将8个bit当做一个块,称为字节(Byte),作为可寻址的最小内存单位。
而操作系统给每个进程提供了虚拟内存的抽象,让进程都能访问从相同地址开始的、连续的虚拟内存空间,每个内存单位都有唯一的编码进行标识,这个编码称为地址(Address),而所有地址的集合就构成了虚拟内存空间。
2.1.1 字长
问:虚拟内存空间最大能有多大呢?
答:主要取决于计算机的一个参数——字长(Word Size),我们可以将若干个字节当做一个块,称为字(Word),而这里的**字节的数目就是字长。字长指明了指针数据的标称大小(Nominal Size),而指针指向虚拟内存空间,它的位数就决定了它能索引多大的空间。由此也就规定了虚拟内存空间的最大大小。所以虚拟空间的最大大小由字长决定**。
字长定义了 操作系统通常处理多大的值和 算数运算,并且指针和地址大小也是字长确定的。
现在大部分机器是32位字长或者64位字长的。
而程序可以通过不同的编译指令将其编译成32位程序或者64位程序(程序的字长是由编译决定的);
- 32位机器可以运行32位程序,但是不能运行64位程序;
- 而64位机器可以运行32位程序和64位程序。
2.1.2 字长对数据类型大小的影响
并且32位程序和64位程序对C数据类型的典型大小也有影响:
2.1.3 如何固定数据大小?
为了避免由于依赖典型大小和不同编译器设置带来的兼容性问题,ISO C99引入了数据大小固定,不随编译器和机器设置而变化的数据类型,比如int32_t
就是4字节的int
类型,int64_t
就是8字节的int
类型。
注意:不同字长的机器中,指针的大小也就不同,并且不同机器/操作系统配置使用不同的存储分配规则,会使得指针的长度和内容差很多。
2.1.4 字节排列方法:小端法|大端法
内存是一系列字节,我们可以根据字长将其划分成不同的字(Word),每个字的地址是该字中最低位的地址。
字:任意多的字节组合起来称之为一个字。
字长:该字的长度称为。
当你某个数据对象跨越了多个字节,则在虚拟内存空间中,这个多字节对象是存在连续的地址中,并且该对象的地址是所使用字节中最小的地址。可以根据该对象的类型来确定字节数目。
而多个字节的排列方法分为两种:小端法(Little Endian)和大端法(Big Endian)。
- 小端法:最低有效字节在较小的内存地址中;
- 大端法:最低有效字节在较大的内存地址中。
不同机器支持不同排列方法;
有的机器还可以支持双端法:也就是可以把它配置成作为大端或者小端的机器运行。
// 测试
namespace test01 {
/************01 内存大端法、小端法***********/
typedef unsigned char *pointer;
void show_bytes(pointer start, size_t len)
{
size_t i;
for(i=0; i<len; i++)
{
printf("%p\t%d\n", start + i, start[i] );
}
printf("\n");
}
void main()
{
int a = 511;
printf("/************01 内存大端法、小端法***********/\n");
printf("int a = 511, 其二进制形式为:0000 0000|0000 0000|0000 0001|1111 1111\n");
show_bytes((pointer)&a, sizeof(int));
}
}
---------------------------------------
结果:
/************01 内存大端法、小端法***********/
int a = 511, 其二进制形式为:0000 0000|0000 0000|0000 0001|1111 1111
0061FF0C 255 // 255 二进制形式:1111 1111
0061FF0D 1 // 1 二进制形式:0000 0001
0061FF0E 0
0061FF0F 0
从结果可以看出:由于int占用4个字节,所以我的电脑使用的是:小端法。
注意:
-
一般而言,机器所使用的的字节顺序是不可见的,但是在有些情况下需要注意字节顺序带来的影响:
- 如果两个机器通过网络进行数据传输,而这个两个机器使用的字节排列方法不一样,就会造成问题。所以网络应用程序的代码编写必须遵守已建立的关于字节顺序的规则, 确保发送方机器将它的内部表示转换成网络标准,而接收方机器将网络标准转换为它自己的内部表示。
- 当我们通过反汇编器得到可执行程序的指令序列时,字节顺序也很重要。
-
当我们保存字符串时,会使用某个标准编码将字符串中的所有字符都转换为对应的编码,比如ASCII字符码、Unicode编码,并且还会在字符串末尾加上全零的二进制数作为字符串结尾。
**注意:**在使用ASCII作为字符码的任何系统上,都将得到相同的结果,与字节顺序和字大小规则无关。
-
当我们保存代码时,如上一节介绍的,编译器首先会将源文件编译成字节表示的机器代码,是由不同的机器指令构成的,需要注意的是,不同机器类型使用不同的且不兼容的机器指令和指令编码方式,所以同一段代码在不同机器中编译的结果是不同的且不兼容的。
2.2 C语言中的基本运算
2.2.1 位运算
C中的整型数据类型是使用二进制数进行编码的,二进制数可以对应为布尔代数的位向量,所以它可以支持按位的布尔运算,比如|
表示OR、&
表示AND、~
表示NOT、^
表示XOR。
可参考:chapterO:位操作
2.2.2 逻辑运算
C提供了一组逻辑运算符||
、&&
和!
分别对应于命题逻辑中的OR
、AND
和NOT
运算。
要注意逻辑运算和位级运算的区别,在逻辑运算中,只要是非零的数据就表示为TRUE
,全零的数据就表示为FALSE
,所以计算时候先将其转换为TRUE
和FALSE
,然后计算出来的结果只会是0x00
或0x01
,分别对应FALSE
和TRUE
。
逻辑运算中有一个特点:提早终止(Early Termination)。当计算逻辑与和逻辑或时,如果只通过左侧的式子就已经能得到最终的结果,就不会再计算右侧的式子了。如:
int i=0;
int ans1=i&&(++i);
//因为i=0,所以计算逻辑与时能够直接得到结果是FALSE,就无需计算右侧的++i了。所以ans=0,i=0
int ans2=i||(++i);
//由于i=0计算逻辑或时无法直接得到结果,所以还需要计算右侧的式子,此时i=1,所以ans=1,i=1
1.2.3 移位运算
C语言表示的二进制编码,可以对其进行移位操作。
-
x向左移k位,表示为
x<<k
,此时会丢弃最高的k位,并在右侧补充k个零。 -
x向右移k位,表示为
x>>k
,由于整型编码的问题,会将右移操作分为逻辑右移和算数右移,- 逻辑右移:就是丢弃最低的k位,并在左侧补充k个零;
- 而算数右移中,因为有符号数使用最高位来表示数字的正负性,为了保证数字的正负性不变,就丢弃最低的k位,并在左侧补充k个最高有效位的值。(即,如果最高位为1,则这里补1;如果最高位为0(此时就是正数的左移右移),则这里补0);(可参考:负数的左移和右移)
**注意:**当k的值大于数据类型的位数w时,有些机器和JAVA会通过计算
k mod w
来确定位移量,所以对于8位的数左移8位,该数不变。但是C语言没有限制,所以要自己保证 0 ≤ k < w 0≤ k <w 0≤k<w。
可参考:chapterO:位操作
3 整数的表示
C语言提供了多种由不同字节数目构成的、具有不同范围的整型数据类型,并且每种整型数据类型都有**有符号(signed)和无符号(unsigned)**两个版本,
-
常量默认是有符号版本,
-
可以加上后缀
u
或者U
来将其指定为无符号版本。
可以发现,有符号数的取值范围是不对称的,负数的范围比正数范围大1。
给定一串二进制编码来表示整数,具体如何解释这些二进制主要取决于它的编码方式,对相同的二进制采用不同的编码方式得到的整数结果是不同的,接下来介绍编码无符号数和有符号数的方法。
3.1 无符号整数、有符号整数的编码
具体内容可参考:原码、补码、反码,关于公式的讲解:[读书笔记]CSAPP:3[VB]整型数据类型中的“2.2 无符号数的编码、2.3 有符号数的编码”
简单可以理解为:
- 无符号整数的编码:使用传统的二进制表示法**(原码)**,大于等于零的整数;
- 有符号整数的编码:最常见的是使用补码进行表示,可正可负;
- 浮点数的编码:以2为奇数的实数科学计数法;
3.2 有符号数和无符号数之间的转换(同样字长的类型之间)
C语言可以在各种不同的数字类型之间做强制类型转换,它的具体实现要从位级角度来看,它保持位值不变,只是改变了解释这些位的方式。
3.1.1 无符号数—>有符号数
看无符号数的最高位是否为1:
- 如果不为为0,则有符号数就直接等于无符号数;
- 如果无符号数的最高位为1,则将无符号数取补码,得到的数就是有符号数。
unsigned char a = 2;
unsigned char b = 130;
printf("无符号数2(占1字节,其最高位不为1),转换为补码表示时,其值为:%d\n", (char)a);
printf("无符号数130(占1字节,其最高位为1),转换为补码表示时,其值为:%d\n", (char)b);
char c = 3;
char d = -7;
printf("有符号数3(占1字节,其最高位不为1),转换为原码表示时,其值为:%u\n", (unsigned char)c);
printf("有符号数-7(占1字节,其最高位为1),转换为原码表示时,其值为:%u\n", (unsigned char)d);
// 结果
无符号数2(占1字节,其最高位不为1),转换为补码表示时,其值为:2
无符号数130(占1字节,其最高位为1),转换为补码表示时,其值为:-126
上面代码中:
- 2的原码是:0000 0010,可知最高位不为1,因此转为有符号数之后也是2。
- 130的原码是:1000 0010,可知最高位为1,因此需要取它的补码,补码为1111 1110,这是一个负数,取最高位作为-号,取最低7位作为数值得到的结果是-126。
3.1.2 有符号数—>无符号数
看有符号数的最高位是否为1:
- 如果不为为0,则无符号数就直接等于有符号数;
- 如果有符号数的最高位为1,则将有符号数取补码,得到的数就是无符号数。
char c = 3;
char d = -7;
printf("有符号数3(占1字节,其最高位不为1),转换为原码表示时,其值为:%u\n", (unsigned char)c);
printf("有符号数-7(占1字节,其最高位为1),转换为原码表示时,其值为:%u\n", (unsigned char)d);
// 结果
有符号数3(占1字节,其最高位不为1),转换为原码表示时,其值为:3
有符号数-7(占1字节,其最高位为1),转换为原码表示时,其值为:249
上面代码中:
- 3的原码是:0000 0011,可知最高位不为1,因此转为无符号数之后也是3。
- -7的原码是:1000 0111,可知最高位为1,因此需要取它的补码,补码为1111 1001,这是一个正数,因此整个数的值就是249。
3.3 有符号数隐式转化为无符号数
在C语言中,当一个有符号数和一个无符号数进行计算时,会隐式地将有符号数转化为无符号数。当进行逻辑判断时,可能会出现问题:
比如-1<0U
,首先有0U
是无符号类型,所以会先将-1
转化为无符号类型,就需要加上
2
w
2^w
2w ,就使得值变得很大,所以结果为0。
**比较的最好方法是将其转换成二进制编码,然后根据特定编码进行计算。**比如
2147483547
,它是补码的最大值,二进制编码为 [ 0 , 1 , 1 , . . . , 1 ] [0,1,1,...,1] [0,1,1,...,1] ,而-2147483547-1
是补码的最小值,二进制编码为 [ 1 , 0 , 0 , . . . , 0 ] [1,0,0,...,0] [1,0,0,...,0],当将这两个二进制编码转化成无符号数时,肯定第二个值更大。其他依次类推。
由于有符号数到无符号数的隐式转换,可能会导致错误或漏洞,因此建议绝不使用无符号数。但是如果我们想把字看成是位的集合,而没有实际意义,则无符号数非常有用。
3.4 不同字长的类型转换
在不同字长的整数之间进行类型转换,要保持在数据类型范围内的数值是不变的。以下有两种情况:
- 从较短字长的数据类型转换到较长字长的数据类型,比如short到int,就需要进行扩展位;
- 从较长字长的数据类型转换到较短字长的数据类型,比如int到short,就需要截断位。
3.4.1 扩展位
我们想要在不改变值的情况下进行扩展。
-
对于无符号数,根据无符号数编码的定义,我们可以直接在位向量的前端扩展0,这个称为零扩展(Zero Extension)。
-
对于有符号数(补码),我们直接在在位向量的前端扩展最高有效位的值(即最高位为1,则扩展1;最高位为0,则扩展0),这个称为符号扩展(Sign Extension)。
3.4.2 有符号数、无符号数之间转换与扩展位的顺序
当扩展位 以及 有符号数和无符号数之间 的顺序变化时,会影响最终计算出来的结果:
假设下面转换过程,都需要进行“扩展位”
转换关系 | 过程(扩展位 的位置不同) | 特点 |
---|---|---|
补码(有符号数)——>无符号数 | 补码(有符号数)——>无符号数——>扩展位(零扩展) | 头部扩展时,扩展为0 |
无符号数——>补码(有符号数) | 无符号数——>补码(有符号数)——>扩展位(符号扩展) | 如果无符号数大于 2 w − 1 2^w -1 2w−1,则转换后的补码小于0。符号扩展头部都为1。 |
补码(有符号数)——>无符号数⭐ | 补码(有符号数)——>扩展(符号扩展)——>无符号数 | 如果补码小于0,则符号扩展的头部都是1。 |
无符号数——>补码(有符号数)⭐ | 无符号数——>扩展(零扩展)——>补码(有符号数) | 头部全为0 |
**需要注意:**如果先进行扩展,则位数w发生变化后,无符号数转化为补码的阈值 T M a x w TMax_w TMaxw以及变化的量 2 w 2^w 2w会发生变化。
**C语言标准要求先进行扩展位,再进行有符号和无符号转换。**即,使用上表的3、4是更优的。
3.4.3 截断位
将一个大的数据类型转换为小的数据类型时,不管是无符号数还是有符号数都是简单地进行位截断。
- 无符号数的数值大小可能因截断而变化,
- 而有符号数不仅数值大小可能变化,符号位也可能发生改变,如8位二进制数00011001(25)转换为4位数截断的结果是1001(-7).
4 整数运算
关于本章的运算公式,详见参考1.
计算机中计算都是通过二进制数来计算的,所以无论是无符号数还是有符号数,计算得到的位模式是相同的(这也保证了他们使用相同的算法、指令进行运算)。
但是由于有限位以及编码方式(无符号数、补码)的限制,可能会导致计算机计算的结果和真实结果之间存在差异,也就发生了溢出。
**溢出:**完整的计算结果不能放到数据类型的字长限制中。
接下来会探讨不同编码不同运算的计算结果。
4.1 无符号数加法
w位的无符号数的取值范围为 [ 0 , 2 w ) [0,2^w) [0,2w)。其中最大值对应的位向量为 [ 1 , 1 , . . . , 1 ] [1,1,...,1] [1,1,...,1],当计算结果大于等于 2 w 2^w 2w时,就需要w+1位来表示,此时直接去掉第w+1位的结果,就是计算机最终计算的结果(就是相加后,直接去掉超过的位数)。
而直接去掉w+1之后的位,相当于是计算结果对 2 w 2^w 2w取模,所以该方法称为模数加法。模数加法是可交换、可结合的。
**加法逆元:**利用溢出效应,每个元素都有一个加法逆元,当与自己的加法逆元相加时,就得到0。
**注意:**无符号数的加法逆元和数学上的相反数数值不同。
即,当两个无符号的非零数相加=0时。原理就是:在发生溢出时,将溢出部分去掉之后,后面的w位都为0,此时结果就是0。
运算的公式,详情见参考1。
4.2 无符号数减法
涉及到减法运算,都是按照补码的运算法则进行的。可以参考[3.2 有符号(补码)加法](##3.2 有符号(补码)加法)
例如:
unsigned char e_4 = 5;
unsigned char f_4 = 6;
计算,无符号数的减法: 5-6 = -1?
// 参照 有符号加法 的规则,计算:[x-y]补 = [x+(-y)]补 = [x]补 + [-y]补
[5]补 = 0000 0101
[-6]补= 1111 1010
[-1]补= 1111 1111
开始计算:
0000 0101
+ 1111 1010
----------------
1111 1111
4.2 有符号(补码)加法
因为补码的出现,就可以让计算机 将加法转化为减法计算。所以,此小结标题叫“补码的运算(加、减)”更醒目。
-
当计算有符号数
x+y
的加法时,有:[x+y]补 = [x]补 + [y]补
,例如:66+51的值为117? [66]补 = 0100 0010 [51]补 = 0011 0011 [117]补= 0111 0101 开始计算: 0100 0010 + 0011 0011 ---------------- 0111 0101
-
当计算有符号数
x-y
的减法时,采用的策略是:将其转化为加法来计算:[x-y]补 = [x+(-y)]补 = [x]补 + [-y]补
。例如:66-51的值为15 ? [66]补 = 0100 0010 [-51]补= 1100 1101 [15]补 = 0000 1111 开始计算: 0100 0010 + 1100 1101 ---------------- 1 0000 1111 ---------------- 由于我们使用8bits(假设为8bits)来表示一个数,而计算结果我们可以看出,有9位。所以,我们要舍弃最高位的那个1.结果如下: 0000 1111
运算的公式,详情见参考1。
4.2.1 溢出问题
对于有符号的加法(Two’s Complement Addition),操作过程和无符号加法一样,只是解释的时候会有不同,因此会得到**正溢出(positive overflow)和负溢出(negative overflow)**两种。
- 正溢出是数值太大,把原来为 0 的符号位修改成了 1,反而成了负数;
- 负溢出是数值太小,把原来为 1 的符号位修改成了 0,反而成了正数。
/*例子(假设使用8bits来表示一个数)*/
// 1. 负溢出:127 + 2 = -127?
[127]补 = 0111 1111
[2]补 = 0000 0010
[-127]补= 1000 0001
开始计算:
0111 1111
+ 0000 0010
----------------
1000 0001
// 2. 正溢出:(-128) + (-1) = 127?
[-128]补 = 1000 0000
[-1]补 = 1111 1111
[127]补 = 0111 1111
开始计算:
1000 0000
+ 1111 1111
----------------
1 0111 1111
---------------- 由于我们使用8bits(假设为8bits)来表示一个数,而计算结果我们可以看出,有9位。所以,我们要舍弃最高位的那个1.结果如下:
0111 1111
4.3 无符号数乘法
对于两个w位的无符号数相乘,会得到2w位的数,计算机会截断得到低w位作为计算结果。
即,无符号数乘法的结果,也可以使用模运算来得到其结果。如下图所示,两数相乘之后在对 2 w 2^w 2w取模:
4.4 补码乘法
对于两个w位的补码相乘,也是得到2w位的数,同样截断低w位作为结果,在计算时使用对 2 w 2^w 2w 取余进行截断,即可的到乘法后的结果(结果也是一个补码,即有符号数)。
即,补码乘法与无符号数乘法:规则是相同的。只是结果在计算机中解释不同:
- 无符号数乘法的结果会被解释为 无符号数;
- 补码乘法的结果会被解释为 补码;
如:使用4bits表示一个数,计算:5*5 = -7?
0101
× 0101
--------------
0101
0000
0101
0000
--------------
0011001
-------------- // 只保留最低位的4位,计算结果仍然是一个 补码
1001
5 计算机实现乘除法
计算机中支持各种整数运算,比如加法、减法、位级运算和移位。
而大多数机器中**,整数乘法指令和整数除法指令都很慢,通常需要几十个时钟周期,所以计算机通常会用移位和加减法的组合来代替乘除法**。
5.1 计算机实现乘法
首先我们讨论乘上2幂的特殊情况,然后将其扩展到乘上任意数。
5.1.1 乘以2的幂
如果在w位的表示范围内,左移k位相当于乘上了 2 k 2^k 2k(当然,有可能会溢出)。
证明:
- 对于无符号数 [ x w − 1 , x w , . . . , x 0 ] [x_{w-1},x_w,...,x_0] [xw−1,xw,...,x0],如果我们左移k位,可得到的值为:
- 而对于补码的计算结果的位向量(就是二进制的序列)和无符号数计算结果的位向量相同,所以对于补码x左移k位,得到的结果是: x ∗ w t 2 k x\ *^t_w\ 2_k x ∗wt 2k
由此,乘上2幂,只要左移幂次就行。
5.1.2 乘上任意数
对于x乘上 任意数k:我们可以得到k的位向量,然后根据位向量进行移位并相加,就能得到乘法运算结果。
例如:k=14。
- 首先,将k按照2的幂次进行展开: 14 = 2 3 + 2 2 + 2 1 14 = 2^3 + 2^2 + 2^1 14=23+22+21。
- 此时 x ∗ 14 = x ∗ 2 3 + x ∗ 2 2 + x ∗ 2 1 = ( x < < 3 ) + ( x < < 2 ) + ( x < < 1 ) x*14 = x*2^3 + x*2^2 + x*2^1 = (x<<3)+(x<<2)+(x<<1) x∗14=x∗23+x∗22+x∗21=(x<<3)+(x<<2)+(x<<1)。由此,就将一个乘法运算转化为了3个移位操作和2个加法操作。
- 更进一步我们可以得到 14 = 2 4 − 2 1 14 = 2^4 - 2^1 14=24−21,所以 x ∗ 14 = ( x < < 14 ) − ( x < < 1 ) x*14 = (x<<14)-(x<<1) x∗14=(x<<14)−(x<<1),转化为了2个移位操作和1个减法操作。
大多数编译器只有在需要少量移位、加法和减法时才用这种优化,不然就直接使用一个乘法操作了。
5.2 计算机实现除法
同乘法相同,我们可以通过右移操作来除以2的幂。
- 对于无符号数,我们使用逻辑右移来除以2的幂;
- 而对于补码,我们要用算术右移来保持符号不变。
- 我们右移1位时,必要还要保持符号位。然后将其他的数字右移。即:1.我们首先保留符号位,2.然后将原来的补码整体右移1位。
- 当补码右移除不尽时:
- 补码大于0,也是向0舍入;
- 补码小于0,结果是远离0进行取整的,不符合预期。详见[5.2.1 补码小于0且除不尽时,使用“偏移量”](###5.2.1 补码小于0且除不尽时,使用"偏移量")
- 当补码右移除不尽时:
- 我们右移1位时,必要还要保持符号位。然后将其他的数字右移。即:1.我们首先保留符号位,2.然后将原来的补码整体右移1位。
在除法运算中,比较麻烦的是出现除不尽的情况,此时就需要舍入,我们希望计算结果都是向0舍入的。
例如:
// 1. 无符号整数(逻辑右移) 3÷2 = ?(还是按照4bits表示一个数) (3÷2) 用右移表示:(3>>1)= (0011 >> 1) = 0001 = 1 //如此,当除不尽时,我们是向0向下舍入的。 // 2. 补码(算术右移) -6 ÷ 2 = ? -3÷2=? (-6 ÷ 2) 用右移表示:(-6 >> 1) = (1010 >> 1) = 1101 = -3 //补码右移1位:首先保留符号位的值不变,然后将原来补码整体右移1位
5.2.1 补码小于0且除不尽时,使用“偏移量”
偏移量:
首先需要一个公式:
⌈
x
/
y
⌉
=
⌊
(
x
+
y
−
1
)
/
y
⌋
\left \lceil{x/y}\right \rceil = \left \lfloor {(x+y-1)/y}\right \rfloor
⌈x/y⌉=⌊(x+y−1)/y⌋(补码小于0时)。其中,我们添加的偏移量为(y-1)
。
上述证明中:
- 如果x是y的整数q倍,即可整除。结果为q;
- 如果x不是y的整数q倍,则不可整除。结果为(q+1);
由于是右移操作,即相当于是除以2。因此我们可以将y替换为
2
k
2^k
2k,因此**偏移量为
(
2
k
−
1
)
(2^k - 1)
(2k−1)。**也可写成(1<<k) -1
的形式。所以,对于补码小于0时,加上偏移量保证其向0取整。
所以C中使用算数右移的表达式为:
(x<0 ? x+(1<<k)-1 : x) >> k
//保证补码在正数和负数下都是向0取整的。
例子:
// 错误
(-3 ÷ 2) 用右移表示:(-3 >> 1) = (1101 >> 1) = 1110 = -2 //这里计算结果是-2,但使用计算机算出来是-1
// 正确:对于补码小于0时,进行右移时,需要先添加一个偏移量,在进行右移。
-3 : 1101
先添加偏移量: 1 // 因为除以2,相当于右移1位,所以偏移量= 2^1 -1 = 1
-------------------
1110
在进行右移1: 1111 // 右移规则如“5.2 计算机实现除法”所示。
6 整数运算例子
**例子1:**如果使用无符号数,可能会出现一些比较诡异的异常。
当运行到i=0
时,对应的位模式为全零,这时候再减一,得到的是全一,对应到无符号编码得到的就是
U
M
a
x
w
UMax_w
UMaxw。所以i不会小于0,所以循环不会停止,或者出现索引错误。
for(unsigned i=n-1;i>=0;i--){
//loop
}
参考:
- https://zhuanlan.zhihu.com/p/104019655