现代计算机存储和处理的信息以二值信号表示。这些微不足道的二进制数字,或者称为位(bit),奠定了数字革命的基础。
把位组合在一起,再加上某种解释,即给不同的可能位模式赋予含义,我们就能够表示任何有限集合的元素。
研究三种最重要的数字表示:无符号(unsigned)、补码(two’s compliment)编码是表示有符号数的最常见方式,浮点数编码是表示实数的科学记数法的以二为基数的版本。
计算机的表示法是用有限数量的位来对一个数字编码,因此当结果太大以至于不能表示时,某些运算就会溢出。
浮点运算具有完全不同的数学属性。整数运算和浮点运算处理数字表示有限性的方式不同,所以会产生不同的数学属性。计算机用几种不同的二进制表示形式来编码数值。如何表述编码,如何推出数字的表示。
======================================================
信息存储
8位的块,即字节(Byte)是最小的可寻址的存储器单位。机器级程序将存储器视为一个非常大的字节数组,称为虚拟存储器。存储器的每个字节都由一个唯一的数字来标识,称它们为地址。所有可能的地址的集合称为虚拟地址空间。虚拟地址空间是展示给机器级程序的概念性映像。
编译器和运行时的系统对于存储空间的分配和管理完全是基于虚拟地址空间里完成的。每个程序对象(程序数据、指令、控制信息)可以简单视为一个字节块,程序本身就是一个字节序列。
十六进制表示法
十六进制数(简写为“hex”)表示位模式非常方便。0x或0X开头的数字常量被认为是十六进制的值。
字
每台计算机都有一个字长(word size)。指明整数和指针数据的标称大小。因为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的参数就是虚拟地址空间的最大大小。
数据大小
计算器和编译器支持多种不同方式编码的数字格式:整数、浮点数。
程序员应该力图使他们的程序在不同的机器和编译器上是可移植的。可移植的一个方面就是使程序对不同数据类型的确切大小不敏感。int和double之类的数据类型在不同平台上实际的长度不同。
寻址和字节顺序
对于跨越多字节的程序对象,必须建立两个规则:
这个对象的地址是什么?
在存储器中如何排列这些字节?
小端法(little endian):在存储器中按照最低有效字节到最高有效字节的方式存储;
大端法(big endian):在存储器中按照最高有效字节到最低有效字节的方式存储;
这个端指的是最低字节地址。
有时候字节顺序会成为问题:小端法机器产生的数据被发送到大端法机器或者反方向发送时会发现,接收程序字里的字节成了反序。
使用typedef命名数据类型可以极大提高代码可读性,因为深度嵌套的类型声明很难读懂。
表示字符串
字符串被编码为以null(其值为0)字符结尾的字符数组。每个字符都由某个标准编码来表示,最常见的是ASCII字符码。
文本数据比二进制数据具有更强的平台独立性。(与平台无关)因为没有最低有效位和最高有效位之说。
文字编码的Unicode标准,支持的字符集合更大。用32位表示字符。
表示代码
相同的代码在不同平台上生成的机器码是不一样的。不同机器使用不同的且不兼容的指令和编码方式。二进制代码是不兼容的,二进制代码很少能在不同机器和操作系统组合之间移植。
布尔代数简介
二进制值是计算机编码、存储和操作信息的核心。逻辑值True和False编码为0和1,,能够设计出一种代数,以研究逻辑推理的基本原则。
最简单的布尔代数是二元集合{0 1}上定义的,与、或、非、异或;
然后扩展到位向量的运算。位向量是有固定长度的,由0或1组成的串。位向量运算可以定义成每个对应元素之间的运算。位模式
位向量的一个很有用的应用就是表示有限集合。指定一个位向量掩码。
与运算:任何数和1进行与运算都等于它本身;0进行与运算就可以把任意值被置为0;
或运算:任何数和0进行或运算都等于它本身;1进行或运算就可以把任意值被置为1;
异或运算:任何数异或0等于其补码,异或1等于其本身;
以上两条原理就是掩码运算的基础。
C语言中的位级运算
C语音的很重要特性就是支持按位布尔运算,即按位位运算。
如果a是位向量,a^a=0;
不使用第三个临时值来交换两个数的值。
//交换两个值,不需要第三个值作为临时存储;这种方式没有性能上的优势,仅仅是一种智力游戏
void inplace_swap(int * x, int * y);
void inplace_swap(int * x, int * y)
{
printf("Before exchange:\n");
printf("x=%d, y=%d\n",*x,*y);
*y = *x ^ *y;
printf("First Step:\n");
printf("x=%d, y=%d\n",*x,*y);
*x = *x ^ *y;
printf("Second Step:\n");
printf("x=%d, y=%d\n",*x,*y);
*y = *x ^ *y;
printf("Final:\n");
printf("x=%d, y=%d\n",*x,*y);
};
int main()
{
int x = 5;
int y = 10;
inplace_swap(&x,&y);
}
C语言中的逻辑运算
逻辑运算容易与位运算混淆;
逻辑运算认为所有非零的参数都表示TRUE,而参数0表示FALSE;它们返回1或0,分别表示TRUE或FALSE;(逻辑运算的结果只有两种0 False或者1 True)
运算符:
OR ||
AND &&
NOT !
逻辑运算第一个参数求值能确定表达式的结果,就不会计算第二个参数的值。
C语言中的移位运算
移位运算,以便向左或向右移动位模式。
位表示为[Xn-1,Xn-2,…X0]的操作数x
x<<k
其位表示为:[Xn-1-k,Xn-2-k,…X0,0,…0]
也就是说x向左移动k位,丢弃最高的k位。并在右端补k个0。
移位运算从左往右可结合,即:
x<<j<<k 等价于 (x<<j)<<k
相应的右移运算有些微妙,机器支持两种形式的右移:逻辑右移,算术右移;
逻辑右移是在左端补k个0;
算术右移是在左端补k个最高有效位的值。->这种方式对于有符号整数数据的运算非常有用。
对于无符号数来说,右移必须是逻辑的。
对于有符号数来说,两种右移都可以。
这就意味着任何假设一种或另一种右移形式的代码都潜在着可移植性问题。实际上,所有机器和编译器都对有符号数采取算术右移,而且许多程序员都假设机器会使用这种右移。
实际上位移量是根据k mod w计算的出来的。所以位移量应保持小于字长。
===================================================
整数表示
用位来编码整数的两种不同方式:1)只能表示非负数,2)能够表示负数,零和正数;
数学属性和机器级实现方面的密切关联。
扩展和收缩一个已编码整数以适应不同长度表示的效果。
整型数据类型
无符号数的编码
B2Uw函数
补码编码
常见的有符号数的表示形式是补码形式(Two’s-complement)。将字的最高有效位解释为负权。假设有w位(0为基),最高位的权重为-xw-1 *2^(w-1);
B2Tw函数(Binary to Two’s-complement的缩写):是一个长度为w的位模式到TMin和TMax之间的映射(用数学来讲它们之间是一种双射)。反过来就是每个Tmin和Tmax之间的整数都有一个唯一的长度为w的位向量二进制表示。
补码的范围是不对称的:|TMin|=|TMax|+1;->这回导致补码运算的某些特殊性质。容易造成程序中细微的错误。之所以会导致这个错误是因为一般的位模式表示负数,而一般的位模式表示非负数和0。能表示的整数比负数少一个。
第二、最大的无符号数值刚刚好是补码的最大值的两倍大一点:UTMax = 2TMax+1;
补码存在的意义是为了统一加减;减法可以用加法代替。
(人的角度有加减法)<--->(对机器而言,只有加法)
时钟顺时针方向拨动+9,逆时针方向拨动-3,结果是一样的。->以12为模mod
有符号数的其他表示方法:
反码
原码
有符号数和无符号数之间的转换
B2UW 和 B2TW 都是双射,它们就有定义明确的逆映射。
U2Bw 和 T2Bw
U2Tw 定义为 U2Tw(x)=B2TW(U2BW(x)) 无符号->有符号
T2UW 定义为 T2UW(x)=B2UW(T2BW(x)) 有符号->无符号
两个公式的推导
C语言中的有符号数与无符号数
大多数数字默认都是有符号的。几乎所有的机器都采用补码。
要创建一个无符号常量,必须加上后缀字符’U’或者’u’,例如:12345U或者0x1A2Bu。
C语音允许有符号数与无符号数之间的转换,转换的原则是底层的位表示不变。
如果一个表达式同时出现了有符号数和无符号数,那么C语言会将有符号数参数强制类型转换为无符号数,并假设这两个数都是非负的。->这样有时候会导致一些问题。
扩展一个数字的位表示
问题:不同字长的整数之间转换,同时又保持数据不变。
1) 从较小字长转换成较大字长时,是可能的。
无符号数的扩展:添加0即可,零扩展。
补码数字:符号扩展,在表中添加最高有效位的值的副本。符号扩展n位都能保持值不变。
2) 从较大字长转换成较小字长时,是不可能的。
截断数字
从较大字长转换成较小字长。例如32位转换成16位。
可能会改变这个数的值à溢出的另一种形式。
将x截断到k位,在数学上相当于计算x mod 2k
有关有符号数与无符号数的建议
可以发现,有符号数到无符号数隐式强制转换导致了一些非直观的行为。
这些非直观的特性经常导致程序错误,并且这种包含隐式强制类型转换的细微差别的错误很难被发现。
所以我们要避免隐式强制类型转换,尽量不去用它,就不会犯错。
如果非要用到它时,更需要注意可能产生的一些非直观特性。
建议就是不使用无符号数计算。无符号数会把负数解释成正数。因为无符号计算的世界里没有负数。
当然无符号数还是有它的使用场景的,例如一些非数学计算场合。
往一个字里放入描述各种布尔条件的位标记时。
地址也是无符号数表示的。
====================================================
整数运算
机器运算存在有限性,所以会出现两个整数相加等于负数。理解计算机运算的微妙之处有助于帮助编写更为可靠的代码;
人是可以区分符号位的,但计算机辨别"符号位"会让计算机的基础电路设计变得十分复杂! 于是人们想出了将符号位也参与运算的方法.即补码运算。
无符号加法
一个算术运算溢出,是指完整的整数结果不能放到数据类型的字长限制中去。例如字长w=4的无符号加法,当两个运算数的和为2w或者更大时,就发生了溢出。加法溢出,相当于从和中减去16,即减去mod24。当执行C语言程序时,不会将溢出作为错误而发信号,不过有时候希望判定是否发生了溢出。如何判定是否发生了溢出呢?我们知道x+y=s,所以s>=x,s>=y。因此如果出现s<x,或s<y的情况,则可以判定发生了溢出。
无符号数加法溢出,数学直观上很好理解。
理解一种加法叫作模数加法;
补码加法
存在正溢出和负溢出两种情况。有个公式,可以反映正负溢出的结果。
补码的非
补码的不对称性;
-2w-1<=x<2w-1 ,如果x=-2w-1时,-x是多少?
有个公式,其逆元还是等于本身。
加法逆元。
无符号乘法
被看成是乘积模2w
补码乘法
模数乘法,然后U2T。
乘以常数
在大多数机器上,整数乘法指令相当慢。需要10个或者更多的时钟周期。然而其他整数运算(加法、减法、位级运算和移位)只需要1个时钟周期。编译器使用了一项重要优化,试着用移位和加法运算的组合来代替乘以常数因子的乘法。
首先会考虑乘以2的幂的情况,然后再概括成乘以任意常数。
逻辑左移和乘法的关系:x<<k等于x*pwr2k。pwr2k等于2k。
由于整数乘法比移位和加法的代价要大得多,许多C语言编译器试图以移位、加法和减法的组合来消除很多整数乘以常数的情况。
例如一个程序包含表达式x*14,利用等式14=23+22+21,编译器会将乘法重写为(x<<3)+(x<<2)+(x<<1)。实现了将一个乘法替换为三个移位和两个加法。
无论是无符号运算还是补码运算,乘以2的幂都可能导致溢出。即使溢出的时候,我们通过移位得到的结果也是一样的。
模数加法形成了一种数学结构,叫做阿贝尔群。阿贝尔群是可交换和可结合的。
除以2的幂
大多数机器上,整数除法要比整数乘法更慢。除以2的幂可以用移位运算来实现,只不过我们用的是右移。无符号数和补码数分别使用逻辑移位和算术移位来达到目的。
逻辑右移和除以2的幂之间的关系。x>>k等价于x/pwr2k。pwr2k等价于2k。
但是对于补码进行算术右移时会出现向下舍入的情况。-7/2应该得到-3,而不是-4。
整数除法应该将符的结果向上朝零舍入。通过修正偏置这个值来实现。9
当有舍入发生时,将一个负数右移k位不等于把它除以2k。
关于整数运算的最后思考
正如我们看到的,计算机执行的“整数”运算实际上是一种模运算形式。表示数字的有限性限制了可能的值的取值范围,结果运算可能溢出。补码提供了一种既能表示负数也能表示正数的灵活方式,同时使用了与执行无符号算术相同的位级实现。无论算数是以无符号形式还是以补码形式表示的,都有完全一样或者类似的位级行为。
C语言中的某些规定可能会产生令人意想不到的结果,而这些可能是难以察觉和理解的缺陷的源头。特别是看到unsigned数据类型,虽然它概念上非常简单,但可能导致即使是资深程序员都意想不到的行为。比如,书写整数常数和当调用库函数时。
=====================================================
浮点数
浮点数对涉及非常大和非常小的数字,以及更普遍地作为实数运算的近似值的计算,是很有用的。
一开始厂商不追求计算的精确性,而是把实现的速度和简便性看得更重要。
目前实际上所有的计算机都支持这个后来被称为IEEE浮点的标准。
将讨论IEEE浮点格式中是如何表示数字的?还将讨论舍入的问题。
二进制小数
对十进制表示法的一种扩展。
数字的权从10变成了2。
幂从正幂变成负幂。
这也就是定点表示法
IEEE浮点表示
通过给定x和y,来表示形如x*2y的数。
IEEE浮点标准:V=(-1)s*M*2E的形式来表示一个数:
符号s,决定这个数是负数还是整数;
尾数M是一个二进制小数;
阶码E的作用是对浮点数加权。Exponent
将浮点数的位表示划分为三个字段,分别对这些值进行编码。
数字示例
舍入
由于有限性导致必须通过舍入来满足有限性,但会缺少精确性。
浮点运算
浮点加法不具有分配性和结合性
C语言中的浮点数
所有C语言版本都挺了两种不同的浮点数据类型:float和double。
====================================================
小结
大多数机器对整数使用补码编码,而对浮点数使用IEEE编码。在位级上理解这些编码,并理解算术运算的数学特性,对于编写的程序能够在全部数值范围上正确运算的程序员来说,非常重要。
计算机将信息按位编码,通常组织成字节序列。用不同的编码方式表示整数、实数、字符串。
在相同长度的无符号整数和有符号整数之间进行强制类型转换时,大多数C语言遵循的原则是底层的位模式不点。对于w位的值,这种行为是由函数T2U和U2T来描述的。要注意C语言的隐式强制类型转换。
由于编码的长度有限,计算机运算与传统的整数和实数运算相比具有完全不同的属性。当超出表示范围时,有限的长度能够引起数值溢出。
使用移位运算、加减法运算的组合可以代替乘法和除法运算来提升效率。
必须非常小心地使用浮点运算,因为浮点运算只有有限的范围和精度,而且不遵守普遍的算术属性,比如结合性。