浮点数

在日常生活中,我们使用带小数点的数(即实数)的频率与使用整数的频率一样高。“实数”这个名称本身表明,使用这些数,您可以表达现实世界的一些有形的事物,如重量、长度、体温,也就是说,这些事物可以用非整数单位数量来度量,但要“带点零头”。

我们在交易中也经常用到实数。例如,它们用于表示交易订单中的交易品种价格或交易量(通常允许整手交易的小数部分)。

MQL5 中提供了 2 种实数类型:表示单精度的 float 和表示双精度的 double

在源代码中,floatdouble 类型的常量值通常记录为整数和小数部分(每个部分都是一个数字序列),由字符 . 分隔,如 1.23 或 -789.01。可以没有整数或小数(但不能都没有),但必须有点。例如,.123 表示 0.123,而 123. 表示 123.0。而 123 将创建一个整数类型常量。

然而,还有另一种记录实数常量的形式,即指数形式。其中,整数和小数部分后跟 'E' 或 'e' (不分大小写),整数表示以 10 为底的幂数,即附加因子。例如,以下几种表示法以指数形式表示同一个数 0.57:

 .0057e2
0.057e1
.057e1
57e-2

记录实数常量时,后面的几个字节默认定义为 double 类型(占用 8 个字节)。要设置 float 类型,需要在常量右侧添加 'F' 或 'f' 后缀。

floatdouble 类型在大小、值范围和数字表示精度方面有区别。所有这些如下表所示。

类型

大小(字节)

最小值

最大值

精度(位数)

float

4

±1.18 * 10-38

±3.4 * 1038

6 至 9,通常是 7

double

8

±2.23 * 10-308

±1.80 * 10308

15 至 18,通常是 16

值范围以绝对值形式显示:最小值和最大值决定了正负区域中允许值的范围幅度。与整数类型类似,这些极限值也有内嵌的命名常量:FLT_MIN、FLT_MAX、DBL_MIN、DBL_MAX。

请注意,实数都是有符号的,也就是说,不存在无符号的实数。

精度是指相关类型的实数能够不失真地存储的有效位数(十进制位数)。

的确,实数类型数字的精度要低于整数类型。这是为了获得普遍性和更广泛的可能值而付出的代价。例如,如果无符号 4 字节整数 (uint) 的最高值为 4294967295,即大约 4 百万或 4.29*109那么 4 字节实数 (float) 的最大值为 3.4 * 1038,后者比前者高了 29 个数量级。对于 8 字节类型,这种差异更加明显:ulong 可以容纳 18446744073709551615 (18.44*1018或大约 18 个一百亿亿),而 double 可以容纳 1.80 * 10308,后者比前者高 289 个数量级。插叙内容提供了关于精度的更多细节。

尾数和指数
 
实数在内存中的内部表示(以分配的字节为单位)十分复杂。高阶位被用作负号标志(我们在整数类型中也看到过)。所有其他位被分成两组。较大一组包含数字的尾数,即有效位(我们指的是二进制位)。较小一组存储以 10 为底的幂数(指数),通过将幂数与尾数相乘得到存储的数。特别是,float 类型尾数大小为 24 位 (FLT_MANT_DIG),而 double 类型尾数大小为 53 位 (DBL_MANT_DIG)。就传统的十进制位而言,我们将得到与上表所示相同的精度:float 的最低有效位数为 6 (FLT_DIG),而 double 的最低有效位数为 15 (DBL _DIG)。但某些特定数字会有“幸运”的位组合,对应着更大的十进制位数。floatdouble 的参数大小分别为 8 位和 11 位。
 
