深入理解计算机对定点数的表示与处理(基于C语言)

一直想系统的总结一下计算机关于定点数的存储与表示,能清楚直白的阐述其中的原理。个人认为这是非常重要的内容,可以算是真正的内功心法,基础中的基础,能提升对技术的理解能力。

本文重点解决两个问题:

  1. 为什么计算机对于定点数要用补码表示

  2. 怎样理解反码等于原码保持符号位不变,数据位取反;补码等于反码加一

如果你能解释以上两个问题,请飘过!如果有点儿犹豫,不妨静下心来看看;如果0基础,毋庸置疑,赶紧往下撸呀!

前面会介绍一些铺垫性的概念和表述方式,都是为了对核心内容更好的表达。

位、字节、字

​ 我们知道二进制数就只有0和1,而计算机对信息的存储就是使用二进制数字。通常,存储二值信号的单元叫做位(bit)。单个的位只能表示两种状态,没有太大的作用。但是,如果多个位组合起来,那就能表示非常丰富的含义。一般位的单位使用小写字母b表示,例如,10b表示10个bit

​ 计算机中将8个位组成一组,称作字节(byte),作为最小的可寻址的存储单元。存储器的每个字节都由一个唯一的数字来标识,这个标识称为地址。所有可能地址的集合称为虚拟地址空间。我们常说的32位机器,就是指该计算机的最小可寻址的存储单元为4字节,那么,虚拟地址空间就是[0,2^32-1],也就是4GB。一般字节的单位使用大写字母B表示,例如,10B表示10个字节。

​ 早些年的计算机大多数是32位,如今很多计算机都是64位的。“32位”,"64位"还有一个名称叫做字长(word size),字长为4表示32位机器,字长为8表示64位机器。

字节8个位的组合形成的一个单元;便是n个字节的组合形成的一个单元,n的大小一般为4或8,32位机器的字长为4个字节,64位机器的字长为8字节。

位、字节、字之间的大小关系是:word > byte > bit。

由于字大小不同,程序的平台移植性需要考虑。

进制

10进制是我们在日常生活中使用了一千多年的数字表示方法,某种角度来说,10进制也是一种"编码"方式。以10为基数,逢10进1。

2进制和16进制是在计算机的世界里,常用的数字"编码"方式。

10进制16进制2进制
00x000000 0000
10x010000 0001
20x020000 0010
30x030000 0011
40x040000 0100
50x050000 0101
60x060000 0110
70x070000 0111
80x080000 1000
90x090000 1001
100x0A0000 1010
110x0B0000 1011
120x0C0000 1100
130x0D0000 1101
140x0E0000 1110
150x0F0000 1111

从上表可以看出,数字0-15有三种不同的"编码"方式。这里之所以反复强调"编码"这个概念,是为了后面的内容做准备。要表示16个数字,若以10进制为度量,则为0-15;如果16进制为度量,则为0-F;若以2进制为度量,则为0000-1111;三种不同的表示形式,但表达的内容都是相同的。

大小端字节序

大小端问题其实可以看做快递存取问题。从哪儿开始存,怎么存?从哪儿开始取,怎么取?

假设有一个空的快递柜,每一层有4个柜子,按照常识,每个柜子都会有一个编号,假设第一层的4柜子的编号依次为0x100-0x103,现在,我们来玩一个游戏,将4种水果放进柜子里面,按照依次存取方式,保证4种水果的摆放顺序不变。每个水果也有编号,苹果(1号)、香蕉(2号)、草莓(3号)、葡萄(4号)

存之前4种水果的摆放顺序是:苹果(1) 香蕉(2) 草莓(3) 葡萄(4)

方法一(类似 大端)

存入:

0x100 0x101 0x102 0x103

苹果(1) 香蕉(2) 草莓(3) 葡萄(4)

取出(从编号小柜子先取):1 2 3 4

方法二(类似 小端)

存入:

0x100 0x101 0x102 0x103

葡萄(4) 草莓(3) 香蕉(2) 苹果(1)

取出(从编号大柜子先取):1 2 3 4

在计算机中,对于占用多字节的对象,同样也要考虑,这个对象的地址在哪儿,该对象的内容按照什么顺序排列在多字节的space中。

