信息存储
二进制与十六进制
计算机内所有的信息均以二进制的形式表示,也就是由值0和值1组成的序列。大多数计算机使用8位的块作为最小的可寻址单位,也就是常说的字节(Byte)。
一个字节包含8比特。1Byte = 8bit ,1KB = 1024Byte 。
二进制表示法比较冗长,我们一般使用十六进制来代替二进制,在二进制表示法中,它的值域为 00000000——11111111。十六进制使用数字0~9,以及字符 A~F来表示16个可能的值。规则是:借一当十六,逢十六进一。二进制与十六进制关系:
在编程语言一般使用 0x
或者0X
开头表示十六进制数据,例如 0x01
代表10进制的1。
1、十六进制与十进制转换:
假设16进制0x12C
,对应的 10进制数是: (1*16^2 + 2*16^1 + 12*16^0 = 256+32+12 = 300)。
2、十进制与十六进制转换:
假设10进制300
,对应的 16进制数是:300 / 16 = 18......12,18 / 16 = 1......2, 1 / 16 = 0......1
,停止计算,那么对应的16进制数需要倒序取余数,也就是 1、2、12,也就是0x12C
。
3、二进制与十进制转换:
假设2进制1100
,对应的 10进制数是: (1*2^3 + 1*2^2 + 0*2^1 + 0*2^0 = 8+4+0+0 = 12)。
4、十进制与二进制转换:
假设10进制12
,对应的 2进制数是:12 / 2 = 6......0, 6 / 2 = 3......0, 3 /2 = 1.......1, 1/2 = 0.....1
,停止计算,那么对应的2进制数需要倒序取余数,也就是 1、1、0、0,也就是1100
。
总结:对于整数,10进制转N进制的算法就是不断用商除以N,直到商为0,再把得到的余数倒序就得到对应进制的数;N进制转10进制,使用公式: $ sum_{i=0}^{w-1}x_{i}cdot N^{i} $ 。
w为数字长度,N为进制。
字符串的表示
对于英文字符最常用到的编码方案有ASCII编码:http://c.biancheng.net/c/ascii/。 对于中文,也有对应的编码,比如 GBK,GB2312等。为了统一各国语言编码,现在一般使用Unicode编码:https://baike.baidu.com/item/Unicode/750500?fr=aladdin 。
一个例子就是字符a
的 ASCII 码十进制值为 97,在计算机中用二进制表示就是 01100001;大写A
则是65(二进制01000001)。
关于字符串本文不做展开描述。
位运算
记忆口诀:
- 与:全1为1
- 或:有1为1
- 异或:相异为1
移位运算
整数表示
整数包含正整数,0,负整数。计算机只能支持有限范围的整数。
下面的符号后续会用到,不用强记:
整数数据类型
C 语言是支持多种整型数据类型的,下面我们看一下在 32 位机器和 64 位机器中,C 语言整型数据类型的取值范围:
- 数据类型分配的字节数会根据机器的字长和编译器有所不同,不同的大小所表示的范围是不同的。例如 long 类型。
- 负数的范围要比正数的范围大1。(为什么?)
实际上,C 语言标准所定义的每种数据类型所能表示的最小的取值范围与上面的是不一样的:
从上表可以看出:C 语言标准里,正数和负数的取值范围是对称的。
无符号数编码
无符号数,在C语言中,即用 unsigned 声明的整数。
原理1:
w指的是一个w位的二进制数。下面是一个示例:
这个原理说明了二进制数如何转为十进制数。
原理2:
结论:无符号的二进制,对于任意一个w位的二进制序列,都存在唯一一个整数介于0 到 $ 2^w-1 $之间,与这个二进制序列对应。反过来,在0 到 $ 2^w-1 $ 之间的每一个整数,存在唯一的二进制序列与其对应。
补码编码
学习了无符号数的编码后,我们还得知道计算机是如何存储负数的。
在计算机中,最常见的表示有符号的数就是补码(two's complement)。补码的定义如下:
原理1:
最高有效位 $ x_{w-1} $ 也称为符号位:符号位为 1 时表示负数,当设置为 0 时,表示非负数。这个公式后面还会用到,请牢记。
下面是示例:
补码格式的最小值: $ 1000......000_{2} = -2^{w-1} + 0 = -2^{w-1} $ ,假设w=8, 值为 -128
补码格式的最大值: $ 0111......111_{2} = 2^w - 1 (。推导:二进制数按照补码编码展开:) -0 + 2^{w-1} + 2^{w-2} + ... + 2^0 $ ,发现符合等比公式(1,2,4,8...),所以利用等比公式求得:(1 * frac{1-2^w}{1-2} = -1 * (1-2^w) = 2^w - 1)。假设w=8, 值为127。
由此可知,w=8长度时,负数可表示-128,正数最大是127。这也就是为什么负数的范围要比正数的范围大1。
附:等比公式:
原理2:
结论:对于任意一个w位的二进制序列,都存在唯一一个介于 (-2^{w-1}) 到 (2^{w-1}-1)的整数,与这个二进制序列对应。反过来,对于任意介于 (-2^{w-1}) 到 (2^{w-1}-1) 的整数,存在唯一的长度为w二进制序列与其对应。
无符号数转补码
示例:
w=8,u=255,$TMax_w = 2^7-1=127 $,结果是 : (255 - 2^8 = -1)
w=8,u=126,$TMax_w = 2^7-1=127 $,结果是 : 126。
C代码:
#include <stdio.h>
int main()
{
unsigned char u = 126;
char t = (char)u;
//%d把对应的整数按有符号十进制输出,%u把对应的整数按无符号十进制输出
printf("u=%u,u2t=%d
",u,t);
return 0;
}
输出:
$ gcc t1.c -o t1 && ./t1
u=126,u2t=126
补码转无符号数
示例:
w=8,x=-1,结果是 : (-1 + 2^8 = 255)
w=8,x=126,结果是 : 126。
C代码:
#include <stdio.h>
int main()
{
char t = -1;
unsigned char u = (unsigned char)t;
printf("t=%d,t2u=%u
",t,u);
return 0;
}
输出:
$ gcc t1.c -o t1 && ./t1
t=-1,t2u=255
原码和反码
有符号数还可以使用原码、反码表示。
从公式可以看出:
原码公式里,正数$ x_{w-1} $ = 0,那么 (-1^0) = 1,与补码相同;负数$ x_{w-1} $ = 1,那么 (-1^1) = -1;
反码公式里,(-x_{w-1}^{(2^{w-1}-1)}),正数$ x_{w-1} $ = 0,反码与原码相同。
原码和补码互相转换的简便方法:
正数的原码、反码、补码相同,负数的反码是原码按位取反(符号位不变),负数的补码是反码加1(符号位不变)。但是使用原码、反码,对于0的表示是不一致的,而使用补码则是一致的。
以8位的二进制为例:
1的原码为:00000001, 反码为:00000001, 补码为:00000001
-1的原码为:10000001,反码为:11111110,补码为:11111111
+0的原码为:00000000, 反码为:00000000, 补码为:00000000
-0的原码为:10000000,反码为:11111111,补码为:00000000
(反码加1产生了进位,变成100000000,最高位舍弃掉了)
扩展一个数字的位表示
扩展一个数字的位,简单来说就是在不同字长的整数之间转换,而这种转换我们可以需要保持前后数值不变。
- 将一个无符号数转换为一个更大的数据类型,我们只需要简单的在二进制序列前面添加 0 即可。
无符号的,添加0,这很好理解,前后数值不变。
- 将一个补码数字转换为一个更大的数据类型,我们需要在开头添加符号位。
上面的原理表明,在开头添加若干个符号位,补码的值与原来保持相等。那么我们可以推导出补码加了k位的符号位的数也满足:
下面是推导证明过程:
由 (B2T_{w} = -x_{w-1}2^{w-1} + sum^{w-2}_{i=0}x_i2^i) 可得出 (B2T_{w+1}) 展开后的值:
其中,需要理解的是 (2^w - 2^{w-1}) 为什么等于 (2^{2-1}):
$2^w - 2^{w-1} = 2^{w-1+1} - 2^{w-1} = 2^{w-1} * 2 - 2^{w-1} = 2^{w-1} $
示例:
w=4, 有符号二进制数 $1001_2 = -2^3 + 0 + 0 + 1 = -7 $;
扩展导w=8,补充4位符号位(1),即 $11111001_2 = -2^7 + 2^6 + 2^5 + 2^4+ 2^3 + 0 + 0 + 1 = -128+64+32+16+8+1 = -7 $
C代码:
#include <stdio.h>
int main()
{
char t = -7;
short int t2 = (short int)t;
printf("t=%d,t2=%d
",t,t2);
return 0;
}
输出:
$ gcc t1.c -o t1 && ./t1
t=-7,t2=-7
截断数字
假设我们需要把一个数的位数减少,例如由 short int 变成 1字节的 char类型,这就需要使用截断原理了。
示例:
w=8, 有符号二进制数 $11111001_2 = -7 $;截断4位,得到: $1001_2 = U2T_k( 9 mod 16 ) = U2T_k(9) = 9 - 2^4= -7 (,其中:)TMax_w = 2^3-1 = 7$
附:
整数运算
无符号加法
原理:
注意:当 (2^w) <= x+y <(2^{w+1}),对 x + y 进行(2^w)的取模运算,与 x + y - (2^w)是等价的。
示例:
#include <stdio.h>
int main()
{
unsigned short int i = 65535;
unsigned short int j = i+2;
printf("%u
",j);
return 0;
}
运行结果:
$ gcc t1.c -o t1 && ./t1
1
注: $ 2^{16} $ = 65536, (65536+ 2) % 65536= 1 。
补码加法
与无符号加法运算不同,补码加法会出现三种情况:正溢出、正常、负溢出。
原理:
示例:
#include <stdio.h>
int main()
{
short int i = -32768;
short int j = i-2;
printf("%u
",j);
return 0;
}
运行结果:
$ gcc t1.c -o t2 && ./t2
32766
注: $ 2^{16-1} $ = 32768 。-32768 -2 + 65536 = 32766
补码的非
(TMin_w)表示补码最小值,对于1字节也就是 (10000000_2 = 0) ,这个时候取非等于自身。x大于补码最小值,是自身的逆值。
无符号乘法
示例:
-
x=2, y=4, 均为 short int 类型,那么 值为 (2*4) mod 2^16 = 8 mod 65536 = 8
-
x=65535, y=2, 均为 short int 类型,那么 值为 (65535*2) mod 2^16 = 65534 mod 65536 = 65534。
(65535*2) 发生溢出了,值等于 (65535*2 - 2^{16} = 65534)
补码乘法
其中,无符号数转补码公式:
(TMax_w)表示补码的最大值,对于1字节也就是 (01111111_2 = 2^7-1 = 127) 。
补码乘法先当做无符号乘法计算,得出的结果使用无符号数转补码公式计算:如果结果大于补码最大值,需要减去模值 (2^w),否则就是无符号乘法的结果。
乘以常数
对于一个w位的二进制数来说,它与2k的乘积,等同于这个二进制数左移k位,在低位补k个0。
最后的x就代替了被左移的数本身,相当于中括号那一串。
除以2的幂
对于除以 2 的幂可以用移位来运算。无符号除法使用逻辑移位,补码除法使用算术移位。
- 逻辑右移在左端补k 个0。C语言中对于无符号数据必须逻辑右移。
- 算术右移是在左端补 k 个最高有效位的值。对于一个正整数,由于最高有效位是 0 ,所以效果和逻辑右移是一样的;对于非负数,算术右移 k 位与除以 2k 是一样的。
浮点数表示
浮点数在计算机内部是按照IEEE 754规范存储的。
IEEE浮点表示
在IEEE 754标准中浮点数由三部分组成:符号位
(sign bit),有偏指数
(biased exponent),小数
(fraction)。
指数又称阶码。
浮点数分为两种:单精度浮点数
(single precision)和双精度浮点数
(double precision),它们两个所占的位数不同。
-
符号位:这个就是指的该数是正数还是负数。
-
有偏指数: 实际存储的指数部分不是直接存储的,需要加上一个偏移量(Bias)。这个偏移量对于单精度浮点数来说是 (2^7-1) = 127,对于双精度浮点数来说则是 (2^{10}-1) = 1023。假设e是阶码的无符号数值,那么真实的阶码
E = e - Bias
。-
如果指数是正数,有偏指数将大于偏移量,单精度区间 127 ~ 255;
-
如果指数是负数,有偏指数将小于偏移量,单精度区间 0 ~ 126;
-
如果存储的值等于偏移量,那么就意味着指数为0。(有特殊意义,后文再讲)
-
-
小数:就是小数有效位。
如图:对于单精度浮点数,符号位占1位,有偏指数占8位,小数占23位;对于单精度浮点数,符号位占1位,有偏指数占11位,小数占52位。
以单精度浮点数 0.15625
讲解浮点数的存储过程:
(0.15625_{10}) 转化为二进制就是 (0.00101_2),然后将该数写成科学计数法,根据IEEE 754的规定,小数点的左边只能有一个1,所以最终的科学计数法形式是:
(0.15625_{10}) = (0.00101_2) = (1.012 * 10^{-3}), 然后就可以得到小数部分为 (.01_2),指数部分为-3
。
其中:符合位为0,表示正数;在单精度浮点数中偏移量是127,因此127+(-3)=124,所以偏移指数是124(在双精度浮点数中偏移量是1023,因此偏移指数是1020);小数: (.01000000000000000000000_2) 。最终在内存中的存储结果就是如下图:
根据有偏指数 的取值,可以分为三种类型的数:
规格化
有偏指数既不全为0(数值0),也不全为1,这种情况下,指数字段被解释为以偏置形式表示的有符号整数。
既然小数点的左边都是以一个1开始的,为了节约内存,IEEE 754又规定:所有数在小数点左边默认有一个1。
按照这个规定的话,那么能够表示的最小正数就是(单精度浮点数):
$ 0 00000001 00000000000000000000001_{2f} = 1.00000000000000000000001_2 * 2^{-126} $
其中有有偏指数为1,实际指数是1-127 = -126;小数部分是 $00000000000000000000001_2 $ ,注意小数点左边默认有一个1,然后就得到上面的值。
示例:
$25_{10} = 11011_2 = 1.1011_2 * 2^4 $
$0.125_{10} = 0.001_2 = 1.0_2 * 2^{-3} $
非规格化
如果有偏指数全为0,只能表示数字0的话,那么表示小数位的23位就没有利用起来。于是IEEE754的设计者,规定了一种新的数 非规格化的值:当有偏指数域为全 0 的时候,所表示的数就是非规格化的值。。
这种情况下,指数的值 E = 1 - Bias
。这样做是为了能够平滑的从非规格化的浮点数过渡到规格化的浮点数。这个时候我们默认小数点的左边只能有一个0。
在次正规数中所有的偏移指数位都是0,于是规定在单精度浮点数中指数应该为-126(并非-127),在双精度浮点数中指数应该为-1022(并非-1023)。
这样一来,能表示的最小正数就是(单精度浮点数):
$ 0 00000000 00000000000000000000001_{2f} = 0.00000000000000000000001_2 * 2^{-126} $
那么0在IEEE 754中怎么表示呢?
数值0被特殊表示:
符号位(sign) = 0或1
有偏指数(biased exponent) = 0
小数(fraction)= 0
对应的二进制也就是(单精度浮点数):
$ 0 00000000 0000000000000000000000_{2f} = 0.0_2 * 2^{0-127} $ = 0.0
$ 1 00000000 0000000000000000000000_{2f} = - 0.0_2 * 2^{0-127} $ = 0.0
特殊值
特殊值是指有偏指数全为 1 的时候出现的。
特殊值包括2种:
- 无穷大:小数部分全是0。如果符号位为0,表示正无穷大;如果符号位为1,表示负无穷大。
- NaN:小数部分不全为0。
浮点数的范围
注意:由于浮点数在正负的区间内是一一对应的,因此我们将忽略符号位对取值范围的影响,我们只讨论符号位为0的情况。
-
0:有偏指数全为0,小数部分全为0.
-
最小非规格化数:有偏指数全为0,小数部分最后一位是1。
(0.00000000000000000000001_2 * 2^{1-127} = 2^{-23}*2^{-126})
-
最大非规格化数:有偏指数全为0,小数部分全是1。
(0.11111111111111111111111_2 * 2^{1-127} = (1-2^{-23})*2^{-126})
$1-2^{-23} $书里说写做 $ 1 - ε$
-
最小规格化数:有偏指数末位为1,小数部分全是0。
$ 1.0 * 2^{1-127} = 1.0 * 2^{-126} $
-
1:有偏指数为01111111,值 (2^7-1 = 127),小数部分全是0。
(1.0 * 2^{127-127} = 1.0 * 2^0 = 1.0)
-
最大规格化数:有偏指数为11111110,小数部分全是1。
[1.11111111111111111111111_2 * 2^{2^8-1-1-127} = 2-2^{-23} * 2^{127} ] -
正无穷大:有偏指数为11111111,小数部分全是0。
-
NaN:有偏指数为11111111,小数部分不全是0。
浮点数的精度
在单精度浮点数中的二进制小数位有23个,所能表示2^23个数,那么只需要换算成在10进制下能够表示相同个数的位数,就可以得到精度了。
(10^n = 2^{23} = 8388608)
$lg{8388608} = 6.92368990 $
所以但精度浮点数的精度范围为6位,精度误差 (10^{-6})。同理也可以得到双精度浮点数的精度为15位。
浮点数的相等判断中,只需要判断他们的差值小于精度误差就可以了。
二进制小数
1、二进制转为十进制:以小数点为界,整数位从最后一 位(从右向左)开始算,依次列为第0、1、2、3………n,然后将第n位的数(0或1)乘以2的n-1次方,然后相加即可得到整数位的十进制数;小数位则 从左向右开始算,依次列为第1、2、3……..n,然后将第n位的数(0或1)乘以2的-n次方,然后相加即可得到小数位的十进制数(按权相加法)。。
示例:将二进制数(10.101)转化为十进制数。
(10.101)2 = 1 * 2^1 + 0* 2^0 + 1*2^-1+ 0*2^-2+ 1*2^-3 = 2+0+0.5+0+0.125 = 2.625
2、十进制转为二进制:整数部分转换规则不变:不断用商除以2,直到商为0,再把得到的余数倒序就得到对应进制的数;小数部分不断乘以2,得到的数的整数部分按顺序保留,小数部分不为0则继续乘以2,最终小数部分由各整数部分组成的数字就是转化后二进制小数的值(正序)。
示例:求十进制10.4对应的二进制:
10 /2 = 5...0, 5 / 2 = 2...1, 2 /2 = 1...0, 1/2=0...1 所以整数部分的二进制是:1010
0.4 * 2 = 0.8, 整数部分是0,小数位不为0,继续;
0.8 * 2 = 1.6, 整数部分是1,
0.6*2 = 1.2 , 整数部分是1,
0.2*2 = 0.4 , 整数部分是0,
0.4*2 = 0.8, 整数部分是0,
...
保留3位小数,则是0.011。最终10.4对应的二进制是1010.011,是个近似数,这也说明浮点数是不精确的。