第二章 信息的表示和处理(第一小节 信息存储)
这是本书中第一部分的内容,这第一部分讲解的是程序结构与执行,我们在了解了计算机是由处理器与存储器子系统组成后,就需要考虑我们用什么方法来表示基本数据类型,然后机器指令怎么操作这些数据,以及编译器如何将C程序编译为这样的机器指令,后面我们还继续了解硬件资源是怎样被拿来执行指令的,理解了这些我们就能知道怎样编写C程序以及如何编译它们来使程序性能最大化,这一部分最后会以存储器子系统的设计而结尾,这是计算机系统中最复杂的一部分之一。好了,现在开始第二章笔记之旅吧!
作者先介绍了二进制,然后提及了三种二进制表示法和整数与浮点数的溢出的情况以及算术运算,最后解释研究这些的好处
为何计算机使用二进制:对于具有10个手指头的我们来说,更喜欢用十进制来对数据进行表示,但是计算机却是更喜欢用二进制值来构造存储和处理信息,因为二进制值信号很容易地被表示、存储和传输,比如可以表示穿孔卡片上有洞或无洞、导线上的高电压或低电压、顺时针和逆时针的磁场等,而且对二值信号进行存储和执行计算的电子电路非常简单和可靠,数百万甚至数十亿个这样的电路能就集成在一个单独的硅片上。这要是用十进制存储信息的电子电路就可说不准能有这么强大了。
组合的位:讲道理,单个位并不是非常有用,只是能用0和1来表示两种状态而已,但是组合在一起的位,通过某些解释,也就是位模式,就能够表示任何有限集合的元素。例如二进制数字系统中,使用位组来编码非负数,也可以使用标准的字符码可以来对字母和符号进行编码。
三种重要的数字表示:无符号编码、补码编码、浮点数编码。无符号编码就是传统的二进制表示法,表示大于等于0的数;补码就是可以表示有符号的整数,也就是正整数或负整数;而浮点数编码就是表示实数的科学计数法的以2为基数的版本。计算机就是通过这些不同的数字表示方式来进行算术运算,比如加法减法、除法乘法这些。
整数与浮点数的数学属性:因为计算机是用有限的位数来对一个数字编码,所以当结果太大就有可能出现溢出这种情况。而整数与整数的乘积发生的溢出就违背了正常的整数运算的特性,因为可能会得到一个负数。而浮点数与浮点数的乘积发生的溢出是不会得出负数的。这就是整数与浮点数的不同的数学属性。这一切的原因是因为它们处理数字表示有限性的方式不同,整数是要表示一个精确的数,只能编码一个相对较小的范围,而浮点数就是因为可以编码一个相对较大的范围,所以只能表示一个近似的数。这可能就是俗话说的狗急跳墙,就那么小的范围,逼急了走不出去,那就只有跳墙了,所以结果偏差很夸张。
研究本章的目的:研究数字的实际表示,能让我们了解表示的值的范围和不同算术运算的属性,也为了能让编写的程序能在全部数值范围内正确工作以及跨越不同机器、操作系统和编译器组合的移植性,很多安全漏洞就是是算术运算的微妙细节引起的的。了解几种不同的二进制表示方式,也是为了后面的机器级编程做准备。通过直接操作数字的位级表示,我们得到了几种进行算术运算的方式(加减乘除?),通过了解这些技术对于理解编译器产生的机器级代码有重要帮助的,因为编译器会试图优化算术表达式求值的性能。程序员需要了解计算机运算与整数和实数之间的运算之间的关系。
然后讲述信息存储这个大概念
虚拟地址空间:计算机用8位的块,也就是字节,来作为最小的可寻址的内存单位,内存被机器级程序视为一个巨大的字节数组,称为虚拟内存。内存中的每个字节都是由一个唯一的数字来标识,称为它的地址,这些地址集合起来就叫做虚拟地址空间,虚拟地址空间只是给机器级程序展示的一个概念性映像,具体的实现很复杂,是由动态随机访问存储器、闪存、磁盘存储器、特殊硬件和操作系统软件结合起来的实现的,目的就是为程序提供一个看上去统一的字节数组。
预告的后续内容:后面几章将会讲述编译器和运行时系统如何将存储器空间划分为更可管理的单元,用以存放不同的程序对象,也即是存放程序数据、指令和控制信息。可以用各种机制来分配和管理程序不同部分的存储,这种管理完全是在虚拟地址空间里完成的。比如C语言中的一个指针的值,无论它指向一个整数、一个结构还是其他程序对象,它都是某个存储块的第一个字节的虚拟地址。C编译器还把每个指针和类型信息联系起来,这样就可以根据指针值的类型,生成不同的机器级代码来访问存储在指针所指向位置处的值。每个程序对象都可以简单地视为一个字节块,而程序本身就是一个字节序列
十六进制表示法:
- 为何使用十六进制:因为二进制描述位模式显得太冗长,它显式一个字节的值域为000000002 ~ 111111112,十进制描述的值域是010 ~ 25510十进制表示法与位模式的互相转化很麻烦,所以16进制就闪亮登场,用以表示位模式,它表示值域就是0016 ~ FF16。
- 十六进制的写法:在C语言中,我们可以使用0x或者0X来表示一个数为16进制,比如F1AB2C16就可以写作0xF1AB2C,或者0xf1ab2c,甚至可以是大小写混合,反正又不区分大小写,怎么写随意。
- 十六进制的转化:编写机器级程序的一个常见任务就是在位模式的十进制、二进制和十六进制表示之间人工转换。
- 二进制与十六进制的转换:十六进制转二进制是先将每个数字转换成十进制,然后将每个十进制转化为二进制,然后拼在一起。同理二进制转化为十六进制就是将二进制数每4位分成一组,最左边的可以不足4位,然后转换对应的十进制,然后将这个十进制转化为十六进制,最后依旧拼在一起!
- 十六进制表示2的n次幂:2的非负整数n次幂的值x,也就是x=2n,很容易写成十六进制形式。转换的技巧就是x的二进制是1后面跟n个0。而十六进制数字0代表4个二进制0,所以,当n表示成i+4j的形式,其中0 ≤ i ≤ 3,然后我们把x写成十六进制数,就是开头的十六进制数字为1(i=0)、2(i=1)、4(i=2)、8(i=3),后面跟j个十六进制的0。比如2048=211,n=3+4*2,得到的十六进制数为0x800。
- 十六进制与十进制的转换:十进制x转化为十六进制,x除以16,得到的余数将其转化在低位的十六进制数,得到的商继续除以16,依次类推,最后得到的余数转化为十六进制后就是高位的十六进制数。而十六进制转化为十进制,就是用相应的16的幂乘以每个十六进制数字然后相加,跟二进制转化为十进制很像。
字数据大小:
- 字长的作用:每台计算机都有一个字长,它指明指针数据的标称大小。虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。一个字节为w位机器,虚拟地址的范围为0 ~ 2w-1,程序最多访问2w个字节。
- 32位到64位:最近这些年出现了大规模的从32位字长机器到64位字长机器的迁移,从应用于科学的高端机器到台式机到笔记本到手机的处理上。32位字长限制虚拟地址空间为4千兆字节(4GB),也就是刚刚超过4 X 109字节。而64位字长使得虚拟地址空间为16EB,大约是1.84 X 1019字节。而大多数64位机器也可以运行为32位机器编译的程序,这是一种向后兼容。
- 数据类型:计算机和编译器支持多种不同方式编码的数字格式,比如我们常提及的整数和浮点数,而机器处理的指令也有不同字节的,如2字节、4字节或8字节的整数,又或者4字节或8字节的浮点数。虽然C语言中int不管是被32位系统编译还是64位,通常也只有4个字节,但是long类型在32位程序就是4字节,在64位程序中则为8字节,为了避免这种情况,ISO C99引入了int32_t和int64_t,其数据大小是固定的,并不会随着编译器和机器设置而变化,它们分别是4个字节和8个字节。使用这种确定了大小的整数类型是程序员准确控制数据表示的最佳途径。char类型不像其他类型一样可以指定特定的无符号声明,虽然大多数编译器和机器将它们视为有符号数,但C标准不保证这一点,程序员也应该用有符号字符的声明来保证其为一个字节的有符号数据,不过很多情况下,程序对char是否有符号并不敏感。
- 声明指针:T *p;表示的是p是一个指针变量,指向一个类型为T的对象。比如char *p就是将p这个指针声明为指向一个char类型的对象。
- 可移植性:程序员应该使得它们的程序可以在不同的机器或编译器上可移植,其中可移植性的一个方面就是使程序对不同数据类型的确切大小不敏感,也可以说程序不用因为担心数据类型大小会在不同机器或编译器上有变化而考虑写兼容性代码。C语言标准对不同数据类型的数字范围设置了下界,但是却没有上界。因为1980到2010年左右,32位机器和32位程序是主流的组合,许多程序的编写都是按照32位程序的字节分配来的,但是随着后面64位机器的日益遍及,将这些程序移植到新机器上时,许多隐藏的对字长的依赖性也就会显现出来,而成为错误。
寻址和字节顺序:
- 多字节对象的地址:对于跨越多字节的程序对象,我们必须建立规则,这个对象的地址是什么,以及在内存中如何排列这些字节。在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。比如,假设一个类型为int的变量x的地址为0x100,那么当数据类型int为32位表示时,x的4个字节将被存储在内存的0x100、0x101、0x102、0x103位置。
- 字节排列的规则:排列表示一个对象的字节有两个通用的规则。考虑一个w位的整数,其位表示为[xw-1,xw-2,...,x1,x0],其中xw-1是最高有效位,而x0是最低有效位。假设w是8的倍数,这些位就能被分组成为字节,其中最高有效字节包含位[xw-1,xw-2,xw-3,...,xw-8],而最低有效字节包含位[x7,x6,x5,...,x0],其他字节包含中间的位。所以一个规则是某些机器选择在内存中按照从最低有效字节到最高有效字节的顺序存储对象,而另一个规则是某些机器会按照从最高有效字节到最低有效字节的顺序存储对象。前一种的按最低有效字节在最前面的方式的规则,称为小端法,后一种将最高有效字节放在最前面的规则,称为大端法。假设变量x的类型为int,位于地址0x100处,其十六进制值为0x01234567。其地址范围0x100~0x103的字节顺序依赖于机器的类型:
大端法: 0x100 0x102 0x103
01 23 45
小端法:0x100 0x102 0x103
67 45 23
- 大端小端的使用情况:大多数Intel兼容机都只用小端模式,而IBM和Oracle的大多数机器则是按大端模式操作的,但是IBM和Oracle制造的个人计算机使用的是Intel兼容的处理器,因此采用的却是小端法。一些比较新的微处理器是双端法,也就是可以把它们配置成作为大端或者小端的机器来运行。但是实际情况是一旦选择了特定操作系统,那么字节顺序也就固定下来了。比如手机的ARM微处理器,其硬件可以按小端或大端两种模式操作,但是这些芯片上常见的两种操作系统——安卓和ISO,却只能运行与小端模式。
- 大端小端的来历:出自《格利佛游记》中,其中交战的两个派别无法就应该从大端还是小端打开一个半熟的鸡蛋达成一致。就像这个鸡蛋的问题,选择何种字节顺序没有技术上的理由,因此争论就只能从社会政治方面进行争论。但是我们只要选择了一种规则并且始终如一地坚持,那么对于哪种字节排序的选择都是任意的。
- 字节顺序变得重要的第一种情况:对于大多数应用程序员来说,其机器所使用的字节顺序是完全不可见的。无论为哪种类型的机器所编译的程序都会得到同样的结果,但是有时候字节顺序会成为问题。如在不同类型的机器之间通过网络传送二进制数据时,如果是小端法机器产生的数据发送到了大端法机器时,接收程序会发现字里的字节成了反序的。而为了避免这种问题的出现,网络应用程序的代码编写必须遵守已建立的关于字节顺序的规则,以确保发送方机器将它的内部表示转换为网络标准,而接收方机器将其网络标准转换为它的内部表示。
- 字节顺序变得重要的第二种情况:阅读表示整数数据的字节序列时字节顺序也很重要,这通常发生在检查机器级程序时。如从某个文件中摘出了下面这行代码,该文件给出了一个针对Intel x86-64处理器的机器级代码的文本表示: 4004d3: 01 05 43 0b 20 00 add %eax,0x200b43(%rip) 这一行是由反汇编器生成的,反汇编器是一种确定可执行程序文件所表示的指令序列的工具,我们现在只需要注意这行表述的意思是十六进制字节串 01 05 43 0b 20 00是一条指令的字节级表示,这条指令是把一个字长的数据加到一个值上,该值的存储地址由0x200b43加上当前程序计数器的值得到的,当前程序计数器的值即为下一条将要执行指令的地址。如果取出这个序列的最后4个字节:43 0b 20 00,并且按照相反的顺序写出,我们得到00 20 0b 43。去掉开头的0,得到值0x200b43,这就是右边的数值。这种小端法机器生成的机器级程序表示,经常会将字节按照相反的顺序显式,跟通常书写数字时最高有效位在左边,最低有效位在右边是相反的。
- 字节顺序变得重要的第三种情况:当编写规避正常的类型系统的程序时,在C语言中,可以通过使用强制类型转换或联合来允许以一种数据类型引用一个对象,而这种数据类型与创建这个对象时定义的数据类型不同。大多数应用编程都强烈不推荐这种编码技巧,但是它们对系统级编程来说是非常有用的,甚至是必需的。
1 #include <stdio.h> 2 3 typedef unsigned char *byte_pointer; 4 5 void show_bytes(byte_pointer start, size_t len) { 6 size_t i; 7 for (i = 0; i < len; i++) 8 printf(" %.2x", start[i]); 9 printf("\n"); 10 } 11 12 void show_int(int x) { 13 show_bytes((byte_pointer) &x, sizeof(int)); 14 } 15 16 void show_float(float x) { 17 show_bytes((byte_pointer) &x, sizeof(float)); 18 } 19 20 void show_pointer(void *x) { 21 show_bytes((byte_pointer) &x, sizeof(void *)); 22 }
上面这段代码是使用强制类型转换来访问和打印不同程序对象的字节表示。我们用typedef将数据类型byte_pointer定义为一个指向类型为"unsigned char"的对象的指针。这样一个字节指针引用一个字节序列,其中每个字节都被认为是一个非负整数,程序中的第一个函数show_bytes的输入是一个字节序列的地址,它用一个字节指针以及一个字节数来指示。该字节数指定为数据类型size_t,它是表示数据结构大小的首选数据类型。show_bytes打印出每个以十六进制表示的字节。C格式化指令”%.2x”表明整数必须用至少两个数字的十六进制格式输出,如0b 32这种形式。而函数show_int、show_float、show_pointer展示了如何使用程序show_bytes来分别输出类型为int、float、void *的C程序对象的字节表示。可以观察看它们仅仅传递给show_bytes一个指向它们参数x的指针&x,且这个指针被强制类型转化为“unsigned char *”。这种强制类型转换告诉编译器,程序应该把这个指针看成指向一个字节序列,而不是指向一个原始数据类型的对象。然后这个指针会被看成是对象使用的最低字节地址。其中使用了sizeof来确定对象使用的字节数,一般来说,表达式sizeof(T)返回存储一个类型为T的对象所需要的字节数,使用sizeof而不是一个固定的值,是向编写在不同机器类型上可移植的代码迈进了一步。
1 void test_show_bytes(int val) { 2 int ival = val; 3 float fval = (float) ival; 4 int *pval = &ival; 5 show_int(ival); 6 show_float(fval); 7 show_pointer(pval); 8 }
在不同的机器上运行上面的测试代码,会得到下面的结果。这里使用的机器有:
LInux 32: 运行LInux的Intel IA32处理器
Windows:运行WIndows的Intel IA32处理器
Sun:运行Solaris的Sun Microsystems SPARC处理器(这些机器现在由Oracle生产)
Linux64:运行Linux的Intel x86-64处理器机器 值 类型 字节(十六进制) Linux32
Windows
Sun
Linux6412345
12345
12345
12345int
int
int
int39 30 00 00
39 30 00 00
00 00 30 39
39 30 00 00Linux32
Windows
Sun
Linux6412345
12345
12345
12345float
float
float
float00 e4 40 46
00 e4 40 46
46 40 e4 00
00 e4 40 46Linux32
Windows
Sun
Linux6412345
12345
12345
12345int *
int *
int *
int *e4 f9 ff bf
b4 cc 22 00
ef ff fa 0c
b8 11 e5 ff ff 7f 00 00
参数12345的十六进制表示为0x00003039.对于int类型和float类型的数据,除了字节顺序以外,我们在所有机器上都得到相同的结果。而在Linux32、Windows和Linux64上,最低有效字节值0x39最先输出,这说明它们是小端法机器;而在Sun上最后输出,这说明Sun是大端法机器。而另一方面,指针值却是完全都不相同的,不同的机器/操作系统配置使用不同的存储分配规则。值得注意的是除了Linux64使用8字节地址,其余都是使用4字节地址。还可以观察到的是尽管浮点型和整型数据都是对数值12345编码,但是它们有截然不同的字节模式。这是因为两种格式各用了不同的编码方法。但是将它们都转化为二进制后通过移位后会有13个相匹配的位的序列,这个会在后面浮点数讲到。
表示字符串:C语言中字符串被编码为一个以null(其值为0)字符结尾的字符数组。每个字符都有某个标准编码来表示,比如最常见的ASCII字符码。因此我们如果以参数"12345"和6来运行show_bytes方法,得到的结果是31 32 33 34 35 00。注意了,十进制数字x的ASCII码正还是0x3x,如7对应的ASCII码就是0x37,而终止字节的十六进制表示为0x00.使用ACSII码作为字符码在任何系统上都将得到相同的结果,与字节顺序和字大小无关,所以文本数据比二进制数据具有更强的平台独立性。
表示代码:
1 int sum(int x, int y) { 2 return x + y; 3 }
当我们在示例机器上编译上面代码时,生成如下字节表示的机器代码:
Linux 32 55 89 e5 8b 45 0c 03 45 08 c9 c3
Windows 55 89 e5 8b 45 0c 03 45 08 5d c3
Sun 81 c3 e0 08 90 02 00 09
Linux 64 55 48 89 e5 89 7d fc 89 75 f8 03 45 fc c9 c3
我们发现指令编码是不同的。不同的机器类型使用不同的且不兼容的指令和编码方式。即使是完全一样的进程,运行在不同操作系统上也会有不同的编码规则,因此二进制代码是不兼容的。所以二进制代码很少能在不同机器和操作系统组合之间移植。从机器的角度看,程序仅仅是字节序列,机器也没有关于原始源程序的任何信息。
布尔代数简介:
- 布尔起源:二进制值是计算机编码、存储和操作信息的核心,所以围绕数值0和1的研究已经演化出了丰富的数学知识体系。1850年前后乔治·布尔设计了一种通过将逻辑值TRUE和逻辑值FALSE编码为二进制值1和0的代数,也就是布尔代数,用以研究逻辑推理的基本原则。
- 布尔运算:布尔运算符 ~ 对应逻辑运算NOT,在命题逻辑中用符号 ¬ 表示。P为假,¬P就为真;P=0,则~P=1,反之亦然。布尔运算符 & 对应于逻辑运算AND,在命题逻辑中用符号 Λ 表示,当P和Q都为真时,PΛQ为真;p=1且q=1,则p&q才等于1。布尔运算符 | 对应于逻辑运算OR,在命题逻辑中用符号 V 表示,当P或Q为真时,P V Q成立;当p=1或者q=1时,p | q等于1。布尔运算符 ^ 对应与逻辑运算异或EXCLUSIVE-OR,在命题逻辑中用符号 ⊕ 表示,当P或者Q为真且不同时为真时,P⊕Q成立;当p=1且q=0,或者p=0且q=1时,p ^ q等于1。
- 位向量的布尔运算:可以将上述的4个布尔运算扩展到位向量的运算中,所谓位向量指的是固定长度为w、由0和1组成的串。如位向量a=[0100],而对于位向量的布尔运算可以变成对位向量中每个元素之间的运算。
- 布尔分配律:布尔代数的运算也有跟整数算术运算的相似之处,比如布尔运算有着 & 对 | 的分配律,a & ( b | c ) = ( a & b ) | ( a & c),|对&也有分配律,a | ( b & c ) = ( a | b ) & ( a | c )。
- 布尔环:布尔环与整数运算也有很多相同的属性,比如整数运算中的每个值 x 都有一个加法逆元 -x,使得x + (-x) = 0。而布尔环类似的属性就是 ˆ 异或,不过这个"加法运算" ˆ 中的每个元素的“加法逆元”是它自己本身,也就是 a ˆ a = 0。而通过这个属性的扩展,我们可以得出 (a ˆ b) ˆ a = b。这个属性有时被用于a与b之间不通过中间数而交换值,有时也被用于从一个已知有限的集合中找出重复的一个数。
- 位向量的应用:位向量有一个很有用的应用,那就是表示有限集合,可以用位向量[aw-1,...,a1,a0]编码任何子集A⊆ {0,1,...,w - 1},其中ai=1当且仅当i∈A,也就是只有位为1的时候,才算进集合,位为0时不算入集合,只是充当一个位数。比如a=[01101001]表示集合A={0,3,5,6},b=[01010101]表示集合B={0,2,4,6}。使用这种编码集合的方法,布尔运算 | 和 &分别对应于集合的并和交,而 ~ 对应于集合的补。运算 a & b得到位向量[01000001],就是A ∩ B也就是合并集合,得出{0,6}。
- 实际运用:在大量的实际运用中,我们都能看到用位向量来对集合编码。例如在第8章中,我们会看到有很多不同的信号会中断程序执行。我们能够通过指定一个位向量掩码,有选择地使能或是屏蔽一些信号,其中某一位置上为1时,表明信号 i 是有效的(使能),而0表明该信号是被屏蔽的。因而,这个掩码表示就是设置为有效信号的集合。就是使用&使某些信号的集合A跟有效信号的集合B进行合并,结果就是这个A集合中的有效信号留下来,无效信号被屏蔽掉。
C语言中的位级运算:C语言一个很有用的特性就是它支持按位布尔运算。事实上,我们能把布尔运算运用到整型的数据类型上。比如确定一个位级表达式的结果,就是将十六进制的参数扩展成二进制表示并执行二进制运算,然后将运算后的结果再转换为十六进制。位级运算的一个常见用法就是实现掩码运算,这里掩码是一个位模式,表示从一个字中选出的位的集合。如掩码0xFF(最低的8位为1)表示一个字的低位字节。位级运算 x & 0xFF 生成一个由x的最低有效字节组成的值,而其他的字节就被置为0。比如 x = 0xA2C8EDEF,其运算后得到 0x000000EF。表达式 ~0将生成一个全1的掩码,不管机器的字的大小是多少。但是对于一个32位的机器来说,将同样的掩码可以写成0xFFFFFFFF,但这样的代码不是可移植的。
C语言中的逻辑运算:C语言还提供了一组逻辑运算符 || 、 && 和 !,分别对应于命题逻辑中的OR、AND和NOT运算。逻辑运算很容易和位级运算相混淆,但是它们的功能是完全不同的。逻辑运算认为所有非零的参数都表示TRUE,而参数0表示FALSE。它们返回1或者0,分别表示结果为TRUE或者FALSE。而位级运算返回的值就不只是0或1了,这就是其中的差别,位级运算只有在特殊的情况下,也就是参数被限制为0或者1时,才和与其对应的逻辑运算有相同的行为。逻辑运算符 && 和 || 与它们对应的位级运算 & 和 | 之间第二个重要的区别是,如果对第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。也因此利用这个属性能避免一些问题,如表达式a && 5/a 将不会造成被零除,又如表达式p && *p++也不会导致间接引用空指针。
C语言中的移位运算:
- 左移运算:C语言提供了一组移位运算,向左或向右移动位模式。比如一个位表示为[xw-1,xw-2,...,x0]的操作数x,C表达式 x << k会生成一个值,其位表示为[xw-k-1,xw-k-2,...,x0,0,...,0]。也就是说,x向左移动k位,丢弃最高的k位,并在右端补k个0。移位量应该是一个0~w-1之间的值。移位运算是从左到右可结合的,所以x<< j << k 等价于( x << j ) << k。
- 右移运算:还有一个相应的右移运算 x >> k,但是跟左移不太一样的就是机器支持两种形式的右移,逻辑右移和算术右移。逻辑右移在左端补k个0,得到的结果是[0,...,0,xw-1,xw-2,...,xk]。算术右移是在左端补k个最高有效位的值,得到的结果是[xw-1,...,xw-1,xw-1,xw-2,...,xk],其对有符号整数的运算非常有用。下面有例子展示三种移位的结果:
操作 值 参数x [01100011] [10010101] x << 4 [00110000] [01010000] x >> 4(逻辑右移) [00000110] [00001001] x >> 4 (算术右移) [00000110] [11111001]
黑体的数字表示的是最右端(左移)或最左端(右移)填充的值。可以看到除了一个条目之外,其他的都包含填充0。唯一的例外是算术右移[10010101]的情况。因为操作数的最高位是1,填充的值就是1。 - 机器中的右移运算:C语言标准并没有明确定义对于有符号数应该使用哪种类型的右移——算术右移或逻辑右移都可以。但是这就意味着任何假设一种或者另一种右移形式的代码都可能会遇到可移植性问题。实际上却是几乎所有的编译器/机器组合都对有符号数使用算术右移,且许多程序员也都假设机器会使用这种右移。另一方面,对于无符号数,右移必须是逻辑的。与C相比,Java对右移有明确的定义。>>表示算术右移,>>>表示逻辑右移
- 超出位长的移位运算:对于移动超过值的数据类型的位长的位数,是根据超过多少位来进行移位运算的。比如对有32位组成的数据类型int的值进行移动32位、36位和40位的操作,最后真正的移位运算依次是移动了0位、4位和8位。不过这种行为对于C程序来说是没有任何保证的,所以我们应该保持位移量小于待移位值的位数。另一方面,Java特别要求位移数量应该按照我们前面所讲的求模的方法来计算。
- 移位操作符优先级:在C语言中,加减法的优先级比移位运算要高。所以不要把本意是(1<<2) + (3<<4)写成 1 << 2 + 3 << 4。还有如果拿不准优先级,就加上括号吧。