一、信息编码与储存
现代计算机储存和处理的信息以二值信号表示,这些二进制数字,或者称为位【bit】,形成了数字革命的基础。
1.1 信息存储
大多数计算机使用8位的块,或者称为字节【byte】,作为最小的可寻址的内存单位,而不是访问内存中单独的位。一个字节由8位组成,在二进制表示法中,其值域为
[
0000000
0
2
,
1111111
1
2
]
[00000000_2,11111111_2]
[000000002,111111112],在十进制的值域为
[
0
,
255
]
[0, 255]
[0,255]。二进制表示法太冗长,而十进制表示法与位模式的互相转换很麻烦。替代的方法是,以16为基数来表示位模式,称为十六进制表示法。一般地,二进制数用后缀字母B,十六进制数用后缀字母H,形如
126
=
01111110
B
126
=
7
E
H
\begin{aligned} 126 &= 01111110B \\ 126 &= 7EH \end{aligned}
126126=01111110B=7EH
1.2 位级运算
二进制是计算机编码、存储和操作的核心,围绕二进制的研究已经演化出了丰富的数学知识体系,起源于布尔【Boolean】代数。
最简单的布尔代数是在二元集合
{
0
,
1
}
\{0, 1\}
{0,1}基础上的定义。包括与、或、非、异或运算。C支持按位布尔运算,要注意与逻辑运算的混淆。
C还提供了一组移位运算,对于操作数
x
x
x,C表达式
x
<
<
k
x<<k
x<<k会产生一个值,其丢弃最高的
k
k
k位,并在右端补
k
k
k个0。
x
>
>
k
x >> k
x>>k的行为有点微妙,机器支持两种形式的右移:逻辑右移与算数右移。逻辑右移在左端补k个0,算术右移是在左端补k个最高有效位的值。一般的,有符号数使用算数右移,无符号数使用逻辑右移。
要注意的是,如果操作数的移位的位数
k
k
k高于总位数
w
w
w,那么移位的位数为
k
m
o
d
w
k\ mod\ w
k mod w。
1.3 字数据
机器级程序将内存是为一个非常大的字节数组,称为虚拟内存。内存的每个字节都由一个唯一的数字来表示,称为地址,而指针变量便保存着地址的数值。
每台计算机都有一个字长,指明指针数据的标称大小。近年以64位字长的计算机为主流。地址指定了字节的位置,其指向了字中第一个字节的地址,相邻的地址相差8,即64位。
1.4 寻址与字节顺序
对于跨越多字节的程序对象,必须建立两个规则:这个对象的地址是什么,以及在内存中如何排列这些字节。假设一个类型为int的32位变量x的地址为0x100,那么x的4个字节将被存储在内存的0x100~0x103位置。
排列表示一个对象的字节有两个通用的规则:某些机器选择在内存中按照从最低有效字节到最高有效字节的顺序存储对象,称为小端法,如x86,ARM,iOS,Windows;反之,称为大端法,如Internet。假设变量x位于地址0x100处,其十六进制值为0x01234567,地址范围0x100~0x103的字节顺序依赖于机器的类型,形如
1.5 字符串表示
C中,字符串被编码为一个以null字符结尾的字符数组,每个字符由某个标准编码来表示,最常见的是ASCII码。在使用ASCII码作为字符码的任何系统上,字符串的结果与字节顺序与字大小规则均无关。
二、整数型表示与运算
C支持多种整型数据类型,表示有限范围的整数。C数据类型的宽度如下表
2.1 整数型表示
假如有一个整数数据类型有w位,可以写成
x
=
[
x
w
−
1
,
x
w
−
2
,
.
.
.
,
w
0
]
\bm{x} = [x_{w-1}, x_{w-2}, ..., w_0]
x=[xw−1,xw−2,...,w0]表示向量中的每一位。把
x
\bm{x}
x看做一个二进制表示的数,就获得
x
\bm{x}
x的无符号表示。用
B
2
U
w
B2U_w
B2Uw【Binary to Unsigned】函数来表示无符号数编码的定义,形如
B
2
U
w
(
x
)
=
∑
i
=
0
w
−
1
x
i
2
i
B2U_w(\bm{x}) = \sum_{i = 0}^{w - 1}x_i2^i
B2Uw(x)=i=0∑w−1xi2i在许多应用,还希望表示负数值。最常见的有符号数的计算机表示方式就是补码形式,用
B
2
T
w
B2T_w
B2Tw【Binary to Two’s-complement】来表示补码编码的定义,形如
B
2
T
w
(
x
)
=
−
x
w
−
1
2
w
−
1
+
∑
i
=
0
w
−
2
x
i
2
i
B2T_w(\bm{x}) = -x_{w-1}2^{w-1} + \sum_{i = 0}^{w - 2}x_i2^i
B2Tw(x)=−xw−12w−1+i=0∑w−2xi2i无符号数值的最小值
U
M
i
n
=
0
UMin = 0
UMin=0,最大值
U
M
a
x
=
2
w
−
1
UMax = 2^w - 1
UMax=2w−1;补码数值的最小值
T
M
i
n
=
−
2
w
−
1
TMin = -2^{w - 1}
TMin=−2w−1,最大值
T
M
a
x
=
2
w
−
1
−
1
TMax = 2^{w - 1} - 1
TMax=2w−1−1。可以观察到补码的范围是不对称的,即
∣
T
M
i
n
∣
=
T
M
a
x
+
1
|TMin| = TMax + 1
∣TMin∣=TMax+1并且最大的无符号数值刚好比补码的最大值的两倍大一点,即
U
M
a
x
=
2
∗
T
M
a
x
+
1
UMax = 2 * TMax + 1
UMax=2∗TMax+1
C标准没有要求要用补码形式来表示有符号整数,但是几乎所有的机器都是这么做的。C库中的文件<limits.h>定义了一组常量,来限定编译器运行的这台机器的不同整数数据类型的取值范围。
有符号数还有两种标准的表示方法:反码的最高有效位的权是
−
(
2
w
−
1
−
1
)
-(2^{w - 1} - 1)
−(2w−1−1),其它与补码一样;原码最高有效位是符号位,用来确定剩下的位应该取负权还是正权。
2.2 整数型转换与截断
C允许在各种不同的数字数据类型之间做强制类型转换。一个整型数据在强制类型转换后,位模式不变,而按不同编码规则重新解读得到的数值可能改变。
C的大多数数字都默认是有符号的,要创建一个无符号的常量,必须加上后缀字符U。当执行一个运算时,如果一个运算数是有符号的而另一个是无符号的,那么C会隐式的将有符号参数强制转换为无符号数,并假设这两个数都是非负的,来执行这个运算。
一个常见的运算是在不同字长的整数之间转换,同时又保持数值不变。要将一个无符号数转为一个更大的数据类型,只要简单的在表示的开头添加0。这种运算被称为零扩展,应该总是可能的。要将一个补码数字转换为一个更大的数据类型,可以执行一个符号扩展,在表示中添加最高有效位的值。
假设不用额外的位来扩展一个数值,而是减少表示一个数字的位数,称为截断。截断会丢弃高位,可能改变其值,即溢出的一种形式。
2.3 整数运算
考虑两个非负整数
x
x
x和
y
y
y,满足
0
≤
x
,
y
,
≤
2
w
0 \le x, y, \le 2^w
0≤x,y,≤2w,定义无符号加法
+
w
u
+_w^u
+wu,形如
x
+
w
u
y
=
{
x
+
y
,
x
+
y
<
2
w
x
+
y
−
2
w
,
2
w
≤
x
+
y
<
2
w
+
1
,
o
v
e
r
f
l
o
w
x+_w^u y= \left\{\begin{aligned} &x + y, &&x+y<2^w \\ &x + y - 2^w,2^w \le x + y < 2^{w + 1}, &&overflow \\ \end{aligned}\right.
x+wuy={x+y,x+y−2w,2w≤x+y<2w+1,x+y<2woverflow一个算数运算溢出,是指完整的整数运算结果不能放到数据类型的字长限制中去。再考虑两个整数
x
x
x和
y
y
y,满足
−
2
w
−
1
≤
x
,
y
,
≤
2
w
−
1
−
1
-2^{w - 1} \le x, y, \le 2^{w - 1}-1
−2w−1≤x,y,≤2w−1−1,定义补码加法
+
w
t
+_w^t
+wt,形如
x
+
w
t
y
=
{
x
+
y
−
2
w
,
2
w
−
1
≤
x
+
y
,
p
o
s
t
i
v
e
o
v
e
r
f
l
o
w
x
+
y
,
x
+
y
<
2
w
x
+
y
+
2
w
,
x
+
y
<
2
w
−
1
,
n
e
g
t
i
v
e
o
v
e
r
f
l
o
w
x+_w^t y= \left\{\begin{aligned} &x + y - 2^w,2^{w-1} \le x + y , &&postive\ overflow \\ &x + y, &&x+y<2^w \\ &x + y + 2^w, x + y < 2^{w - 1}, &&negtive\ overflow \\ \end{aligned}\right.
x+wty=⎩⎪⎨⎪⎧x+y−2w,2w−1≤x+y,x+y,x+y+2w,x+y<2w−1,postive overflowx+y<2wnegtive overflow 考虑两个非负整数
x
x
x和
y
y
y,满足
0
≤
x
,
y
,
≤
2
w
0 \le x, y, \le 2^w
0≤x,y,≤2w,它们的乘积可能需要2w位来表示。不过,C的无符号乘法被定义位产生w位的值,得到无符号乘法
∗
w
u
*_w^u
∗wu,形如
x
∗
w
u
y
=
(
x
⋅
y
)
m
o
d
2
w
x*_w^u y= (x·y)mod\ 2^w
x∗wuy=(x⋅y)mod 2w相似的,补码数的截断相当于先计算该值模
2
w
2^w
2w,再将无符号数转换为补码,考虑两个整数
x
x
x和
y
y
y,满足
−
2
w
−
1
≤
x
,
y
,
≤
2
w
−
1
−
1
-2^{w - 1} \le x, y, \le 2^{w - 1}-1
−2w−1≤x,y,≤2w−1−1,定义补码乘法
∗
w
t
*_w^t
∗wt,形如
x
∗
w
t
y
=
U
2
T
w
(
(
x
⋅
y
)
m
o
d
2
w
)
x*_w^t y= U2T_w((x·y)mod\ 2^w)
x∗wty=U2Tw((x⋅y)mod 2w) 以往,在大多数机器上,整数乘法指令相当慢,需要10个或者更多的时钟周期,然而其他整数运算(例如加法、位级运算和移位)只需要1个时钟周期。因此,编译器使用了一项重要的优化,试着使用移位和加法运算的组合来代替乘以常数因子的乘法。
三、浮点型表示与运算
3.1 二进制小数
考虑形如
b
m
b
m
−
1
.
.
.
b
1
b
0
b
−
1
.
.
.
b
−
n
b_mb_{m-1}...b_1b_0b_{-1}...b_{-n}
bmbm−1...b1b0b−1...b−n的表示法,其中
b
i
b_i
bi表示一个位,使得
f
=
∑
i
=
−
n
m
2
i
b
i
f = \sum_{i=-n}^m2^ib_i
f=i=−n∑m2ibi称为二进制小数。二进制小数点向左移动一位相当于这个数被2除,而向右移动一位相当于这个数乘2。
然而,仅考虑有线长度的编码,某些有理数不能被准确的表示。
3.2 IEEE浮点表示
IEEE浮点标准用
V
=
(
−
1
)
s
×
M
×
2
E
V = (-1)^s × M × 2^E
V=(−1)s×M×2E的形式来表示一个数:
-符号【sign】:
s
∈
{
0
,
1
}
s\in\{0, 1\}
s∈{0,1},决定了数的正负,而对于数值0的符号位解释作为特殊情况处理;
-尾数【mantissa】:
M
M
M是一个二进制小数,范围是
[
1
,
2
)
[1, 2)
[1,2)或
[
0
,
1
)
[0, 1)
[0,1);
-阶码【exponent】:
E
E
E的作用是对浮点数加权,这个权重是2的
E
E
E次幂。
将浮点数的位划分为三个字段,分别对这些值进行编码:
-一个单独的符号位
s
s
s直接编码符号
s
s
s;
-k位的阶码字段
e
x
p
=
e
k
−
1
.
.
.
e
1
e
0
exp = e_{k-1}...e_1e_0
exp=ek−1...e1e0编码阶码
E
E
E;
-n位小数字段
f
r
a
c
=
f
n
−
1
.
.
.
f
1
f
0
frac = f_{n-1}...f_1f_0
frac=fn−1...f1f0编码尾数
M
M
M,但是编码出来的值也依赖于阶码字段的值是否等于0;
在单精度浮点数<float>格式中,
s
s
s、
e
x
p
exp
exp、
f
r
a
c
frac
frac字段分别为1、8、23,得到一个32位的表示;在双精度浮点<double>格式中,
s
s
s、
e
x
p
exp
exp、
f
r
a
c
frac
frac字段分别为1、11、52,得到一个64位的表示。
根据
e
x
p
exp
exp的值,被编码的值可以分为三种不同的情况。
规范化数是最普遍的情况。当
e
x
p
exp
exp的位模式既不全为0,也不全为1时,解码字段被解释为以偏置形式表示的有符号整数,即
E
=
e
−
B
i
a
s
B
i
a
s
=
2
k
−
1
−
1
E = e - Bias \\ Bias = 2^{k - 1} - 1
E=e−BiasBias=2k−1−1而小数字段
f
r
a
c
frac
frac被解释为描述小数值
f
∈
[
0
,
1
)
f \in [0, 1)
f∈[0,1)。尾数定义为
M
=
1
+
f
M = 1 + f
M=1+f这种表示方法是一种轻松获得一个额外精度位的技巧。
非规范数的阶码域为全0,这种情况下
E
=
1
−
B
i
a
s
M
=
f
E = 1 - Bias \\ M = f
E=1−BiasM=f其有两个用途。首先提供了一种表示数值0的方法;另外一个功能时表示那些非常接近与0.0的数,提供了逐渐下溢的属性,即可能的数值分布均匀的接近于0.0。
特殊值当阶码全为1的时候出现,当小数域全为0时,得到的值表示无穷,当
s
=
0
s = 0
s=0时为
+
∞
+ \infty
+∞,或者当
s
=
1
s = 1
s=1时为
−
∞
- \infty
−∞,无穷能够表示溢出的结果。当小数域为非零时,结果值被称为
N
a
N
NaN
NaN【Not a Number】,表示不能是实数或无穷。
3.3 浮点数的舍入与运算
表示方法限制了浮点数的范围和精度,所以浮点运算只能近似的表示实数运算。对于一个值,需要一种系统的方法找到最接近的匹配值,可以用期望的浮点形式表示出来,就是舍入运算的任务。
舍入分为向上舍入、向下舍入、向零舍入与向偶舍入。其中默认的舍入方法为向偶舍入,因为其在大多数现实情况中可以避免统计偏差。
向偶舍入采用的方法是将数字向上或者向下舍入,使得结果的最低有效数字使偶数。
IEEE标准指定了一个简单的规则,来确定浮点算术运算的结果。定义 R o u n d ( x ) Round(x) Round(x)为对 x x x的舍入,那么有 x + f y = R o u n d ( x + y ) x ∗ f y = R o u n d ( x ∗ y ) x +^f y = Round(x + y) \\ x *^fy=Round(x*y) x+fy=Round(x+y)x∗fy=Round(x∗y)