二进制数字系统和标准字符码
无符号(unsigned)编码基于传统的二进制表示法,表示大于或者等于零的数字。
补码(two’s-complement)编码是表示有符号整数的最常见的方式,有符号整数就是可以为正或者为负的数字。
浮点数(floating-point)编码是表示实数的科学记数法的以二为基数的版本。
地址
机器级程序将存储器视为一个非常大的字节数组,称为虚拟存储器(virtual memory)。存储器的每个字节都由一个唯一的数字来标识,称为它的地址(address)。所有可能地址的集合称为虚拟地址空间(virtual address space)。
补充:指针的值表示某个对象的位置,指针的类型表示那个位置上所存储对象的类型(比如整数或者浮点数)。
十六进制表示法,十进制、二进制和十六进制表示之间进行转换
字
每台计算机都有一个字长(word size),指明整数和指针数据的标称大小(nominal size)。因为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。也就是说,对于一个字长为w位的机器而言,虚拟地址的范围为0~2w-1,程序最多访问2w个字节。现在计算机多为32和64位。
数据大小
计算机和编译器支持多种不同方式编码的数字格式,如整数和浮点数,以及其他长度的数字
C声明 32位机器 64位机器
Char 1 1
short int 2 2
int 4 4
long int 4 8
long long int 8 8
char * 4 8
float 4 4
double 8 8
C语言中数字数据类型的字节数
寻址和字节顺序
按照从最高有效字节到最低有效字节的顺序存储。前一种规则—最低有效字节在最前面的方式,称为小端法。大端法与之相反。
强制类型转换(cast)来允许以一种数据类型引用一个对象,而这种数据类型与创建这个对象时定义的数据类型不同。大多数应用编程都强烈不推荐这种编码技巧,但是它们对系统级编程来说是非常有用,甚至是必需的
使用typedef命名数据类型
C语言中的typedef声明提供了一种给数据类型命名的方式。
使用printf格式化输出
printf函数(还有它的同类fprintf和sprintf)提供了一种打印信息的方式,这种方式对格式化细节有相当大的控制能力。第一个参数是格式串(format string),而其余的参数都是要打印的值。在格式串里,每个以'%'开始的字符序列都表示如何格式化下一个参数。典型的示例有:'%d'是输出一个十进制整数,'%f'是输出一个浮点数,而'%c'是输出一个字符,其编码由参数给出。
指针的创建和间接引用
C的“取地址”运算符&创建一个指针。在这三行中,表达式&x创建了一个指向保存变量x的位置的指针。这个指针的类型取决于x的类型,因此这三个指针的类型分别为int*、float*和void**。(数据类型void*是一种特殊类型的指针,没有相关联的类型信息。)
表示字符串
C语言中字符串被编码为一个以null(其值为0)字符结尾的字符数组。每个字符都由某个标准编码来表示,最常见的是ASCII字符码。
ASCII字符集适合于编码英语文档,但是在表达一些特殊字符方面却没有太多办法,它完全不适合编码希腊语、俄语和中文这样语言的文档。近几年 ,开发出很多方法来对不同语言的文字编码。Java编程语言使用Unicode来表示字符串。对于C语言也有支持Unicode的程序库。
表示代码
不同的机器类型使用不同的且不兼容的指令和编码方式。即使是完全一样的进程运行在不同的操作系统上也会有不同的编码规则,因此二进制代码是不兼容的。二进制代码很少能在不同机器和操作系统组合之间移植。
布尔代数
与或非,取反运算。位运算。
C语言中的位级运算
确定一个位级表达式的结果最好的方法,就是将十六进制的参数扩展成二进制表示并执行二进制运算,然后再转换回十六进制
掩码:某一位置为一,表示信号i有效,0则为信号i是被屏蔽的。
位级运算的一个常见用法就是实现掩码运算,这里掩码是一个位模式,表示从一个字中选出的位的集合。让我们来看一个例子,掩码0xFF(最低的8位为1)表示一个字的低位字节。位级运算x&0xFF生成一个由x的最低有效字节组成的值,而其他的字节就被置为0。比如,对于x=0x89ABCDEF,其表达式将得到0x000000EF。表达式~0将生成一个全1的掩码,不管机器的字大小是多少。尽管对于一个32位机器来说,同样的掩码可以写成0xFFFFFFFF,但是这样的代码不是可移植的。
C语言中的逻辑运算
C语言还提供了一组逻辑运算符 ||、&&和!,分别对应于命题逻辑中的OR、AND和NOT运算。
C语言中的移位运算
C语言还提供了一组移位运算,以便向左或者向右移动位模式。对于无符号数据(也就是以限定词unsigned声明的整型对象),右移必须是逻辑的。而对于有符号数据(默认的声明的整型对象),算术的或者逻辑的右移都可以。
与移位运算有关的操作符优先级问题
常常有人会写这样的表达式1<<2+3<<4,其本意是(1<<2)+(3<<4)。但是在C语言中,前面的表达式等价于1<<(2+3)<<4,这是由于加法(和减法)的优先级比移位运算要高。然后,按照从左至右结合性规则,括号应该是这样打的(1<<(2+3))<<4,因此得到的结果是512,而不是期望的52。
整型数据类型
C、C++和Java中的有符号和无符号数C和C++都支持有符号(默认)和无符号数。Java只支持有符号数。
无符号数的编码
补码编码
利用补码可以把数学运算统一成加法,只要一个加法器就可以实现所有的数学运算。
有符号数还有两种标准的表示方法:
反码(Ones’ Complement):除了最高有效位的权是-(2w-1-1)而不是-2w-1,它和补码是一样的。
原码(Sign-Magnitude):
最高有效位是符号位,用来确定剩下的位应该取负权还是正权。
有符号数和无符号数之间的转换
将函数U2Tw定义为U2Tw(x) = 4B2Tw(U2Bw(x))。这个函数的输入是一个0~2w - 1之间的数,结果得到一个-2w-1~2w-1-1之间的值,这里两个数有相同的位模式,除了参数是无符号的,而结果是以补码表示的。类似地,对于-2w-1~2w-1-1之间的值x,函数T2Uw定义为 T2Uw(x) = 4 B2Uw(T2Bw(x)),生成一个数的无符号表示和x的补码表示相同
函数U2T描述了从无符号数到补码的转换,而T2U描述的是补码到无符号的转换。这两个函数描述了在大多数C语言实现中这两种数据类型之间的强制类型转换效果。
C语言中的有符号数和无符号数
由于C语言对同时包含有符号和无符号数表达式的这种处理方式,出现了一些奇特的行为。当执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么C语言会隐式地将有符号参数强制类型转换为无符号数,并假设这两个数都是非负的,来执行这个运算。就像我们将要看到的,这种方法对于标准的算术运算来说并无多大差异。
扩展一个数字的位表示
将一个无符号数转换为一个更大的数据类型,我们只需要简单地在表示的开头添加0,这种运算称为零扩展(zero extension)。将一个补码数字转换为一个更大的数据类型可以执行符号扩展(sign extension),规则是在表示中添加最高有效位的值的副本。由此可知,如果我们原始值的位表示为[xw-1,xw-2,…,x0],那么扩展后的表示就为[xw-1,…,xw-1,xw-1,xw-2,…,x0]。(我们用浅灰色标出符号位xw-1来突出它们在符号扩展中的角色。)
这表明当把short转换成unsigned时,我们先要改变大小,之后再完成从有符号到无符号的转换。也就是说(unsigned)sx等价于(unsigned)(int)sx,求值得到4 294 954 951,而不等价于(unsigned)(unsigned short)sx,后者求值得到53 191。事实上,这个规则是C语言标准要求的。
截断数字
于一个补码数字x,相似的推理表明B2Tw ([xw-1, xw-2,…, x0]) mod 2k = B2Uk[xk-1, xk-2,…, x0]。也就是,x mod 2k能够被一个位级表示为[xk-1,…, x0]的无符号数表示。不过,一般而言,我们将被截断的数字视为有符号的。这将得到数值U2Tk(x mod 2k)。 总而言之,无符号数的截断结果是:
B2Uk([xk−1,xk−2,...,x0])=B2Uw([xw−1,xw−2,...,x0])mod2k,, (2-9) 而补码数字的截断结果是:
B2Tk([xk−1,xk−2,...,x0])=U2Tk(B2Uw([xw−1,xw−2,...,x0])mod2k)(2 (2-10)
关于有符号数与无符号数的建议
就像我们看到的那样,有符号数到无符号数的隐式强制类型转换导致了某些非直观的行为。而这些非直观的特性经常导致程序错误,并且这种包含隐式强制类型转换细微差别的错误很难被发现。因为这种强制类型转换是在代码中没有明确指示的情况下发生的,程序员经常忽视了它的影响。
无符号加法
无符号加法等价于计算和模上2w。可以通过简单的丢弃x + y的w + 1位表示的最高位,来计算这个数值。比如,考虑一个4位数字表示,x = 9和y = 12的位表示分别为[1001]和[1100]。它们的和是21,5位的表示为[10101]。但是如果丢弃最
高位,我们就得到[0101],也就是十进制值的5。这就和值21 mod 16 = 5一致。
补码加法
定义z为整数和z =4 x + y,z'为 z' = 4z mod 2w,而z''为z'' =U2Tw(z')。数值z''等于x +wt
y。我们分成4种情况分析,
1) -2w ≤z < -2w-1。然后,我们会有z' = z + 2w。这就得出0≤z'<-2w-1+2w=2w-1。检查等式(2-8),我们看到z'在满足z'' = z'的范围之内。这种情况称为负溢出(negative overflow)。我们将两个负数x和y相加(这是我们能得到z < -2w-1的唯一方式),得到一个非负的结果z'' = x + y + 2w。
2)-2w-1≤z < 0。那么,我们又将有z'= z + 2w,得到-2w-1+2w=2w-1≤z' < 2w。检查等式(2-8),我们看到z'在满足z'' = -2w的范围之内,因此z'' = z'-2w = z + 2w-2w = z。也就是说,我们的补码和z''等于整数和x + y。
3)0≤z<2w-1。那么,我们将有z' = z,得到0≤z' < 2w-1,因此z'' = z' = z。补码和z''又等于整数和x + y。
4)2w-1≤z<2w。我们又将有z' = z,得到2w-1≤z' < 2w。但是在这个范围内,我们有z'' = z'-2w,得到z''= x + y -2w。这种情况称为正溢出(positive overflow)。
补码的非
无符号乘法
因此,w位无符号乘法运算* wu的结果为:
x * wu y=(x·y) mod 2w
补码乘法
w位的补码乘法运算* w t 的结果为:x * w t y = U2Tw((x · y) mod 2w)
乘以常数
由于整数乘法比移位和加法的代价要大得多,许多C语言编译器试图以移位、加法和减法的组合来消除很多整数乘以常数的情况。例如,假设一个程序包含表达式x*14。利用等式14 = 23 + 22 + 21,编译器会将乘法重写为(x<<3)+(x<<2)+(x<<1),实现了将一个乘法替换为三个移位和两个加法。无论x是无符号的还是补码,甚至当乘法会导致溢出时,两个计算都会得到一样的结果。
除以2的幂
除以2的幂可以通过逻辑或者算术右移来实现。这也正是为什么大多数机器上提供这两种类型的右移。不幸的是,这种方法不能推广到除以任意常数。同乘法不同,我们不能用除以2的幂的除法来表示除以任意常数K的除法。
关于整数运算的最后思考
正如我们看到的,计算机执行的“整数”运算实际上是一种模运算形式。表示数字的有限字长限制了可能的值的取值范围,结果运算可能溢出。我们还看到,补码表示提供了一种既能表示负数也能表示正数的灵活方法,同时使用了与执行无符号算术相同的位级实现,这些运算包括加法、减法、乘法,甚至除法,无论运算数是以无符号形式还是以补码形式表示的,都有完全一样或者非常类似的位级行为。 我们看到了C语言中的某些规定可能会产生令人意想不到的结果,而这些可能是难以察觉和理解的缺陷的源头。我们特别看到了unsigned数据类型,虽然它概念上很简单,但可能导致即使是资深程序员都意想不到的行为。我们还看到这种数据类型会以出乎意料的方式出现,比如,当书写整数常数和当调用库函数时。
二进制小数
IEEE浮点数表示
IEEE浮点标准用V = (-1)s × M × 2E的形式来表示一个数:
• 符号(sign) s决定这个数是负数(s=1)还是正数(s=0),而对于数值0的符号位解释作为特殊情况处理。
• 尾数(significand) M是一个二进制小数,它的范围是1~2-ε,或者是0~1-ε。• 阶码(exponent) E的作用是对浮点数加权,这个权重是2的E次幂(可能是负数)。将浮点数的位表示划分为三个字段,分别对这些值进行编码:
• 一个单独的符号位s直接编码符号s。
• k位的阶码字段exp = ek-1…e1e0编码阶码E。
• n位小数字段frac = fn-1…f1 f0编码尾数M,但是编码出来的值也依赖于阶码字段的值是否等于0。
情况1:规格化的值 这是最普遍的情况。当exp的位模式既不全为0(数值0),也不全为1(单精度数值为55,双精度数值为2047)时,都属于这类情况。在这种情况中,阶码字段被解释为以偏置(biased)形式表示的有符号整数。也就是说,阶码的值是E = e-Bias,其中e是无符号数,其位表示为ek-1…e1e0,而Bias是一个等于2k-1-1(单精度是127,双精度是1023)的偏置值。由此产生指数的取值范围,对于单精度是-126~+127,而对于双精度是-1022~+1023。
情况2:非规格化的值
当阶码域为全0时,所表示的数就是非规格化形式。在这种情况下,阶码值是E = 1 - Bias,而尾数的值是M = f,也就是小数字段的值,不包含隐含的开头的1
情况3:特殊值
最后一类数值是当指阶码全为1的时候出现的。当小数域全为0时,得到的值表示无穷,当s = 0 时是+∞,或者当 s = 1时是-∞。
舍入
向偶数舍入,向零舍入,向下舍入,向上舍入。
浮点运算
IEEE标准指定了一个简单的规则,用来确定诸如加法和乘法这样的算术运算的结果。把浮点值x和y看成实数,而某个运算⊙定义在实数上,计算将产生Round (x ⊙ y),这是对实际运算的精确结果进行舍入后的结果。在实际中,浮点单元的设计者使用一些聪明的小技巧来避免执行这种精确的计算,因为计算只要精确到能够保证得到一个正确的舍入结果就可以了。当参数中有一个是特殊值(如-0、-∞或NaN)时,IEEE标准定义了一些使之更合理的规则。例如,定义1/-0将产生-∞,而定义1/+0会产生+∞。
C语言中的浮点数
所有的C语言版本提供了两种不同的浮点数据类型:float和double。在支持IEEE浮点格式的机器上,这些数据类型就对应于单精度和双精度浮点。