前面举了一个实际生活中的栗子,下面再举一个计算机世界的例子:

对于16进制数0x01234567,该数占4个字节,那么以小端序该数在计算机中怎样存储?以大端序该数在计算机中怎样存储?

大端(big endian):(把数值的高位字节放在内存的低位地址上,把数值的低位字节放在内存的高位地址上)

地址0x1000x1010x1020x103
数值0x010x230x450x67

小端(little endian):(把数值的高位字节放在高位地址上,低位字节放在低位地址上)

地址0x1000x1010x1020x103
数值0x670x450x230x01

总之,小端序是数值高位对应地址高位,数值低位对应地址低位;而大端序则数值高位对应地址低位,数值低位对应地址高位。小端序在阅读时,通常是将数值字节按照(与我们书写)相反的顺序显示。

书写字节序列的自然方式是最低位字节在左边,而最高位字节在右边,这正好和通常书写数字时最高有效位在左边,最低有效位在右边的方式相反。

由于字节序的不同,要保证读取和写入的字节序相同,发送和接收的字节序相同。

因此,由于字节序和字大小平台差异性,所以二进制数据的可移植性较文本数据弱,需要两端统一。

位向量

位向量只是一种数学上的表示方式,也是为了后文内容做铺垫。所谓位向量就是固定位宽为w,由0和1组成的串。位向量的运算可以定义成向量的每个对应元素之间的运算。而对应元素之间的运算也就是位运算。

x ⃗ = [ x w − 1 , x w − 2 , . . . , x 2 , x 1 , x 0 ] \vec{x}=[x_{w-1}, x_{w-2}, ..., x_2, x_1, x_0] x =[xw1,xw2,...,x2,x1,x0]

假设w=4, 向量 a ⃗ \vec{a} a =[0110],向量 b ⃗ = [ 1100 ] \vec{b}=[1100] b =[1100],那么,a&b = [0100]。

移位运算

在c语言中,对于一个以位模式表示的数x= [ x w − 1 , x w − 2 , . . . , x 2 , x 1 , x 0 ] [x_{w-1}, x_{w-2},...,x_2,x_1,x_0] [xw1,xw2,...,x2,x1,x0]

左移运算,x << k 即为 [ x w − k − 1 , x w − k − 2 , . . . , x 0 , 0 , . . . , 0 ] [x_{w-k-1}, x_{w-k-2}, ...,x_0, 0, ...,0] [xwk1,xwk2,...,x0,0,...,0],从左到右,丢弃高k位,低位补k个0;

右移运算,x >> k,涉及两个概念逻辑右移和算术右移。

逻辑右移就是高位补0,算术右移就是高位补符号位。

对于无符号数,x >> k 即为 [ 0 , . . . , 0 , x w − 1 , x w − 2 , . . . , x k ] [0, ..., 0, x_{w-1}, x_{w-2}, ..., x_k] [0,...,0,xw1,xw2,...,xk]

对于有符号数,x >> k 即为 [ x w − 1 , . . . , x w − 1 , x w − 1 , x w − 2 , . . . , x k ] [x_{w-1}, ..., x_{w-1}, x_{w-1}, x_{w-2}, ..., x_k] [xw1,...,xw1,xw1,xw2,...,xk]

在c标准中并未明确定义使用哪种右移运算。但是,几乎所有的编译器都会对有符号数使用算术右移。当然,有些语言(如,java)对逻辑移位和算术移位做了明确的定义和区分。

整数表示

在c语言中支持多种整型数据类型,不同数据类型所表示的取值范围也各不相同,但是其背后的规律值得我们探索。

32位机器的c的整型数据类型的取值范围:

数据类型最小值最大值
char-128127
unsigned char0255
short [int]-3276832767
unsigned short [int]065535
int-2,147,483,6482,147,483,647
unsigned [int]04,294,967,295
long [int]-2,147,483,6482,147,483,647
unsigned long [int]04,294,967,295
long long [int]-9,223,372,036,854,775,8089,223,372,036,854,775,807
unsigned long long [int]018,446,744,073,709,551,615

long long(8字节)是在ISO C99标准引入的,所以在此之前的标准中都是占4个字节。

64位机器的c的整型数据类型的取值范围:

