正文开始之前,我首先要感谢博客园提供的这个优秀的平台。通过在这个优秀的平台上和很多志同道合的朋友交流,互相帮助,我也很荣幸的获得了15年的微软MVP的奖项。也使我更加坚信了代码改变世界。感激!感恩!努力!加油!
0x00 前言慕容在生活和工作中常常会遇到一些十分迷信机器的人,他们之中很多人都相信机器是最理智的,没有任何感情,是真正的铁面无私,因此机器的运算所给出的答案总是正确的,如果答案错误,那么一定是操作机器的人的问题。但机器的运算就一定是正确的吗?事实上,机器出现运算错误并不是一个罕见的情况,一个典型的例子便是小数运算。下面就让我们来聊一个相关的话题,在机器或者具体的说在C#语言中小数是如何被处理的?
0x01 先从一个“错误”的答案说起既然要聊一聊机器是怎么把算术题做错的,那么我们自然要先来看一个机器运算错误的小例子。
#include <stdio.h> void main(){ float num; int i ; num = 0; for(i = 0; i < 100; i++) { num += 0.1; } printf(, num); }
这是一份C语言写成的小程序,逻辑十分简单易懂,所要实现的结果无非是将0.1相加100次之后再输出。我想不需要计算机来计算,我们自己心算就能立刻得出答案——10。那么计算机会交给我们一份什么样的答案呢?下面我们将这份C代码编译并且运行一下。
答案一输出,就让人大吃一惊。怎么计算机还不如人的心算吗?如果按照慕容在前言中提到的那些朋友这时可能就开始纠结是否是代码写错了,亦或者是慕容的电脑出现了什么问题。但事实上代码是正确的,机器也是运行如常的。那么究竟是为什么计算机的运算输给了人的心算呢?这就引出了下一个问题,计算机是如何处理小数的呢?如果我们了解一下计算机处理小数的机制,那么这一切的迷就能够解开了。(当然如果有朋友用C#来对0.1加100次,之后的结果是10。但那是C#在幕后为我们做的一些隐藏工作,在计算机处理小数的问题上,本质是一样的)。
0x02 数字的格式一个程序可以看做是现实世界的一个数字化的模型。现实世界中的一切都可以转化为数字在计算机的世界中重新复活。因此,一个不得不解决的问题就是数字是如何在计算机中表达的。这也是数字格式出现的意义。
众所周知,机器语言全部都是数字,但是本文自然不会关心全部的二进制格式。这里我们只关心现实中有意义的数字是如何在计算机中被表示的。简单而言,有意义的数字大体可以分为以下三种格式。
整数格式我们在开发的过程中遇到的大部分的数字其实都是整数。而整数在计算机中也是最容易表示的一种。我们遇到的整数都可以使用32位有符号整数来表示(Int32)。当然,如果需要,还有有符号 64 位整数数据类型(Int64)可供选择。至于和整数相对应的便是小数,而小数主要有两种表示方式。
定点格式所谓定点格式,即约定机器中所有数据的小数点位置是固定不变的。而定点小数的最常见的例子是SQL Server中的money类型。事实上定点小数已经很不错了,它显然能够适合很多需要处理小数的情况。但是它有一个与生俱来的缺点,那就是由于小数点的位置固定,因此它能表示范围是受限的。因此下面我们本文的主角就要出场了。
浮点格式解决定点格式先天问题的方案便是浮点格式的出现。而浮点格式的组成则包括符号、尾数、基数和指数,通过这四部分来表示一个小数。由于计算机内部是二进制的,因此基数自然而然是2(就如十进制的基数是10一样)。因此计算机在数据中往往无需记录基数(因为总是2),而是只用符号、尾数、指数这三部分来表示。很多编程语言都至少提供了两种使用浮点格式表示小数的数据类型,即我们常常能见到的双精度浮点数double和单精度浮点数float。同样,在我们的C#语言中也存在着这两种使用了浮点格式表示小数的数据类型——按照C#语言标准双精度浮点数和单精度浮点数在C#中对应的是System.Double和System.Single。但是事实上在C#语言中还存在着第三种使用了浮点格式表示小数的数据类型,那就是decimal类型——System.Decimal。需要注意的是,浮点格式的表示形式有很多,而在C#中遵循的是IEEE 754标准:
既然聊完了数字在计算机中的几种表示形式,那么接下来我们就不得不提一下在选择数字格式时的一些指标。最常见的无非是这几点:表示范围、精度、准确度。
数字格式的表示范围顾名思义,数字格式的表示范围指的就是这种数字格式所能表示的最小的值到最大的值的范围。例如一个16位有符号整数的表示范围是从-32768到32767。如果要被表示的数字的值超出了这个范围,那么使用这种数字格式就不能正确的表示这个数字了。当然在这个范围内的数字也有可能无法被正确的表示,例如16位有符号整数是无法准确表示一个小数的,但是总有一个接近的值是可以用16为有符号整数格式来表示的。
数字格式的精度实话实说,精度和准确度让很多人都有一种十分模糊的感觉,似乎是一样的却又有区别。但慕容需要提醒各位注意的是,精度和准确度是两个有巨大差距的概念。
通俗的来讲,数字格式的精度可以认为是该格式有多少信息用来表示一个数字。更高的精度通常意味着能够表示更多的数字,一个最明显的例子便是精度越高那么这种格式所能表示的数字就越接近真实的数字。例如我们知道1/3如果换算成小数0.3333....是无穷尽的,那么它在五位精度的情况下可以写成0.3333,而在七位的情况下就又变成了0.333333(当然,如果用七位表示五位,那么就是0.333300)。
数字格式的精度还会影响到计算的过程。举一个简单的例子,如果在计算中我们使用的是一位精度。那么整个计算可能就变成了下面的这种情况:
0.5 * 0.5 + 0.5 * 0.5 = 0.25 + 0.25