信息的表示和处理
前言
计算机是以二进制(0 和 1)的方式存储信息的,包括数字,字符,图像,音频等。每个二进制位称为比特(bit),8 个二进制位为一个字节(byte)。内存的最小寻址单位就是一个字节。在研究二进制数据时使用 01 的方式不便于阅读,一般会转换十六进制来表示。
大端存储和小端存储
对于二进制数如何在计算机中存储这个问题,人们提出了两种解决方案,大端法和小端法。
对于数据 0x01234567 的存储:
大端法:
… | 0x100 | 0x101 | 0x102 | 0x103 | … |
---|---|---|---|---|---|
… | 01 | 23 | 45 | 67 | … |
小端法:
… | 0x100 | 0x101 | 0x102 | 0x103 | … |
---|---|---|---|---|---|
… | 67 | 45 | 23 | 01 | … |
大端法将高位字节 0x01 放在了前面,而小端法将低位字节放在了后面。
下面有一个示例程序来验证机器使用的存储方式为何种:
1 |
|
如果是小端法的机器则输出67 45 23 01
,而大端法机器输出01 23 45 67
。
这两种存储方式在技术上并无优劣,只是顺序上有所不同,对于这两种存储方式的争论并无意义。
整数表示
C 语言数据类型 | 最小值 | 最大值 | 占用空间(字节) |
---|---|---|---|
char | $-2^7$ | $2^7-1$ | 1 |
unsigned char | $0$ | $2^8-1$ | 1 |
short | $-2^{15}$ | $2^{15}-1$ | 2 |
unsigned short | $0$ | $2^{16}-1$ | 2 |
int | $-2^{31}$ | $2^{31}-1$ | 4 |
unsigned int | $0$ | $2^{32}-1$ | 4 |
long | $-2^{31}$ | $2^{31}-1$ | 4 |
unsigned long | $0$ | $2^{32}-1$ | 4 |
int32_t | $-2^{31}$ | $2^{31}-1$ | 4 |
uint32_t | $0$ | $2^{32}-1$ | 4 |
int64_t | $-2^{63}$ | $2^{63}-1$ | 8 |
uint64_t | $0$ | $2^{64}-1$ | 8 |
整型数据使用补码方式表示:
比如:
$(1){10}=(00000001){2}$
$(-1){10}=(11111111){2}$
此时最高位的位权不再是128
而是-128
原码转补码就是x->~x+1
整数运算
加法
当进行求和计算时,若它们的和超过了数据类型的表示范围时就发生了溢出(overflow),计算机会将溢出的数据直接截断保留低位有效字节,比如:0xF1 + 0x0F = 0x00
减法
进行减法计算时,会将减数取补然后使用加法的电路进行计算,比如:0x7F - 0x12 = 0x7F + 0xEE = 0x6D
乘法
进行乘法计算时也是和加法一样,溢出的部分截断保留低位有效字节,比如:-3 * 3 = 101 * 011 = 110111
然后截断成111
。
计算机计算二进制乘法的方式类似于人类笔算的方式:- 正数 * 正数
1
2
3
4
5
6
7
8
90011 3
* 0100 4
-------
0000000
000000
00011
0000
-------
0001100 12- 正数 * 负数
1
2
3
4
5
6
7
8
90011 3
* 1100 -4
-------
0000000
000000
00011
1101
-------
1110100 -12- 负数 * 正数
1
2
3
4
5
6
7
8
91101 -3
* 0100 4
-------
0000000
000000
11101
0000
-------
1110100 -12- 负数 * 负数
1
2
3
4
5
6
7
8
91101 -3
* 1100 -4
-------
0000000
000000
11101
0011
-------
0001100 12乘以常数
如果作为乘数的常数为 2 的幂则可以直接进行x<<n
的操作,如果不是 2 的幂可以通过左移和求和的方式来完成乘法,比如:$14 = 2^3 + 2^2 + 2^1$,所以x * 14 = (x<<3) + (x<<2) + (x<<1)
或者$14 = 2^4 - 1$,所以x * 14 = (x<<4) - (x<<1)
,显然后者的步数比较少,效率更高除以 2 的幂
如果作为除数的常数为 2 的幂则可以进行x>>n
的操作,注意,这里的右移是算数右移。
浮点数表示
使用浮点数的表示方式可以让计算机以损失一定精度的代价来表示更大范围的数字,IEEE(I triple E,电气电子工程师学会)浮点标准用$V = (-1)^s \times M \times 2^E$来表示一个数:
- 符号:s 为 1 时,为负数,s 为 0 时,为正数。
- 尾数:M 为二进制小数,它的范围为 [1, 2),或者是 [0, 1)。
- 阶码:E 为浮点数加权,这个权重是 2 的 E 次幂。
举个例子:12345.0
的单精度存储为:
符号(31:31) | 阶码(30:23) | 尾数(22:0) |
---|---|---|
0 | 10001100 | 10000001110010000000000 |
符号 1 位,阶码 8 位,位数 23 位
12345.0
的双精度存储为:
符号(63:63) | 阶码(62:52) | 尾数(51:0) |
---|---|---|
0 | 10000001100 | 1000000111001000000000000000000000000000000000000000 |
符号 1 位,阶码 11 位,位数 52 位
这个浮点数根据以下规则转换成 10 进制值:
- 符号位为 0 为正数,为 1 为负数。
- 根据阶码的情况分为三种:
- 阶码全为 0,此时尾数为非规格化的,其余情况尾数则是规格化的,E 为 1 减去偏置量$2^{k-1}-1$,其中 k 为阶码位数,2 的 E 次幂为这个数的权重。
- 阶码全为 1,此时尾数必须全为 0,浮点数表示为$\infty$,若尾数不为 0,则为特殊值
NaN
(Not a Number)。 - 除了上述两种情况,其余情况 E 为阶码为减去偏置量$2^{k-1}-1$,2 的 E 次幂为这个数的权重。
- 根据尾数是否为规格化的分为两种情况:
- 规格化的,在最前面补 1,尾数补全后为 1.xxxx,补全后的尾数记作 M。
- 非规格化的,在最前面补 0,尾数补全后为 0.xxxx,补全后的尾数记作 M。
- 整个浮点数表示的 10 进制为$V = (-1)^s \times M \times 2^E$。
现在给出一个示例:
假设一个基于 IEEE 浮点格式的 5 位数,有 1 个符号位,2 个阶码位,2 个尾数位,以下二进制数表示的十进制值是多少:
偏置量为$2^{2-1}-1=1$
- 00000
- 00010
- 00100
- 00110
- 01000
- 01011
- 01100
- 01101
浮点数的舍入
由于浮点数的表示方式导致的精度和范围,我们只能使用近似值来进行实数运算,所以 IEEE 指定了一个向偶数舍入的舍入规则来用浮点数能表示的数来近似代表对应实数。
这个向偶数舍入的规则与四舍五入有些相似,但是它规定当需要舍入为两个最低有效数字的平均值时,则向偶数舍入使得最低有效数字为偶数,举几个例子:
假设最低有效数字为小数点后两位
- 1.454 -> 1.45
- 1.456 -> 1.46
- 1.455 -> 1.46
- 1.465 -> 1.46
- 1.995 -> 2.00
对于二进制数也相似,而且二进制的向偶数舍入更简单,毕竟二进制只有 0 和 1,0 为偶数,1 为奇数,举几个例子:
假设最低有效数字为小数点后两位
- 11.100 -> 11.10
- 11.101 -> 11.10
- 11.110 -> 11.11
- 11.111 -> 100.00
浮点数的运算
由于精度和范围,浮点数计算时有可能会发生舍入和溢出,比如在单精度情况下,$(1e20 \times 1e20) \times 1e-20 = +\infty$,而$1e20 \times (1e20 \times 1e-20) = 1e20$,这是因为在计算$1e20 \times 1e20$时,发生了溢出,再举个例子,比如在单精度情况下,$1e20 \times (1e20-1e20) = 0.0$,而$1e20 \times 1e20-1e20 \times 1e20=NaN$ (Inf-Inf=NaN)。在类型转换时,int 转 float 会发生舍入,而 int 转 double 就不会。