数据类型最小值最大值
char-128127
unsigned char0255
short [int]-3276832767
unsigned short [int]065535
int-2,147,483,6482,147,483,647
unsigned [int]04,294,967,295
long [int]-9,223,372,036,854,775,8089,223,372,036,854,775,807
unsigned long [int]018,446,744,073,709,551,615
long long [int]-9,223,372,036,854,775,8089,223,372,036,854,775,807
unsigned long long [int]018,446,744,073,709,551,615

在64位机器中,long数据类型占8字节。

观察上述两个表格的取值范围,发现最小值和最大值的绝对值不对称,负数的绝对值比整数的绝对值大1。这是一个有意思的现象,后文在讲解补码时会解释缘由。

无符号整数编码

前面讲过位向量的概念,这里就是用位向量来引出无符号数的数学表示公式。

假设一个无符号整数x,有w位,

位向量表示法为: x ⃗ = [ x w − 1 , x w − 2 , . . . , x 2 , x 1 , x 0 ] \vec{x}=[x_{w-1}, x_{w-2}, ..., x_2, x_1, x_0] x =[xw1,xw2,...,x2,x1,x0]。为了后文方便表述,会将这种表示方式称为位模式

函数表示法为: B 2 U w ( x ⃗ ) = ∑ i = 0 w − 1 x i 2 i B2U_w(\vec{x})=\sum_{i=0}^{w-1}x_i2^i B2Uw(x )=i=0w1xi2i,该函数将一个长度为w的0、1串映射到非负整数。

B2U:Binary to Unsigned

B 2 U w ( [ 0001 ] ) = 0 3 × 2 3 + 0 2 × 2 2 + 0 1 × 2 1 + 1 0 × 2 0 = 0 + 0 + 0 + 1 = 1 B2U_w([0001])=0_3\times2^3+ 0_2\times2^2+0_1\times2^1+1_0\times2^0=0+0+0+1=1 B2Uw([0001])=03×23+02×22+01×21+10×20=0+0+0+1=1

B 2 U w ( [ 1001 ] ) = 1 3 × 2 3 + 0 2 × 2 2 + 0 1 × 2 1 + 1 0 × 2 0 = 8 + 0 + 0 + 1 = 9 B2U_w([1001])=1_3\times2^3+ 0_2\times2^2+0_1\times2^1+1_0\times2^0=8+0+0+1=9 B2Uw([1001])=13×23+02×22+01×21+10×20=8+0+0+1=9

B 2 U w ( [ 1111 ] ) = 1 3 × 2 3 + 1 2 × 2 2 + 1 1 × 2 1 + 1 0 × 2 0 = 8 + 4 + 2 + 1 = 15 B2U_w([1111])=1_3\times2^3+ 1_2\times2^2+1_1\times2^1+1_0\times2^0=8+4+2+1=15 B2Uw([1111])=13×23+12×22+11×21+10×20=8+4+2+1=15

那么, B 2 U w ( x ⃗ ) B2U_w(\vec{x}) B2Uw(x )所能表示的数值范围是[0, 2 w − 1 2^w-1 2w1];

[0…0]表示最小值0,

[1…1]表示最大值 ∑ i = 0 w − 1 2 i = 2 w − 1 + 2 w − 2 + . . . + 2 3 + 2 2 + 2 1 + 2 0 = 2 w − 1 \sum_{i=0}^{w-1}2^i=2^{w-1}+2^{w-2}+...+2^3+2^2+2^1+2^0=2^w-1 i=0w12i=2w1+2w2+...+23+22+21+20=2w1

显然,无符号数的二进制表示有个很重要的属性, B 2 U B2U B2U是一个双射;即对于每一个长度为w的位向量二进制表示都有一个唯一的值与之对应;反过来, [ 0 , 2 w − 1 ] [0,2^w-1] [0,2w1]之间的每一个值都有一个唯一的长度为w的位向量二进制表示与之对应。

B2U只能表示非负数值,若要表示负数,还需要其他的编码方式。

补码编码

当代计算机中有符号数的计算机表示方式就是补码(Two’s complement)编码。

补码将字的最高有效位作为负权(negative weight), 假设一个有符号整数x,位宽为w,

