信息的表示和处理

前言
当某些计算结果太大而无法表示时,就会发生溢出。
整数的计算机运算满足人们所熟知的真正整数运算的许多性质。
信息存储
大多数计算机使用8位的块,或字节作为内存可寻址的最小单位,而不是通过访问单独的位来实现。对于字节和比特的关系,有:1字节(byte)=8位(bit)。机器级程序把内存视为超大数组来进行寻址处理。
程序对象存放在存储器里,程序对象一般包括程序数据,指令和控制信息。C语言中的一个指针的值,是某个存储块的第一个字节的虚拟地址。可以把程序对象视为一个或多个字节块,而程序本身就是一个字节序列。
十六进制表示法
为了方便记录,程序有时使用十六进制的表示法,在C语言中,以0x或0X开头的常量一般都是十六进制值。十六进制位0-9,A-F;对于字母部分,并不区分大小写。如果对于一个数据,它的位数是4的倍数,那么可以很容易把二进制形式的表示转换成十六进制的表示。
这里有一个对照表,可以供参考:

字数据大小
每台计算机都有一个字长,用于指明指针数据的标称大小,它也代表着系统虚拟地址最大大小。对于一个字长位w位的机器而言,虚拟地址的范围是0~2w−1,程序最多访问2w个字节。
寻址和字节顺序
多字节对象被存储为连续的字节序列,对象的地址是所使用的字节中最小的地址,也就是首地址。
对于一个w位的整数来说,其位表示为 [xw−1,xw−2,⋯,x1,x0] ,其中xw−1为最高有效位,而x0为最低有效位。假设w是8的倍数,那么就可以把这些位分组成一个个字节,[xw−1,xw−2,⋯,xw−8]为最高有效字节,[x7,x6,⋯,x0]为最低有效字节。
某些机器选择在内存中按照从最低有效位到最高有效位的顺序存储对象,这叫大端法,反之为小端法。假设变量x的类型是int十六进制为0x01234567,起始地址是0x100,那么大端小端表示方法分别为:

多数Intel兼容机使用小端法,iOS和Android也是用小端法。
关于大小端的区别,还是有的,比方说在网络传输方面,如果发送者和接收者不一致,就会发生倒序现象;第二种情况在于阅读字节序列时,最后一种则是在编写规避正常的类型系统的程序时。
表示字符串
文本数据相较于二进制数据,有更强的平台独立性。
表示代码
计算机系统的一个基本概念是,从机器的角度来看,程序仅仅是字节序列。
布尔代数简介
布尔运算有四种运算,非(~),且(&),或(|),异或(^)。它们所定义的运算是这样的:

而对于两个位向量的运算可以定义成它们的每个对应元素之间的运算。见C语言中的位级运算。
位向量的一个很有用的应用就是表示有限集合,可以使用位向量编码其任何子集,对于位向量[aw−1,aw−2,⋯,a1,a0],我们假设有一个集合A ⊆{0,1,⋯,w−1},其中ai=1当且仅当i∈A。当a=⋅[01101001]。时,有A ={0,3,5}
C语言中的位级运算
C语言的一个很有用的特性就是它支持按位进行布尔运算。直接看一个例子好了:

C语言中的逻辑运算
没什么好说的,就是表达式求值。
C语言中的移位运算
有两种移位方式,左移和右移,右移又分为算术右移和逻辑右移。
左移:对于一个操作数x,左移k位意味着把x向左移k位,丢弃最高的k位,并在最右端(最低有效位方向)补0。如x=[xw−1,xw−2,⋯,x1,x0],此时x << k结果为
x=[xw−k−1,xw−k−2,⋯,x0,k0,⋯,0]
所述。
来看看右移中的逻辑右移,它仅仅是在左端补0,而算术右移补最高有效位的值。对于操作x >> k的逻辑右移结果为
x=[k0,⋯,0,xw−1,xw−2,⋯,xk]
,而对于算术右移的结果为
x=[kxw−1,⋯,xw−1,xw−2,⋯,xk]
。
对于所有的有符号数,几乎所有的编译器/机器都会使用算术右移;而对于无符号数,右移是逻辑右移。
对于k,如果k很大,则取k=k mod w。
整数表示
进行之前,请允许我在这里定义一些函数:
其中w是数据位数。比如32位/64位。
整数数据类型
C语言支持很多整型数据类型,来看一下:
这是32位机的
这是64位机的
有一个很值得注意的地方就是,有符号数的取值范围不是对称的。负数要多一个。为此C语言定义了一个每种类型必须满足的取值范围:

