1

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这个数字,尽管它们的小数点位置各不相同,但因为后面乘了不同的10的幂次方,因此最终结果一致。

浮点数的标准形式如下:

$$N = M \times 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^4$$

非常好理解。有些地方会把阶码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太长就不写了,原理是一样的,大家如果有兴趣可以自己试着算一算。

3. 总结

作为二进制系列的最后一篇,这可能是整个系列里最短的一篇了,主要是考虑到定点数和浮点数,这两个东西一般仅了解概念,实际应用的机会非常少,也就浮点数我们可能会在写代码时考虑溢出等问题的时候才会用到它,也因此我把本就不长的篇幅中的大部分都给了浮点数。

二进制系列算是完结了,本人才疏学浅,几篇写下来文字量也不少,若文章中有什么遗漏或错误,欢迎大家指出,非常感谢!


程序员小杜
1.3k 声望37 粉丝

会写 Python,会写 Go,正在学 Rust。