0. 概要

二进制系列文章已经写到第五篇了,不出意外的话,这应该会是二进制系列的最后一篇。我们先来罗列一下前四篇:

其中,在上一篇里,我们认识了四种机器数,它们各司其职,但总的来说,有一个特点,就是在对计算机里的正负号做文章。今天介绍的定点数和浮点数,则是对小数点做文章。

上一篇文章的开头,我们说到,计算机中只能存储数字,因此需要用01来表示正负,同样的,计算机中的小数点,也要用特殊的形式来表示,共有两种,即本文所要讲的定点数浮点数

1. 定点数

所谓定点数,就是指小数点的位置是固定的,约定小数点在某一个位置上,因此,机器在处理定点数时,并不存储它的小数点。使用定点数的机器,被称为定点机。当然了,现代计算机一般只要有运算部件,都会提供对定点数运算的支持。

虽然理论上,定点数的小数点的位置可以任意规定,但通常只会用定点数表示纯小数整数,当表示纯小数时,小数约定在上一篇文章里反复提及的符号位和数值部分之间,同理,表示整数时,则在数值部分的后面。下图展示了定点小数和定点整数的结构:

定点数

为什么通常只用定点数表示纯小数或整数呢?因为上面我们提到的,定点机在存储定点数时,并不存储小数点,因此我们的数字在定点机中是一串看上去像整数的东西,如果我们存入了非纯小数或整数,它们的计算结果就会很容易出现问题,譬如1.2312.3,在定点机中它们没有小数点,都是123,那么它们在做加、减、除后的结果都是不对的,即便是乘法,其结果也需要我们自己再去算它的小数点位。

而纯小数或整数处理起来就方便多了,譬如整数,它的加、减、乘三种运算结果都是整数,而除法如果遇到除不尽的情况,一般也会取余处理。纯小数同理,但比整数稍微麻烦一点,主要是加法和除法,会有益处的风险,此时一些定点机可能会直接抛出异常。

定点机由于它的特性,在硬件层面设计会更简单。

2. 浮点数

浮点数是大家比较熟悉的一个词汇,也就是我们平时编程语言中的floatdouble。前面定点数由于本身性质的限制,难以处理复杂的非纯小数和整数,此时就需要浮点数来处理了。

所谓浮点,与定点相对,就是小数点是浮动的,不固定的,它的形式有点像我们熟悉的科学计数法,譬如12.34这个数,可以写成下面几种形式:

12.34 = 1.234\times10^1 = 0.1234\times10^2 = 1234\times10^{-2}12.34=1.234×101=0.1234×102=1234×10−2

后面这三种形式都能表示12.34这个数字,尽管它们的小数点位置各不相同,但因为后面乘了不同的10的幂次方,因此最终结果一致。

浮点数的标准形式如下:

N = M \times B^EN=M×B**E

其中,M为尾数,B为基数,E为阶码,这个式子和各个字母的含义已经非常清晰了,直接对照上面12.34这个例子看就好。当然了,12.34这个例子举的是我们最熟悉的十进制,我们计算机中使用的当然是二进制,而根据前面几篇(应该是第一篇)二进制系列文章我们知道,二进制各个位数之间相差2倍,因此,如果要用浮点式表示一个二进制数时,这里的基数B就是2了,譬如:

101.11 = 10.111\times2^1 = 1.0111\times2^2 = 10111\times2^{-2} = 0.10111\times2^3 = 0.010111\times2^4101.11=10.111×21=1.0111×22=10111×2−2=0.10111×23=0.010111×24

非常好理解。有些地方会把阶码E也表示成二进制的形式,譬如2次幂使用10次幂来表示,这个大家根据实际情况辨别即可,核心都是不变的。

通常来说,计算机为了提高数据精度和便于浮点数之间的比较,规定浮点数的尾数M用纯小数表示,即上面二进制101.11的最后两种表示形式。同时,将尾数最高位为1的浮点数称为规格化数,对于101.11来说,倒数第二种形式$0.10111\times2^3$就是它的规格化表示。

2.1 计算机中的浮点数

上面介绍的是浮点数的基本定义,但这是给人类看的,计算机中肯定有其特殊的存储形式,我们直接来看现代计算机的通用国际标准IEEE 754,我们现在在用的计算机基本上都是基于这个标准来存储浮点数的,包括我们熟悉的短浮点数(float长浮点数(double,它们俩的表示方法相同,区别仅仅是阶码E和位数M的位数不同:

IEEE754标准

上图就是浮点数IEEE 754的标准形式了,我们逐个来看:

  • 第一个位置是数符,就是表整个数字正负的符号,即01
  • 接着是阶码E,这里的阶码也有正负,并且不用真值来表示,通常会用阶码的真值加上一个偏移量,作为实际存储的偏移值。如在短浮点数float中,这个偏移量为127,即$2^7-1=1111111_{(2)}$。这样做的目的和上一篇文章中我们讲到的移码类似 ,主要是为了比较时更方便。加上偏移量之后,使得原本带符号的阶码E变成了一个无符号数,或者直接理解为去除了符号位对阶码大小的影响,等会儿的例子中我们再来看它的具体表示;
  • 最后是尾数,这个部分为了提高精度,规定将原数尾数转化为1.xxxx的形式,以1为默认最高位,然后储存的时候并不储存最高位1,视其为隐藏的,只存储小数点后面的部分,这样可以使尾数表示的精度达到最高,即存储位数最多,比实际位数多一位。

在讲例子之前,我们再来看一下短浮点数(float长浮点数(doubleIEEE 754中各个部分的位数:

IEEE754中float和double的位数

这就是我们在初学编程语言时,教科书上告诉我们的float32位,精度没有64位的double高的原因了。尾数代表了精度,而阶码代表了表示范围。

然后我们来看一个例子,以float为例,我们取一个十进制数13.625,它对应的二进制是1101.101,我们来看一下它按照IEEE 754标准转换成float的过程和结果。

首先,将原数写成标准规定的格式,以1为最高整数位:$1101.101 = 1.101101\times2^3$。

然后把整数位1舍弃(隐藏),得到一个纯小数尾数101101。因为float的尾数全长为23位,同时这个尾数是纯小数,因此在当前尾数的后面用0补全,得到真正的尾数1011 0100 0000 0000 0000 00023位。

接下来是阶码3,先转成二进制11,这是它的真值,再用真值加上$2^7-1$,即0111 1111 + 0000 0011 = 1000 0010,得到的就是一个8位阶码实际存储值。上面我们说过,这里与偏移量相加,是为了便于比较,而相加后的结果可以看做是一个无符号的二进制数,因此它最高位的1并不指代它的正负。如果我们用一个负数的阶码与偏移量相加,就会得到一个最高位0的结果,这样就能直接比较出阶码的大小了。

得到阶码和尾数后,只需要在最高位上添加数符就好了。因为原数是正数,因此数符S0,最终得到的IEEE 754标准的float浮点数为:

float数

double太长就不写了,原理是一样的,大家如果有兴趣可以自己试着算一算。

转载自:https://segmentfault.com/a/1190000024485146