除了固定长度的类型外,有符号数的负数和整数对称了。
无符号数编码
假设有一个整数数据类型有w位,可以将位向量写成x,或者写成[xw−1,xw−2,⋯,x0]。
把x看成一个二进制表示的数,定义一个函数B2Uw表示把位长为w的二进制数转换成无符号数。那么就可以得到如下表述:
∵无符号数编码的定义
∴对于向量x=[xw−1,xw−2,⋯,x0],有
B2Uw(x)=⋅∑i=0w−1xi2i
来定义一下无符号数的最值。
UMaxw=⋅∑i=0w−12i=2w−1
UMinw=⋅0

二进制和无符号数之间的转换是一一对应的,反之亦然。
∵无符号数编码的唯一性
∴函数B2Uw是一个双射
或者可以这么说:x=U2Bw(B2Uw(x))。
补码编码(有符号数编码)
补码(Two's-Complement)编码用来表示有符号数,在补码编码里,将最高有效位解释成负权。
∵补码编码的定义
∴对于向量x=[xw−1,xw−2,⋯,x0],有
B2Tw(x)=⋅−xw−12w−1+∑i=0w−2xi2i
最高有效位xw−1也称为符号位,它的权重为−2w−1。

来看看补码定义的极值:
TMaxw=⋅∑i=0w−22i=2w−1−1
TMinw=⋅−2w−1
同时补码转换函数也是一个双射,原理是因为补码编码的唯一性。
x=T2Bw(B2Tw(x))
来看一个比较重要的数值对比:

除了补码表示有符号数,还有两种方式,不过这两种方式表示0的方法不唯一。
反码(Ones' Complement):除了最高有效位的权是−(2w−1−1)而不是−2w−1之外,其他的和补码一致。
B2Ow()x=⋅−xw−1(2w−1−1)+∑i=0w−2xi2i
原码:最高有效位是符号位,用来确定剩下的位应该取负权还是正权。
B2Sw(x)=⋅(−1)xw−1(∑i=0w−2xi2i)
有符号数和无符号数之间的转换
强制类型转换的结果保持位值不变,只是改变了解释这些位的方式。
来看看无符号数和有符号数之间的转换。
∵补码转换成无符号数
∴对满足TMinw≤x≤TMaxw的x有:
T2Uw(x)={x+2wxx<0x≥0
这是根据它们的定义得到的,好好想想补码和无符号数是怎么解释x的?!有一个,提一下,就是−2w−1+2w=2w−1。
上述公式还可以化简,得到如下形式:
B2Uw(T2Bw(x))=T2Uw(x)=x+xw−12w

当一个有符号数映射为它对应的无符号数时,负数就被转换成了大的正数,而非负数保持不变。
反过来看,如何把无符号数转换成补码。
∵无符号数转换成补码
∴对满足0≤u≤UMaxw的u有:
U2Tw(u)={uu−2wu≤TMaxwu>TMaxw
上述代码可以化简如下:
U2Tw(u)=−uw−12w+u
C语言中的有符号数和无符号数
C语言允许进行无符号数和补码之间的转换,虽然C语言没有明确地指明如何进行这种转换,但是一般的原则都是,保持底层的位不变而仅仅更改解释方法。
对于一个运算,如果含有无符号数和补码,那么会全部转换成无符号数来处理。来看一些例子来理解:

非直观的情况标注了*。
扩展一个数字的位表示
想要把一个无符号数扩展成更大的位,仅仅简单的在前面加0即可。
∵无符号数的零扩展
∴定义宽度位w的位向量u=[uw−1,uw−2,⋯,u0]
和宽度位w′的位向量u′=[0,0,⋯,0,uw−1,uw−2,⋯,u0],其中w′>w。
则有B2Uw(u)=B2Uw′(u′)
要将一个补码数字转换为一个更大的数据,可以执行符号扩展,在表示中添加最高有效位的值,这有点像算术右移。
∵补码数的符号扩展
∴定义长度为w的位向量x=[xw−1,xw−2,⋯,x0]
和宽度为w′的位向量x′=[xw−1,⋯,xw−1,xw−2,x0],其中w′>w。
则B2Tw(x)=B2Tw′(x′)
来证明一个东西,首先令w′=w+k,试证明
B2Tw+k([kxw−1,⋯,xw−1,xw−1,xw−2,⋯,x0])=B2Tw([xw−1,xw−2,⋯,x0])
在这里想要证明这个,使用关键属性−2w−1+2w=2w−1
贴上递归证明过程:

截断数字
当把一个w位的数x=[xw−1,xw−2,⋯,x0]截断为一个k位数字时,我们会丢弃高w−k位,得到一个新的位向量x′=[xk−1,xk−2,⋯,x0]。
对于无符号数,可以很容易得到其结果:
∵截断无符号数
∴令x=[xw−1,xw−2,⋯,x0],而x′是将其截断为k位的结果
x′=[xk−1,xk−2,⋯,x0]。令x=B2Uw(x),x′=B2Uk(x′)。
则x′=x mod 2k。
来看一个解释:

在这里使用了属性对于任意的i≥k,2i mod 2k=0。
补码截断也有类似的属性,只不过需要把最高位转换成符号位。
∵截断补码数值
∴令x=[xw−1,xw−2,⋯,x0],而x′是将其截断为k位的结果
x′=[xk−1,xk−2,⋯,x0]。令x=B2Uw(x),x′=B2Tk(x′)。
则x′=U2Tk(x mod 2k)。
来看一个化简:
B2Uw([xw−1,xw−2,⋯,x0]) mod 2k=B2Uk([xk−1,xk−2,⋯,x0])
也就是x mod 2k可以被表示为[xk−1,xk−2,⋯,x0]的无符号数表示。将其转换成补码数有x′=U2Tk(x mod 2k)。
总而言之,无符号数阶段结果是:
B2Uk([xk−1,xk−2,⋯,x0])=B2Uw([xw−1,xw−2,⋯,x0]) mod 2k
而补码数字的截断结果是:
B2Tk([xk−1,xk−2,⋯,x0])=U2Tk(B2Uw([xw−1,xw−2,⋯,x0]) mod 2k)
有符号数和无符号数的建议
整数运算
无符号加法
对于两个无符号数x和y,满足0≤x,y≤2w−1。它们都是w位的数。然而它们的和的范围是0≤x+y≤2w+1−2。所以可能会需要w+1位来存储。如果结果的w位(因为从0开始,所以其实是第w+1位)非0,那么可能要舍弃,这就发生了溢出。
在此,来定义无符号数加法:+wu
∵无符号数加法
∴对满足0≤x,y≤2w−1的x,y有:
x+wuy={x+y,x+y−2w,x+y≤2w−12w≤x+y≤2w+1−2正常溢出
一般而言,如果x+y<2w,和的第w+1位会是0,所以即使丢弃也不会影响;另一方面,如果2w≤x+y<2w+1,和的w+1等于1,因此丢弃它就像减去了2w一样。
说一个算数运算溢出,是指完整的整数结果没法放到数据类型的字长限制中去。

对于溢出,需要想办法把它检测到,C语言程序并不会发出溢出信号,因此需要自己检测。
∵检测无符号数加法中的溢出
∴对在范围0≤x,y≤UMaxw中的x,y,令s=⋅x+wuy。
则对计算s,当且仅当s<x(或s<y)时,计算发生了溢出。
对于减法运算,可以运用阿贝尔群来求得被减数的逆元,进而完成运算。
∵无符号数求反
∴对满足0≤x≤2w−1的任意x,其w位的无符号逆元−wux可由下式给出:
−wux={x,2w−x,x=0x>0
减去一个数等于加上这个数的逆元。
有符号加法(补码加法)
对于补码加法,必须确定当结果太大或结果太小时,应该做些什么。
对于x,y,有−2w−1≤x,y≤2w−1−1。则它们的和的范围可能是−2w≤x+y≤2w−2之内。想要结果精确,可能需要w+1位。就像以前一样,依旧得截断为w位。在此,定义x+wty为整数x+y被截断为w位的结果,并将这个结果看成补码数。
∵补码加法
∴对满足−2w−1≤x,y≤2w−1−1的整数x和y,有:
x+wty=⎩⎨⎧x+y−2w,x+y,x+y+2w,2w−1≤x+y−2w−1≤x+y<2w−1x+y<−2w−1正溢出正常负溢出
在这里解释一下,首先看正溢出,为什么要−2w呢?因为这个范围内的计算会造成最高有效位为1(此时位长w+1),所以真实的结果是右w位加起来−2w,不过由于截断刚好只能得到右w位的值,此时减去2w刚好是答案,所以就像保持了原本的结果一样,一切刚刚好!负溢出同理。

case1对应0−2w−1;case4对应−2w−1−0。
来看一个推论吧!
x+wty=⋅U2Tw(T2Uw(x)+wuT2Uw(y))
然后还可以化简一下,对于T2Uw(x)换成xw−12w+x,把T2Uw(y)换成yw−12w+y。然后就能得到:

要知道xw−12w和yw−12w这两项,模2w都是0.
此时,是时候看看怎么检测补码的溢出了。
∵检测补码加法中的溢出
对满足TMinw≤x, y≤TMaxw的x和y,令s=⋅x+wty。
当且仅当x>0, y>0,但s≤0时,计算s发生了正溢出。
当且仅当x<0, y<0,但s≥0时,计算s发生了负溢出。
补码的非
对于补码,也可以定义其逆元来完成减法运算。
∵补码的非
对满足TMinw≤x≤TMaxw的x,其补码的非−wtx由下面的式子给出:
−wtx={TMinw,−x,x=TMinwx>TMinw
对于补码非得位级表示,其实还有别的方法,第一种方法是各位取反,末尾+1,第二种是从右向左找到第一个1得位置,对这个1左边所有的位取反。
第一种
第二种
无符号乘法
对于两个无符号数x, y,它们的范围是0≤x, y≤2w−1。那么它们的乘积的范围可以是0−(2w−1)2。这可能需要2w位来表示。所以还得截断就是了。定义乘积位x∗wuy。
将一个无符号数截断到w位相当于模2w。
∵无符号数乘法
对满足0≤x, y≤UMaxw的x, y有:
x∗wuy=(x⋅y) mod 2w
补码乘法(有符号乘法)
对于两个补码数x, y,它们的范围是−2w−1≤x, y≤2w−1−1,则它们的乘积的范围是−2w−1⋅(2w−1−1)至22w−2之间。当然也是需要截断为w位的。把一个补码数截断为w位相当于先计算该值模2w,再把无符号数转换成补码数。
∵补码乘法
对满足TMinw≤x, y≤TMaxw的x, y有:
x∗wty=U2Tw((x⋅y) mod 2w)
对于无符号数和补码数来说,乘法运算的位级表示都是一样的,所以可以使用无符号数的乘法处理有符号数的乘法,具体细节如下:
∵无符号数乘法和补码乘法的位级等价性。
给定长度为的w的位向量x,y。
用补码形式定义整数x,y,其中x=B2Tw(x),y=B2Tw(y)。
用无符号形式定义整数x′,y′,则x′=B2Uw(x),y′=B2Uw(y)。
则有T2Bw(x∗wty)=U2Bw(x′∗wuy′)
对于无符号数和补码,它们在进行乘法运算之后,再截断,位级表示都是一样的!
现在来推导一下无符号数和补码数的乘法的位级等价性。
因为有x′=x+xw−12w和y′=y+yw−12w,所以有:

由于模运算,所以可以化简得此。再加上x∗wty=U2Tw((x⋅y) mod 2w)。对两边同时应用T2Uw,有:
T2U(x∗wty)=T2Uw(U2Tw((x⋅y) mod 2w))=(x⋅y) mod 2w
代入(x′⋅y′) mod 2w=(x⋅y) mod 2w得:
U2Bw(T2Uw(x∗wty))=T2B(x∗wty)=U2Bw(x′∗wuy′)
乘以常数(乘法优化)
对于乘法,往往需要更多的时钟周期,而加法,减法,移位等只需要一个时钟周期,因此试着把常数乘法拆解为加法移位等组合运算会快一些。
在C语言中,左移k位相当于乘了2k。所以对于一个常数,可以考虑把它分解成多个2的幂相加的形式。
除以2的幂
除法相对于乘法则更慢,需要其三倍的时间甚至更多。所以对于除法也可以考虑进行优化操作。
无符号数和补码数可以分别通过逻辑移位和算术移位来达到目的。
整数除法总是舍入到0,为此,定义一些操作,⌈a⌉是向上取整,使得存在b使得b−1<a≤b比如⌈3.14⌉=4; ⌈−3.14⌉=3;而⌊a⌋是向下取整,使得存在b使b≤a<b+1,比如⌊3.14⌋=3; ⌊−3.14⌋=−4。
对于无符号数的右移,是比较简单的,因为它仅涉及到逻辑右移。
∵除以2的幂的无符号数除法
∴C语言变量x和k分别有无符号数值x和k,且0≤k<w。
则C语言表达式x>>k产生数值⌊x/2k⌋。
对于补码数的操作可能稍微复杂。当补码数位非负数时,操作和无符号数一样,但是当补码数为负数时,需要进行偏置并向上取整。
C语言变量x和k分别有补码数值和无符号数值x和k,且0≤k<w,x<0。
则当执行算数移位时,C语言表达式(x+(1<<k)−1)>>k产生数值⌊x/2k⌋
偏置技术使用了这个原理:对于整数x和y(y>0),⌈x/y⌉=⌊(x+y−1)/y⌋
于是在使用算术右移的补码机器,C语言表达式(x<0 x+(1<<k)-1 : x) >> k会计算数值x/2^k$。C语言默认向下舍入。
浮点数
二进制小数
考虑一个形如bmbm−1⋯b1b0.b−1b−2⋯b−n+1b−n的表示法,其中每个二进制数字,或者称为位,bi的取值范围是0和1。这种表示方法表示的数b定义如下:
b=∑i=−nm2i×bi

二进制小数小数点左移一位相当于除2,右移一位相当于乘2。注意,像0.1111111111⋯1112这样的小数是刚好小于1的数。
小数的二进制法只能表示那些能写成x×2y形式的数。增长二进制表示的长度可以提高表示的精度。
IEEE浮点数表示
IEEE浮点数标准使用V=(−1)s×M×2E来表示一个数
来看看它的每位的意义:
1. 符号位(s)决定这个数是正数(0)还是负数(1)
2. 尾数(M)是一个二进制小数,它的范围是1-(2-x)或0-(1-x)
3. 阶码(E)的作用是对浮点数加权,这个权重是2的E次幂(可能是负数)
将一个浮点数的位划分为三个字段,分别进行编码:
1. 一个单独的符号位
2. k长度的位,用来编码阶码
3. n长度的位,用来记录二进制小数
在C语言中,32位的float,符号位占1位, k=8, n=23,在64位的double中,符号位占1位, k=11, n=52。

对于浮点数形式,来看看一些情况包括格式化的和非格式化的。

来看看他们的情况吧!
首先是规格化的数,这是最普遍的情况,当exp的位模式既不全是0(对应的值是0)也不全是1时(对应的值是255,双精度是2047),指的就是这种情况。不过为了能表示负权值,我们可以使用偏置值来实现,也就是说阶码值是E=exp-Bias(单精度是127,双精度是1023),由此产生的偏执范围是(-126~+127),双精度是(-1022~+1023)。
小数字段frac被解释为描述小数值f,其中0≤f<1,其二进制表示为0.fn−1⋯f1f0,也就是二进制小数点在最高有效位的左边。尾数M定义为M=1+f,因此可以把M看成一个二进制表达式为1.fn−1⋯f1f0的数字。既然第一位可以通过调整E来使他为1,那么可以隐去不表,这样就又多了一个精度来记录值。
其次是非规格化的情况,当阶码域全为0时,所表示的数就是非规格化的情况,此时E=1-Bias,而尾数的值M=f,也就是小数字段的值。不包含隐含的开头的1。
非规格化值有两个用途,首先它提供了一种表示0的方法,因为规格化数M一定大于1。不过还有正负0的区别哈哈哈哈!另一个用途就是可以表示那些非常接近0.0的数,它们提供了一种属性,称为逐渐溢出,其中可能的数值分布均匀地接近于0.0。
对于特殊值,最后一类是指阶码域全为1时的情况,当小数域全为0时,得到的就是无穷,当然有正负之分咯(符号位决定)!无穷可以用来表示溢出结果,比如很大的数相乘,或者除以0时。如果小数域非0,那就是NaN,非数。
数字示例
略略略,累死了累死了!别忘了看,不写可以。
舍入
浮点数使用向偶舍入的方式来处理舍入,啥意思呢?就是如果想要舍入的值小于精度后面全为0的那个参考值的话,直接丢弃,如果大于直接+1,如果等于,舍入到偶数那里,屮没表达清楚,看例子好了:
舍入到小数点后两位:1.2349999->1.23; 1.2350000->1.24; 1.2350001->1.24; 1.245000->1.24
而在二进制里,规定0时偶数,1是计数,参考值是1000...所以假设舍入到小数点后x位,那么看x+1到末尾,如果组成是1000000000...那么久看x位是0是1,是1就在x位+1(然后该进位进位),否则为0就不动。如果是10..0010...0反正只要大于10000...就直接x位+1;小于就不动。
还是舍入到后两位,不过是二进制小数:10.00011->10.00; 10.00110->10.01; 10.11100->11.00; 10.10100->10.10。
浮点运算
整数加法有阿贝尔群,其实现实中的实数加法也有阿贝尔群,但是在处理计算机小数时,必须考虑舍入的影响。
浮点数加法不具有结合性,这是缺少的最重要的群属性。
C语言中的浮点数
啊这!好像没什么好说的,就float和double的用法。