目录
信息存储
信息的存储
访问计算机最小的单位是八个位构成的字节,而不是值0或值1的单个位
程序会将存储器视为一个非常大的字节数组,称为虚拟存储器(virtual memory)。存储器的每一个字节都由唯一的数字来标识,也就是我们说的地址(address),所有可能地址的集合称为虚拟地址空间(virtual address space)
字
计算机进行数据处理时,一次存取、加工和传送的数据长度称为字(word),
一个字通常由一个或多个(一般是字节的整数位)字节构成,
字的位数叫做字长(word size)
- 由于虚拟地址空间中的地址就是使用一个字来编码的,因此字长决定了系统的虚拟地址空间的最大大小。
- 字长是CPU的主要技术指标之一,指的是CPU一次能并行处理的二进制位数,字长总是8的整数倍,通常PC机的字长为16位(早期),32位,64位。
寻址和字节顺序
对于跨越多个字节的程序对象来说,我们需要制定两个规则:
- ①、这个对象的地址是什么?
- ②、在存储器中如何排列这些字节?
1:在几乎所有的机器上,多字节对象都被存储为连续的字节序列,对象的地址为所使用字节中最小的地址。
2:采用如下两种方式:
x 的低位字节值到高位字节值分别为 67,45,23,01。用大端法和小端法表示:
- 小端法:按照从最低有效字节到最高有效字节的顺序存储对象, 也就是最低有效字节在最前面。
- 大端法:按照从最高有效字节到最低有效字节的顺序存储对象, 也就是最高有效字节在最前面。
进制间的转换
①. 其他进制转十进制:将二进制数、十六进制数的各位数字分别乘以各自基数的(N-1)次方,其相加之和便是相应的十进制数,这是按权相加法。
②. 十进制转其他进制:整数部分用除基取余法,小数部分用乘基取整法,然后将整数与小数部分拼接成一个数作为转换的最后结果。
③. 二进制转十六进制:从小数点位置开始,整数部分向左,小数部分向右,每四位二进制为一组用一位十六进制的数字来表示,不足四位的用0补足。
④. 十六进制转二进制:每一位十六进制对应每四位二进制,不足用0补足。
整数的表示
计算机在解释一个数据类型的值时主要有四个因素:
位排列规则(大端或者小端)、起始位置、数据类型的字节数、数据类型的解释方式。
无符号数的编码
定义:假设对于一个w位的无符号整数,用二进制比特位可以表示为[xw-1 , xw-2 , … , x2 , x1 , x0]。那么我们可以用一个函数表示如下:
无符号的二进制,对于任意一个w位的二进制序列,都存在唯一一个整数介于0 到 2w-1之间,与这个二进制序列对应。
反过来,在0 到 2w-1之间的每一个整数,存在唯一的二进制序列与其对应。
有符号-补码编码
补码的定义如下:
其中最高有效位 xw-1 也称为符号位,符号位为 1 时表示负数,当设置为 0 时,表示非负数。下面我们看几个例子:
对于任意一个w位的二进制序列,都存在唯一一个介于-2w-1 到 2w-1-1的整数,与这个二进制序列对应。反过来,对于任意介于-2w-1 到 2w-1-1的整数,存在唯一的长度为w二进制序列与其对应。
反码和原码
反码定义:除了最高有效位的权是-2w-1-1,而不是-2w-1其余的和补码表示方式一样
原码定义:最高有效位是符号位,用来确定剩下的位是正还是负
- 原码:一个整数,按照绝对值大小转换为二进制数,最高位为符号位。
- 反码:将原码除最高位(符号位)外,其余各位按位取反,所得到的二进制码。正数的反码为原码。
- 补码:反码最低位加1即为补码。
对于正整数,原码、反码、补码完全一样,即符号位固定为0,数值位相同。
对于负整数,原码和补码互相转换的简便方法:
- 从数的右边往左开始数,遇到“0”不理它,直到遇到第一个“1”为止,以后的每一位数取反即是它的原码或补码,符号位不变,还是“1”(补码的补码是原码)。
比如:11010100 ----- 从右往左数,第一位是0,不理它,第二位还是0不理它,第三位是1,那么从此以后的每位取反,即为它的补码了.答案为:10101100
有符号和无符号数之间的转换
有符号数强转为无符号数
下图为表示补码编码与无符号编码的对应关系,可以看出在0至2w-1-1之间,两者是相等的,而其余区间则不同。
从上图我们也可以得出:当将一个有符号数映射为它相应的无符号数时,负数就被转换成了大的正数;而非负数会保持不变。
无符号数转换为有符号数
同样的,在0至2w-1-1之间,两者依然是相等的,而其余区间则不同。
C语言中的有符号数和无符号数以及扩展和截断数字
注意:在 C 语言中,当执行一个运算,会隐式的将有符号参数强转为无符号参数
扩展一个数字的位表示
①、零扩展
- 将一个无符号数转换为一个更大的数据类型,我们只需要简单的在二进制序列前面添加 0 即可。
②、符号位扩展
- 将一个补码数字转换为一个更大的数据类型,我们需要在开头添加符号位。
由上面两条我们可以总结:
- 如果我们原始位为[xw-1 , xw-2 , … , x2 , x1 , x0],那么扩展后就可以表示为:[xw-1 ,xw-1 ,...,xw-1 , xw-2 , … , x2 , x1 , x0]。
截断数字
这和上面的扩展刚好相反。即我们不需要额外的扩展一个数的位,而是减少一个数字的位数。
- 将一个 w 位的数 [xw-1 , xw-2 , … , x2 , x1 , x0] 截断为一个 k 位数字时,我们会丢弃高 w-k 位。得到 [xk-1 , xk-2 , … , x2 , x1 , x0]
而对于有符号(补码编码)的截断,我们只需要多加一步,将无符号编码转换为补码编码就可以了。
整数的运算
无符号数加法运算
如果两个无符号整数作加法运算。当 x+y < 2w 时,它们的结果不变;当 2w <= x+y < 2w+1,它们的结果为 x+y-2w
补码加法运算
范围在 -2w-1 <= x,y <= 2w-1-1 做加法运算时
补码加法运算就是先按照无符号加法进行运算,而后在进行无符号和有符号的转换。
#include <stdio.h> int main() { short int i = -32768; short int j = i-1; printf("%d\n",j); return 0; }
为什么 -32768-1 结果会是 32767?
我们需要先将 -32768 和 -1 分别转换成无符号数进行加法运算,然后对得到的结果转换成有符号数。
①、-32768 转换成无符号数也就是 -32768+2^16=32768
②、-1 转换成无符号数也就是-1+2^16=65535
③、将上面两步的结果相加,然后转换成有符号数:
即(65535+32768)-2^16=65535+32768-65536=32767
这个过程用到的公式分别有
无符号数乘法运算
#include <stdio.h> int main() { unsigned short int i = 2; unsigned short int j = i*2; printf("%u\n",j); return 0; }
上面的程序结果是:(2*2)mod 2^16=4 mod 65536=4
补码乘法
补码乘法运算公式为
乘法优化
编译器使用了一项重要的优化,使用移位和加法的组合来代替乘法。
结论:对于一个w位的二进制数来说,它与2k的乘积,等同于这个二进制数左移k位,在低位补k个0。
比如:
计算 x*14 的乘积。
- 由于 14 = 23+22+21 ,那么编译器会将乘法重写为(x<<3)+(x<<2)+(x<<1)。这样就将乘法替换为三个移位和两个加法。无论 x 是无符号还是补码,甚至当乘法会导致溢出时,两个计算都会得到一样的结果。
- 更好的编译器,可能会将 14 = 24-21。那么就会变成(x<<4)-(x<<1),只需要两个移位和一个减法。
除法运算
结论:对于除以 2 的幂可以用移位来运算。无符号除法使用逻辑移位,补码除法使用算术移位。
浮点数
二进制小数
进位计数制的要素:
- ①、数码:用来表示进制数的元素。比如二进制数的数码为:0,1。十进制数的数码为:0,1,2,3,4,5,6,7,8,9。
- ②、基数:数码的个数。比如二进制数的基数为2。十进制数的基数为10。十六进制数的基数为 16.
- ③、位权:数制中每一固定位置对应的单位值称为位权。例如十进制第2位的位权为10^1即10,
我们可以说:每个数码所表示的数值=该数码值 * 所处位置的位权。
比如
- 十进制数:(123.45)10 =1×10^2+2×10^1+3×10^0+4×10^-1+5×10^-2
- 十六进制数:(BAD)16 =11× 16^2+10×16^1+13×16^0=(2989)10
- 二进制小数(10010.1)2 = 1 * 2^4 + 0 * 2^3 + 0 * 2^2 + 1 * 2^1 + 0 * 20 + 1 * 2^-1
对于一个形式为bm....b0.b-1....b-n的二进制小数b来说,二进制表示公式
从上面的二进制公式我们可以看出,
小数点向左移动一位,则相当于 (∑ 2i * bi)/2。因为每一位的位权都*2-1;反过来,小数点向右移动一位,则相当于该数乘以2。
注意:
二进制小数不像整数一样,只要位数足够,它就可以表示所有整数。比如十进制小数0.2,我们并不能将其准确的表示为一个二进制数,只能增加二进制长度提高表示的精度。
IEEE 浮点表示
IEEE 浮点标准表示: V = (-1)^s * M * 2^E 。
- ①、s 是符号位,为0时表示正,为1时表示负。
- ②、M为尾数,是一个二进制小数,它的范围是0至1-ε,或者1至2-ε(ε的值一般是2-k次方,其中设k > 0)
- ③、E为阶码,可正可负,作用是给尾数加权
我们将浮点数的位划分为三个阶段,分别对这些值进行编码。
- 一、一个单独的符号位 s 直接编码符号 s
- 二、k 位的阶码字段 exp =ek-1ek-2...e1e0 编码阶码E
- 三、n 位小数字段 frac = fn-1fn-2...f1f0 编码尾数 M,但是编码出来的值也依赖于阶码字段的值是否等于0.
一般来说,现在的编译器都支持两种浮点格式,一种是单精度,一种是双精度。
- 其中float是单精度的,采用32位二进制表示,其中1位符号位,8位阶码以及23位尾数。
- double是双精度的,采用64位二进制表示, 其中1位符号位,11位阶码以及52位尾数。如下图表示
如果给定了位 s 的表示,根据 exp 的值,被编码的值可以分为三种不同的情况(最后一种情况有两个变种)。下图是单精度的情况:
规格化
阶码E 的位模式exp既不全为0(数值0),也不全为1
非规格化的值
当阶码域为全 0 的时候,所表示的数就是非规格化形式。
特殊值
特殊值是指阶码全为 1 的时候出现的
- 在阶码全为1时,如果尾数位全为0,则表示无穷大。符号位为0则表示正无穷大,相反则表示负无穷大。
- 倘若尾数位不全为0时,此时则表示NaN,表示不是一个数字。
- 一些运算的结果不能是实数或者无穷,就会返回NaN值,比如正无穷减正无穷,-1的根号值。 在某些应用中表示未初始化的值,也很有用处。
舍入
舍入一共有四种方式,分别是向偶数舍入、向零舍入、向上舍入以及向下舍入。
浮点运算
在IEEE标准中的运算规则,就是我们将把两个浮点数运算后的精确结果的舍入值,作为我们最终的运算结果。
我们看到 f1 的值是0,f2的值才是3.14。为什么呢?
- 这是因为前面3.14f+10000000000f 时,会将 3.14 这个有效数值舍入掉,而导致最终结果为0.0
- f2 由于括号的存在,会先进行括号里面的运算,结果是0,然后在与3.14相加
- 也就是浮点运算不满足加法的结合律 a + b + c != a + (b + c)。
- 同时乘法结合律也不满足:a * b * c != a * (b * c);还要分配律也不满足: a * (b + c) != a * b + a * c
浮点数失去了很多运算方面的特性,因此也导致很多优化手段无法进行,比如我们试图优化下面这样一段程序。
/* 优化前 */ float x = a + b + c; float y = b + c + d; /* 编译器试图省去一个浮点加法 */ float t = b + c; float x = a + t; float y = t + d;
上面优化前是进行了四次浮点运算,而编译器优化后只需要进行三次浮点运算。
- 但是这中间的 x 可能回产生与原始值不同的值,因为它使用了加法运算不同的结合方式。所以现在的编译器都倾向于保守的方式,避免任何对功能产生的优化,即使是很轻微的影响。