位向量表示法为: x ⃗ = [ x w − 1 , x w − 2 , . . . , x 2 , x 1 , x 0 ] \vec{x}=[x_{w-1}, x_{w-2}, ..., x_2, x_1, x_0] x =[xw1,xw2,...,x2,x1,x0]。可见,补码编码的位模式和无符号整数编码的位模式相同。

函数表示法为: B 2 T w ( x ⃗ ) = − x w − 1 2 w − 1 + ∑ i = 0 w − 2 x i 2 i B2T_w(\vec x) = -x_{w-1}2^{w-1} + \sum_{i=0}^{w-2}x_i2^i B2Tw(x )=xw12w1+i=0w2xi2i

最高有效位 x w − 1 x_{w-1} xw1表示符号位,它的权重为 − 2 w − 1 -2^{w-1} 2w1

当符号位为1时,表示负数,而负数的最大值是 B 2 T w ( x ⃗ ) = − 2 w − 1 + 2 w − 1 − 1 = − 1 B2T_w(\vec x)=-2^{w-1} + 2^{w-1}-1 = -1 B2Tw(x )=2w1+2w11=1,负数的最小值是 B 2 T w ( x ⃗ ) = − 2 w − 1 B2T_w(\vec x) = -2^{w-1} B2Tw(x )=2w1;

当符号位为0时,表示非负数,而非负数的最大值是 B 2 T w ( x ⃗ ) = 2 w − 1 − 1 B2T_w(\vec x) = 2^{w-1}-1 B2Tw(x )=2w11,非负数的最小值是 B 2 T w ( x ⃗ ) = 0 B2T_w(\vec x)= 0 B2Tw(x )=0

同样 B 2 T w B2T_w B2Tw也是一个双射,每一个长度为w的位模式都有一个唯一的值与之对应; [ − 2 w − 1 , 2 w − 1 − 1 ] [-2^{w-1},2^{w-1}-1] [2w1,2w11]之间的每一个值也都有唯一的长度为w的位模式表示。

通过上述一般表达式的推导,我们可以从两个角度来解释前面引出的一个问题:“整数的最小值和最大值的绝对值不对称”。

在数学层面上,补码的最小值 T M i n = − 2 w − 1 TMin=-2^{w-1} TMin=2w1,补码的最大值为 T M a x = 2 w − 1 − 1 TMax=2^{w-1}-1 TMax=2w11;也就是说补码的取值范围是 [ − 2 w − 1 , 2 w − 1 − 1 ] [-2^{w-1},2^{w-1}-1] [2w1,2w11] ∣ T M i n ∣ = ∣ T M a x ∣ + 1 |TMin|=|TMax|+1 TMin=TMax+1,不对称性显而易见;

在定性的层面上来分析,也能解释的通,因为有一半的位模式(符号位为1时)表示负数,另一半的位模式(符号位为0时)表示非负数,由于0是非负数(占了一个"坑"),所以能表示的整数就会比负数少一个。

反过来也证明了,整数在计算机中的表示是使用补码编码的方式。

如果你觉得上面的公式还不够直观,下面再展示一张表:

在这里插入图片描述

行文至此,又产生一个问题,为什么计算机表示整数要使用补码而不是使用其他编码方式,如,原码编码、反码编码呢?

原码编码

原码(Sign Magnitude)编码是 将最高有效位仅作为符号位(没有权重),其他位作为数据位

位向量表示法为: x ⃗ = [ x w − 1 , x w − 2 , . . . , x 2 , x 1 , x 0 ] \vec{x}=[x_{w-1}, x_{w-2}, ..., x_2, x_1, x_0] x =[xw1,xw2,...,x2,x1,x0]

函数表示为: B 2 S w ( x ⃗ ) = ( − 1 ) x w − 1 ⋅ ( ∑ i = 0 i = w − 2 x i 2 i ) B2S_w(\vec x)=(-1)^{x_{w-1}}\cdot\big(\sum_{i=0}^{i=w-2}x_i2^i\big) B2Sw(x )=(1)xw1(i=0i=w2xi2i)

