二.1.4 表示字符串
C语言中字符串被编码为一个以null(其值为0)字符结尾的字符数组。每个字符都由某个标准编码来表示,最常见的是ASCII字符码。因此,如果我们以参数“12345”和6(包括终止符)来运行例程show_bytes,我们得到结果31 32 33 34 35 00。请注意,十进制数字x的ASCII码0x3x,而终止字节的十六进制表示为0x00。在使用ASCII码作为字符码的任何系统上都将得到相同的结果,与字节顺序和字大小规则无关。因而,文本数据比二进制数据具有更强的平台独立性。
练习题2.7 下面对show_bytes的调用将输出什么结果?
const char*s = "abcdef";
show_bytes((byte_pointer)s,strlen(s));
注意字母‘a’~‘z’的ASCII码为0x61~0x7A。
旁注: 文字编码的Unicode标准
ASCII字符集适合于编码英语文档,但是在表达一些特殊字符方面并没有太多办法,例如法语的“C”。它完全不适合编码希腊语、俄语和中文等语言的文档。这些年,提出了很多方法来对不同语言的文字进行编码。Unicode联合会(Unicode Consortium)修订了最全面且广泛接受的文字编码标准。当前的Unicode标准(7.0版)的字库包括将近100 000个字符,支持广泛的语言种类,包括古埃及和巴比伦的语言。为了保持信用,Unicode技术委员会否决了为klingon(即电视连续剧《星际迷航》中的虚构文明)编写语言标准的提议。
基本编码,称为Unicode“统一字符集”,使用32位来表示字符。这好像要求文本串中每个字符要占用4个字节。不过,可以有一些替代编码,常见的字符只需要1个或2个字节,而不太常用的字符需要多一些的字节数。特别的,UTF-8表示将每个字符编码为一个字节序列,这样标准ASCII字符还是使用和他们在ASCII中一样的单字节编码,这也就意味着所有的ASCII字节序列用ASCII码表示和用UTF-8表示是一样的。
Java编程语言使用Unicode来表示字符串。对于C语言也有支持Unicode的程序库。
二.1.5 表示代码
考虑下面的C函数:
int sum(int x,int y){
return x + y;
}
当我们在示例机器上编译时,生成如下字节表示的机器代码:
- 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
我们发现指令编码是不同的。不同的机器类型使用不同的且不兼容的指令和编码方式。即使是完全一样的进程,运行在不同的操作系统上也会有不同的编码规则,因此二进制代码是不兼容的。二进制代码很少能在不同机器和操作系统组合之间移植。
计算机系统的一个基本概念就是,从机器的角度来看,程序仅仅只是字节序列。机器没有关于原始源程序的任何信息,除了可能有些用来帮助调试的辅助表以外。在第三章学习机器级编程时,我们将更清楚地看到这一点。
二.1.6 布尔代数简介
二进制是计算机编码、存储和操作信息的核心,所以围绕数值0和1的研究已经演化出了丰富的数学知识体系。这起源于1850年前后乔治·布尔(George Boole,1815-1864)的工作,因此也称为布尔代数(Boolean algebra)。布尔注意到通过将逻辑值TRUE(真)和FALSE(假)编码为二进制1和0,能够设计出一种代数,以研究逻辑推理的基本原则。
最简单的布尔代数是二元集合{0,1}基础上的定义。图2-7定义了这种布尔代数中的几种运算。我们用来表示这些运算的符号与C语言位级运算使用的符号是相匹配的,这些将在后面讨论。
布尔运算~对应于逻辑运算NOT,在命题逻辑中用符号¬表示。也就是说,当P不是真的时候,我们就说¬P是真的,反之亦然。相应地,当P等于0时,~P等于1,反之亦然。布尔运算&对应于逻辑运算AND,在命题逻辑中用符号∧表示。当P和Q都为真时,我们说P∧Q为真。相应地,只有当p=1且q=1时,p&q才等于1.布尔运算|对应于逻辑运算OR,在命题逻辑中用符号∨表示。当P或者Q为真时,我们说P∨Q为真。相应地,只有当p=1或者q=1时,p|q等于1.布尔运算^对应于逻辑运算异或,在命题逻辑中用符号⊕表示。当P或者Q有且只有一个为真时,我们说P⊕Q成立。相应地,当p=1且q=0,或者p=0且q=1时,p^q等于1。
后来创立信息论领域的Claude Shannon(1916-2001)首先建立了布尔代数和数字逻辑之间的联系。他在1937年的硕士论文中表明了布尔代数可以用来设计和分析机电继电器网络。尽管那时计算机已经取得了相当的发展,但是布尔代数仍然在数字系统的设计和分析中扮演者重要的角色。
我们可以将上述4个布尔运算扩展到位向量的运算,位向量就是固定长度为w、由0和1组成的串。位向量的运算可以定义成参数的每个对应元素之间的运算。假设a和b分别表示位向量
[a(w-1),a(w-2),…,a0]和[b(w-1),b(w-2),…,b0]。我们将a&b也定义为长度为w的位向量,其中第i个元素等于ai&bi,0≤i<w。可以用类似的方式将运算|、^和~扩展到位向量上。
举个例子,假设w=4,参数a=[0110],b=[1100]。那么4种运算a&b、a|b、a^b和~b分别得到以下结果:
0110 0110 0110
& 1100 | 1100 ^ 1100 ~ 1100
0100 1110 1010 0011
练习题2.8 填写下表,给出位向量的布尔运算的求值结果。
运算 | 结果 |
a | [01101001] |
b | [01010101] |
~a | |
~b | |
a&b | |
a|b | |
a^b |
网络旁注DATA:BOOL 关于布尔代数和布尔环的更多内容
对于任意整数w>0,长度为w的位向量上的布尔运算 | 、& 和 ~ 形成了一个布尔代数。最简单的情况是w = 1时,只有两个元素;但是对于更普遍的情况,2^w个长度为w的位向量。布尔代数和正数算术运算有很多相似之处。例如,乘法对加法的分配率,写为:
a ·(b+c)=(a · b)+(a · c),而布尔运算&对|的分配率,写为a&(b|c)=(a&b)|(a&c)。此外,布尔运算|对&也有分配率,写为a|(b&c)=(a|b)&(b|c),但是对于整数我们不能说a+(b·c)=(a+b)·(a+c)。
当考虑长度为w的位向量上的^、&和~运算时,会得到一种不同的数学形式,我们称为布尔环(Boolean ring)。布尔环与整数运算有很多相同的属性。例如,整数运算的一个属性是每个值x都有一个加法逆元(additive inverse)-x,使得x+(-x)=0。布尔环也有类似的属性,这里的“加法”运算时^,不过这时每个元素的加法逆元是它自己本身。也就是说,对于任何值a来说,a^a=0,这里我们用0来表示全0的位向量。可以看到对单个位来说这时成立的,即0^0=1^1=0,将这个扩展到位向量也是成立的。当我们重新排列组合顺序,这个属性也依然成立,因此有(a^b)^a=b。这个属性会引起一些很有趣的结果和聪明的技巧,在练习题2.10中我们会有所探讨。
位向量一个很有用的应用就是表示有限集合。我们可以用位向量[a(w-1),…,a1,a0]编码任何子集A⊆{0,1,…,w-1},其中ai=1当且仅当i∈A。 例如(记住我们是把a(w-1)写在左边,而将a0卸载右边),位向量a≈[01101001]表示集合A={0,3,5,6},而b≈[01010101]表示集合B={0,2,4,6}。使用这种编码集合的方法,布尔运算|和&分别对应集合的并和交,而~对应于集合的补。还是用前面的那个例子,运算a&b得到向量[01000001],而A∩B={0,6}。
在大量实际应用中,我们都看到用位向量来对集合编码。例如,在第八章,我们会看到有很多不同的信号会中断程序执行。我们能够通过指定一个位向量掩码,有选择地使能或是屏蔽一些信号,其中某一位位置上为1时,表明信号i是有效的(使能),而0表明该信号是被屏蔽的。因而,这个掩码表示的就是设置为有效信号的集合。
练习题2.9 通过混合三种不同颜色的光(红色、绿色和黄色),计算机可以在视频屏幕或者液晶显示器上产生彩色的画面。设想一种简单的方法,使用三种不同颜色的光,每种光都能打开或关闭,投射到玻璃屏幕上,如图所示:
这些颜色中的每一种都能使用一个长度为3的位向量来表示,我们可以对它们进行布尔运算。
A. 一种颜色的补是通过关掉打开的光源,且打开关闭的光源而形成的。那么上面列出的8种颜色每一种的补是什么?
B. 描述下列颜色应用布尔运算的结果:
蓝色 | 绿色 =
黄色 & 蓝绿色 =
红色 ^ 红紫色 =
二.1.7 C语言中的位级运算
C语言的一个很有用的特性就是它支持按位布尔运算。事实上,我们在布尔运算中使用的那些符号就是C语言所使用的:|就是OR(或),&就是AND(与),~就是NOT(取反),而^就是EXCLUSIVE-OR(异或)。这些运算能运用到任何“整型”的数据类型上,包括图2-3所示内容。以下是一些对char数据类型表达式求值的例子:
C的表达式 | 二进制表达式 | 二进制结果 | 十六进制结果 |
~0x41 | ~[0100 0001] | [1011 1110] | 0xBE |
~0x00 | ~[0000 0000] | [1111 1111] | 0xFF |
0x69&0x55 | [0110 1001]&[0101 0101] | [0100 0001] | 0x41 |
0x69|0x55 | [0110 1001] | [0101 0101] | [0111 1101] | 0x7D |
正如示例说明的那样,确定一个位级表达式的结果最好的方法,就是将十六进制的参数扩展成二进制表示并执行二进制运算,然后再转换回十六进制。
练习题2.10 对于任一位向量a,有a^a = 0。应用这一属性,考虑下面的程序:
void inplace_swap(int *x,int *y){
*y = *x ^ *y; //step 1
*x = *x ^ *y; //step 2
*y = *x ^ *y; //step 3
}
正如程序名字所暗示的那样,我们认为这个过程的效果是交换指针变量x和y所指向的存储位置处存放的值。注意,与通常的交换两个数值的技术不一样,当移动一个值时,我们不需要第三个位置来临时存储另一个值。这种交换方式并没有性能上的优势,它仅仅是一个智力游戏。
以指针x和y指向的位置存储的值分别是a和b作为开始,填写下表,给出在程序的每一步之后,存储在这两个位置中的值。利用^的属性证明达到了所希望的效果。回想一下,每个元素就是它自身的加法逆元(a^a=0)。
步骤 | *x | *y |
初始 | a | b |
第1步 | ||
第2步 | ||
第3步 |
练习题2.11 在练习题2.10中的inplace_swap函数的基础上,你决定写一段代码,实现将一个数组中的元素头尾两端依次对调。你写出下面这个函数:
void reverse_array(int a[],int cnt){
int first, last;
for(first = 0,last = cnt-1;first <= last;first++,last--)
inplace_swap(&a[first],&a[last]);
}
}
当你对一个包含元素1、2、3和4的数组使用这个函数时,正如预期的那样,现在数组的元素变为了4、3、2和1。不过,当你对一个包含元素1、2、3、4和5的数组使用这个函数时,你会很惊奇地看到得到数字的元素为5、4、0、2和1。实际上,你会发现这段代码对所有偶数长度的数组都能正确地工作,但是当数组的长度为奇数时,它就会把中间的元素设置成0。
A. 对于一个长度为奇数的数组,长度 cnt = 2k + 1,函数reverse_array最后一次循环中,变量first和last的值分别是什么?
B. 为什么这时调用函数 inplace_swap 会将数组元素设置为0?
C. 对 reverse_array 的代码做哪些简单改动就能消除这个问题?
位级运算的一个常见用法就是实现掩码运算,这里掩码是一个位模式,表示从一个字中选出的位的集合。让我们来看一个例子,掩码0XFF(最低的8位为1)表示一个字的低位字节。位级运算x&0xFF生成一个由x的最低有效字节组成的值,而其他的字节就被置为0.比如,对于 x = 0x89ABCDEF,其表达式将得到0x000000EF,表达式~0将生成一个全1的掩码,不管机器的字大小是多少。尽管对于一个32位机器来说,同样的掩码可以写成0xFFFFFFFF,但是这样的代码不是可移植的。
练习题2.12 对于下面的值,写出变量x的C语言表达式。你的代码应该对任何字长w≥8都能工作。我们给出了当x=0x87654321以及w=32时表达式求值的结果,仅供参考。
A. x的最低有效字节,其他位均置为0。[0x00000021]。
B. 除了x的最低有效字节外,其他的位都取补,最低有效字节保持不变。[0x789ABC21]。
C. x的最低有效字节设置成全1,其他字节都保持不变。[0x876543FF]。
练习题2.13 从20世纪70年代末到80年代末,Digital Equipment的VAX计算机是一种非常流行的机型。它没有布尔运算AND和OR指令,只有bis(位设置)和bic(位清除)这两种指令。两种指令的输入都是一个数据字x和一个掩码字m。它们生成一个结果z,z是由根据掩码m的位来修改x的位得到的。使用bis指令,这种修改就是在m为1的每个位置上,将z对应的位设置为1。使用bic指令,这种修改就是在m为1的每个位置,将z对应的位设置为0。
为了看清楚这些运算与C语言位级运算的关系,假设我们有两个函数bis和bic来实现位设置和位清除操作。只想用这两个函数,而不使用任何其他C语言运算,来实现按位|和^运算。填写下列代码中缺失的代码。提示:写出bis和bic运算的C语言表达式。
/*Declarations of functions implementing operations bis and bic*/
int bis(int x,int m);
int bic(int x,int m);
/*Compute x|y using only calls to functions bis and bic*/
int bool_or(int x,int y){
int result =______________;
return result;
}
/*Compute x^y using only calls to functions bis and bic*/
int bool_xor(int x,int y){
int result = _______________;
return result;
}
二.1.8 C语言中的逻辑运算
C语言还提供了一组逻辑运算符||、&&和!,分别对应于命题逻辑中的OR、AND和NOT运算。逻辑运算很容易和位级运算相混淆,但是它们的功能是完全不同的。逻辑运算认为所有的非零的参数都表示TRUE,而参数0表示FALSE。它们返回1或者0,分别表示结果为TRUE或者为FALSE。以下是一些表达式求值的示例。
表达式 | 结果 |
!0x41 | 0x00 |
!0x00 | 0x01 |
!!0x41 | 0x01 |
0x69&&0x55 | 0x01 |
0x69||0x55 | 0x01 |
可以观察到,按位运算只有在特殊情况下,也就是参数被限制成0或者1时,才和与其对应的逻辑运算有相同的行为。
逻辑运算符&&和||与它们对应的位级运算&和|之间第二个重要的区别是,如果对第一个参数求值就能确定表达式的结果,那么逻辑运算符就不会对第二个参数求值。因此,例如,表达式a&&5/a将不会造成被零除,而表达式p&&*p++也不会导致间接引用空指针。
练习题2.14 假设x和y的字节值分别为0x66和0x39。填写下表,指明各个C表达式的字节值。
表达式 | 值 | 表达式 | 值 |
x&y | x&&y | ||
x|y | x||y | ||
~x|~y | !x||!y | ||
x&!y | x&&~y |
练习题2.15 只使用位级和逻辑运算,编写一个C表达式,它等价于 x==y。换句话说,当x和y相等时它将返回1,否则就返回0。
二.1.9 C语言中的移位运算
C语言还提供了一组移位运算,向左或者向右移动位模式。对于一个位表示为[x(w-1),x(w-2),…,x0]的操作数,C表达式x<<k会生成一个值,其位表示为[x(w-k-1),x(w-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,x(w-1),x(w-2),…,xk]。算术右移是在左端补k个最高有效位的值,得到的结果是[x(w-1),…,x(w-1),x(w-1),x(w-2),…,xk]。这种做法看上去可能有点奇特,但是我们会发现它对有符号整数数据的运算非常有用。
让我们来看一个例子,下面的表给出了一个8位参数x的两个不同的值做不同的移位操作得到的结果:
操作 | 值 |
参数x | [01100011] [10010101] |
x<<4 | [00110000] [01010000] |
x>>4(逻辑右移) | [00000110] [00001001] |
x>>4(算术右移) | [00000110] [11111001] |
斜体的数字表示的是最右端(左移)或最左端(右移)填充的值。可以看到除了一个条目之外,其他的都包含填充0。唯一的例外是算术右移[10010101]的情况。因为操作数的最高位是1,填充的值就是1。
C语言标准并没有明确定义对于有符号数应该使用哪种类型的右移——算术右移或者逻辑右移都可以。不幸地,这就意味着任何假设一种或者另一种右移形式的代码都可能会遇到可移植性问题。然而,实际上,几乎所有的编译器/机器组合都对有符号数使用算术右移,且许多程序员也都假设机器会使用这种右移。另一方面,对于无符号数,右移必须是逻辑的。
与C相比,Java对于如何使用右移有明确的定义。表达式x>>k会将x算术右移k个位置,而x>>>k会对x做逻辑右移。
旁注:移动k位,这里k很大
对于一个由w位组成的数据类型,如果要移动k≥w位会得到什么结果呢?例如,计算下面的表达式会得到什么结果,假设数据类型int 为 w=32;
int lval = 0xFEDCBA98 << 32;
int aval= 0xFEDCBA98 >> 36;
unsigned uval = 0xFEDCBA98u >>40;
C语言标准很小心地规避了说明在这种情况下该如何做。在许多机器上,当移动一个w位的值时,移位指令只考虑位移量的低log₂w位,因此实际上位移量就是通过计算k mod w得到的。例如,当w=32时,上面三个移位运算分别是移动0、4和8位,得到结果:
lval 0xFEDCBA98
aval 0xFFEDCBA9
uval 0x00FEDCBA
不过这种行为对于C程序来说是没有保证的,所以应该保持位移量小于待移位值的位数。另一方面,Java特别要求位移数量应该按照我们前面所讲的求模的方法来计算。
旁注:与移位运算有关的操作符优先级问题
常常有人会写这样的表达式1<<2+3<<4,本意是(1<<2)+(3<<4)。但是在C语言中,前面的表达式等价于1<<(2+3)<<4,这是由于加法(和减法)的优先级比移位运算要高。然后,按照从左至右结合性规则,括号应该是这样打的(1<<(2+3))<<4,得到的结果是512,而不是期望的52。
在C表达式中搞错优先级是一种常见的程序错误原因,而且常常很难检查出来。所以当你拿不准的时候,请加上括号!
练习题2.16 填写下表,展示不同移位运算对单字节数的影响。思考移位运算的最好方式是使用二进制表示。将最初的值转换为二进制,执行移位运算,然后再转换回十六进制。每个答案都应该是8个二进制数字或者2个十六进制数字。
x | x<<3 | x>>2(逻辑的) | x>>2(算术的) | ||||
十六进制 | 二进制 | 二进制 | 十六进制 | 二进制 | 十六进制 | 二进制 | 十六进制 |
0xC3 0x75 0x87 0x66 |