一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情。
从一个实际例子开始
假如你开发了一个后台系统,其中有关于金额的一些操作。比如说,最小精度是分,也就是一分钱。
那么可以规定,1 代表一分,而10代表一角,自然100代表一元钱。
此时此刻,0.1元这个金额就可以用 10 这个整数来表示。
这就代表着,用整数类型的变量是完全可以存储一个小数类型的数值的。
为什么要用整数来存储小数
因为现在计算机里自带的浮点数是不精确的。这个跟浮点数的存储方式有关系。这并不是本文的主要内容。所以这里不详解了。
用两个整数来表示一个小数
既然不能用浮点数,那么用两个整数来表达任意有理数。也就是分数的概念:
- 分子 : 一个任意整数(不能超出计算机能表示的范围)
- 分母 : 一个任意整数(不能超出计算机能表示的范围)
示例代码(Golang):
type MyNumber struct {
Fenzi int64 // 分子
Fenmu int64 // 分母
}
上述代码中,如果我们要表达 , 那么
var n = MyNumber{
Fenzi : 1,
Fenmu : 3,
}
这样一来,只要不超过int64的范围,那么任意有理数都能被表示出来了。
但是有下面的问题:
- 如何来进行加减乘除的运算?
- 如何导出成正规的人能看得懂的数字呢?
答案就是,自己实现这些操作。
这里给出一个加法的例子(请复习小学数学):
// myNumber = myNumber + other
// 加法
func (myNumber *MyNumber) Add (other *MyNumber) {
newFenzi := myNumber.Fenzi * other.Fenmu + myNumber.Fenmu * other.Fenzi
newFenmu := myNumber.Fenmu * other.Fenmu
myNumber.Fenzi = newFenzi
myNumber.Fenmu = newFenmu
}
那么其他的运算啥的跟上面同样,用小学数学知识就行了。
这样搞行不行呢,当然行。
不过,这样搞有点类似于 BigDecimal,就是大数。
注意:真正的大数能超脱int64, 而上面的实现,还是基于int64的。
开始讲定点数
定点数本身的类型可以是int32, 也可以是int64,这取决于你的系统需要多大范围的数。
对,定点数本身不是BigDecimal,无法表示任意有理数,定点数是有范围的。
举一个例子:
int26_6 : 本质类型是 int32,只不过,前面26位bit,用来存放整数部分,后面6位用来存放小数部分。
那么具体怎么存放的呢,举一个例子:
如上,我们将 1.25 分成 1 和 1/4两个部分。
1,我们用 1<<6 = 64来表示。
1/4, 我们用 1<<4 = 16来表示。
1+1/4, 自然就是上述两者相加: 1<<6 + 1<<4 = 64 + 16 = 80来表示。
用二进制表示:
0000 0000 0000 0000 0000 0000 01|01 0000
注意上面的竖线,表示整数和小数之间的小数点。
这里有一个问题,前面整数部分很好理解,最低位是1,那么整数部分整个就是 1。
小数部分呢:
010000 为什么就是 ?
我们看一下 010000 这个二进制代表的是几:16。
而 四个16正好就是 64, 记得吗,64 在我们的算法里,就是整数1。
我们在二进制里也来计算一下四个010000相加:
010000 + 010000 + 010000 + 010000 = 1000000
而1000000在我们的算法里,也正好就是整数1。
从上面可以看出来,int26_6的加减法,正好兼容了原本整数的加减法,无需做改造。
但是乘除就不行了。这里不详解,有兴趣的可以去网上查,也可以自己推导。
用定点数的好处
- 精确 (尤其涉及到金额时, 当然金额最好用BigDecimal方案)
- 多系统同步(如果你做过游戏,就明白了)
- 比浮点数计算更节省cpu