在数学层面上,原码的最小值是 S M i n = − ( ∑ i = 0 i = w − 2 x i 2 i ) = − ( 2 w − 1 − 1 ) SMin=-\big(\sum_{i=0}^{i=w-2}x_i2^i\big)=-(2^{w-1}-1) SMin=(i=0i=w2xi2i)=(2w11),原码的最大值是 S M a x = ∑ i = 0 i = w − 2 x i 2 i = 2 w − 1 − 1 SMax=\sum_{i=0}^{i=w-2}x_i2^i=2^{w-1}-1 SMax=i=0i=w2xi2i=2w11,看起来,原码的表示方式对人来说更友好更直观更对称,但是对于计算机来说,原码的编码方式浪费了一种"状态"的表示(如果你有数电基础应该能直接get到这句话的含义,若没有也没关系,下面会解释)。为方便起见,假设位宽w=4,

位模式[0000] 代表的是数值 +0;

位模式[1000] 代表的是数值 -0;

显然,原码编码中 数值0 占用了两种编码"状态",对于计算机来说,不仅浪费而且处理起来麻烦。

反码编码

反码(Ones’ Complement)编码是 相较于原码,除符号位外,其他数据位取反。

函数表示为: B 2 O w ( x ⃗ ) = − x w − 1 ( 2 w − 1 − 1 ) + ∑ i = 0 w − 2 x i 2 i B2O_w(\vec x)=-x_{w-1}(2^{w-1}-1)+\sum_{i=0}^{w-2}x_i2^i B2Ow(x )=xw1(2w11)+i=0w2xi2i

在数学层面上,反码的最小值 O M i n = − ( 2 w − 1 − 1 ) OMin=-(2^{w-1}-1) OMin=(2w11),反码的最大值是 O M a x = 2 w − 1 − 1 OMax=2^{w-1}-1 OMax=2w11,取值范围和原码相同,也对称。但是,有个但是,同样假设位宽w=4

位模式[0000] 代表的是数值 +0;

位模式[1111] 代表的是数值 -0;

显然,补码编码中 数值0 也占用了两种编码"状态"。

总结

通常定点数(相对于浮点数)就是这三种表示方法:原码、反码、补码。

而我们常使用的口诀:反码等于原码保持符号位不变,数据位取反;补码等于反码加一(现在再来看这句结论是否豁然开朗呢!)。在原码与补码的心算转换过程中,常用反码作为过渡。

前面的内容,从数学层面解释了原码、反码、补码的原理。我相信,以后再看到关于整数的表示、原码、反码、补码相关的结论性描述,就不再迷惑了,彩!

另外,在c语言中,有时候当有符号数特别大或特别小,对于初学者处理不好可能对得不到意料之中的值;那是因为c中存在强制类型转换(显示/隐式),一般出问题是由于隐式强制类型转换导致,因为这种问题程序员不容易发现。针对这种问题,只需要记住一点即可,强制类型转换可能导致数值变化,但位模式不变

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
【问题描述】 合法的实数书写格式分一般格式和科学格式两种。分别描述如下: 一般格式为常见的书写格式,分为整数部分和小数部分两部分,中间分用小数点.分隔。整数部分最开始可能含有正号或负号,之后为不含前导零的数字串;小数部分是由0-9十种字符组成的任意长的字符串。当小数部分为0时,小数部分和小数点可以省略。 科学格式由系数部分和指数部分两部分组成,中间用英文字母E分隔。系数部分为实数书写的一般格式;指数部分为可带有正负号数字串。 例如,+2、-1.56为一般格式的实数,而6.2E-2、-9E8为科学格式的实数。 只有小数点而没有小数部分的书写格式为不合法,例如,23.,23.E16均为不合法的实数书写格式。 编程分析哪些数的书写是正确的,是用哪种方式书写的。 【输入形式】 输入文件为当前目录下的real.in。该文件包含一个字符串(长度不超过20个字符),以回车符结束,表示一个数据(无多余空格)。 【输出形式】 输出文件为当前目录下的real.out。该文件有一行。如果输入数据的书写是非法的,输出Wrong;如果输入数据是用一般格式书写的,输出“Format1”;如果该数据是用科学格式书写的,输出“Format2”。输出的末尾均要以一个回车符作为结束。 【输入样例1】 +1.23 【输出样例1】 Format1 【输入样例2】 -5.1.1 【输出样例2】 Wrong 【输入样例3】 -5.1E-2 【输出样例3】 Format2
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sif_666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值