由于指数的原因,实数的值范围要大得多。同时,随着指数的增加,尾数低位的“比重”也在增加。这意味着,可在计算机内存中表示的两个相邻实数之间有很大区别。例如,对于数字 1.0,低位的“比重”在 float 情况下是 1.192092896 e–07(FLT_EPSILON),而在 double 情况下是 2.2204460492503131e-016 (DBL_EPSILON)。也就是说,如果一个数小于 1.192092896 e–07,那么编译器无法区分这个数字与 1.0 的区别。这好像不重要或者“没什么大不了的”,但是对于更大的数而言,这种不确定区域会变大。如果存储了一个 float 类型的数字,大小约为 10 亿 (1*109那么最后 2 位将无法安全存储,也无法从内存中恢复(参见下文的代码范例)。但是,根本问题不在于数的绝对值,而在于其中位数的最大值,要无损失的召回。同样,我们可以尝试将一个数表示为 float 数 1234.56789(在结构上很像金融工具的价格);由于其内部表示缺乏精度,它的最后两位会“浮动”。
 
如果表示为 double,类似的情况会在处理更大的数字(或更多有效数字)时才会显现,但仍然可能发生,且并不罕见。当运算非常大或非常小的实数时,您应该考虑这一点,并在编写程序时额外检查一下,以防止潜在的精度损失。特别要注意的是,应该以一种特殊方式将实数与零进行比较。我们将在 比较运算符一节中讨论。
 
细心的读者会发现,上面指定的尾数和指数的大小可能有错误。我们以 float 为例进行解释。它存储在大小为 4 字节的内存单元中,即占用 32 位。同时,尾数大小为 24,指数大小为 8,加起来已经是 32 位。那么符号位在哪里?关键就在于 IT 专业人员以“规范化”的形式存储尾数。如果我们回顾一下记录普通十进制数的指数形式,就会容易理解这个问题。例如,数字 123.0 可以表示为 1.23E2、12.3E1 或 0.123E3。一种表示法被认为是规范化形式,其中小数点之前只有一个有效位(即,非零)。对于 123.0 这个数字,可以表示为 1.23E2。根据定义,第 1 到 9 位被认为是十进制记数法中的有效位。顺理成章的,我们进入二进制记数法。其中只有一个有效位,1。似乎二进制记数法中的规范化形式总是从 1 开始,并且可以被省略(无须占用内存)。以这种方式,尾数可以节省一位。事实上,它包含 23 位(多出的一个高阶单位是隐式的,在重构数字并从内存中检索时会自动添加)。将尾数减少 1 位可以为有符号位腾出空间。

在应使用浮点类型时,我们主要选择 double 类型,这样会更精确。仅在需要节省内存时才会用 float 类型,例如处理非常大的数据数组。

MQL5/Scripts/MQL5Book/p2/TypeFloat.mq5 脚本中显示了一些实数类型常量的应用示例。

void OnStart()
{
  double a0 = 123;      // ok, a0 = 123.0
  double a1 = 123.0;    // ok, a1 = 123.0
  double a2 = 0.123E3;  // ok, a2 = 123.0
  double a3 = 12300E-2// ok, a3 = 123.0
  double b = -.75;      // ok, b = -0.75
  double q = LONG_MAX;  // warning: truncation, q = 9.223372036854776e+18
                        //               LONG_MAX = 9223372036854775807
  double d = 9007199254740992// ok, maximal stable long in double
  double z = 0.12345678901234567890123456789// ok, but truncated
                           // to 16 digits: z = 0.1234567890123457
  double y1 = 1234.56789;  // ok, y1 = 1234.56789
  double y2 = 1234.56789f// accuracy loss, y2 = 1234.56787109375
  float m = 1000000000.0;  // ok, stored as is
  float n =  999999975.0;  // warning: truncation, n = 1000000000.0
}

a0a1a2a3 变量包含用不同方法编写的同一个数 (123.0)。

b 变量的常量中,小数点之前省略了无意义的零。此外,这里演示了使用如何使用负号 '-' 记录一个负数。

尝试在 q 变量中存储最大整数。此时,编译器给出一个警告,因为 double 无法精确地表示 LONG_MAX:实际存储的是 9223372036854776000 而不是 9223372036854775807。这明显表明,虽然 double 的值范围远远超过整数的值范围,但这是通过丢失低阶位而实现的。

作为对比,double 类型能够无失真存储的最大整数被赋予 d 变量的值。如果对整数序列使用 double 类型,序列中会间歇性跳过某些数字。

z 变量再次提醒我们关于最大有效位数 (16) 的局限性:超过该限制的常量会被截断。

y1y2 变量中,当同一个数以不同格式(doublefloat)记录时,我们就会看到因转换为 float 而损失的精度。

实际上,mn 变量将被编译器视为相等,因为 999999975.0 以内部表示存储时,就变成了 1000000000.0。

数字类型常用于公式计算;而且为数字类型定义了一组广泛的运算(参见 表达式)。

这类计算有时会产生不正确的结果,也就是说,它们无法表示为数字。例如,无法定义负数的平方根或零的对数。在这类情况下,实数类型可以存储一个名为 NaN(Not A Number,非数字)的特殊值。事实上,这类值有几种不同的类型,例如,区分正无穷大和负无穷大。MQL5 提供了一个特殊函数,MathIsValidNumber,该函数检查 double 值是数字还是 NaN 值。