Вещественные числа

Числа с дробной частью, или иначе — вещественные, используются нами в повседневной жизни не менее часто, чем целые. Само название "вещественные" говорит о том, что с помощью таких чисел можно выразить нечто осязаемое из реального мира — вес, длину, температуру тела — то, что измеряется не ровным количеством единиц измерения, а "с хвостиком".

В трейдинге мы также часто оперируем вещественными числами. Например, в них выражаются цены инструментов или объемы в торговых приказах (обычно, допускающие дробные части целого лота).

В MQL5 предусмотрено 2 вещественных типа: float — нормальной точности и double — удвоенной точности.

В исходном коде константные значения типов float и double обычно записываются как целая и дробная части (каждая из них — это последовательность цифр), разделенные символом '.', например, 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').

Типы float и double отличаются размером, диапазоном значений и точностью представления чисел. Все это указано в таблице ниже.

Тип

Размер (байты)

Минимальное

Максимальное

Точность (разряды)

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). Но в зависимости от конкретного числа, в нем могут быть "удачные" сочетания битов, соответствующие большему количеству десятичных цифр. Размеры показателей — 8 и 11 бит для float и double соответственно.
 
За счет степенного показателя вещественные числа получают гораздо больший диапазон значений. Вместе с тем, при увеличении показателя "удельный вес" младшего разряда мантиссы также увеличивается, что означает, что два соседних вещественных числа, которые возможно представить в памяти компьютера, существенно отличаются. Например, для числа 1.0 "удельный вес" младшего бита равен 1.192092896e–07 (FLT_EPSILON) в случае float, и 2.2204460492503131e-016 (DBL_EPSILON) в случае double. Иными словами, число 1.0 неотличимо от любого числа в его окрестности меньше 1.192092896e–07. Это кажется не очень важным или страшным, но для более крупных чисел данная область неопределенности также становится больше. Если сохранить во float число около 1 миллиарда (1*109), последние 2 цифры перестанут надежно сохраняться и восстанавливаться из памяти (см. пример кода ниже). Но, в принципе, проблема не в абсолютной величине числа, а в максимальном количестве цифр в нем, которые нужно воспроизвести без потерь. С тем же "успехом" мы можем попытаться уместить во float число вида 1234.56789 (которое по структуре очень похоже на цену финансового инструмента), и два его последних разряда "будут гулять" из-за недостатка точности во внутреннем представлении.
 
Для double похожая ситуация начнет проявляться для гораздо больших чисел (или для гораздо большего количества значащих цифр), но она все равно возможна и часто случается на практике. Это необходимо учитывать при работе с очень большими или очень маленькими вещественными числами и писать программы с дополнительными проверками на возможную потерю точности. В частности, сравнивать вещественное число на ноль нужно особым образом. Мы коснемся этого в разделе про операции сравнения.
 
Внимательному читателю можно показаться, что размеры мантиссы и показателя указаны выше неверно. Поясним на примере типа float. Он хранится в ячейке памяти размером 4 байта, то есть занимает 32 бита. При этом размер мантиссы (24) и показателя (8) в сумме уже дают 32. А где же тогда бит со знаком? Дело в том, компьютерщики договорились хранить мантиссу в нормализованном виде. Понять, что это такое будет проще, если для начала рассмотреть показательную форму записи обычного десятичного числа. Скажем, число 123.0 можно представить как 1.23E2, 12.3E1, 0.123E3. Нормализованной формой считается запись, когда перед точкой стоит только одна значащая цифра, то есть не ноль. Для данного числа это — 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
}

Переменные a0, a1, a2, a3 содержат одинаковые числа (123.0), записанные разными способами.

В константе для переменной b опущен незначащий ноль перед точкой. Кроме того, здесь демонстрируется запись отрицательного числа с помощью знака минус '-'.

В переменную q сделана попытка сохранить максимальное целое число. В этом месте компилятор выдает предупреждение, потому что double не способен точно представить LONG_MAX: вместо 9223372036854775807 получится 9223372036854776000. Это наглядно показывает, что хоть диапазоны значений double намного превосходят диапазоны целых чисел, это достигается за счет потери младших разрядов.

Для сравнения, максимальное целое, которое double способен хранить без искажений, приведено в качестве значения переменной d. После него в последовательности целых чисел начнут встречаться пропуски, если мы используем для них double.

Переменная z напоминает еще раз об ограничении на максимальное количество значащих цифр (16) — более длинная константа будет усечена.

Переменные y1 и y2, в которые записывается одно и то же число, но в разных форматах (double и float), позволяют увидеть потерю точности из-за перехода к float.

Переменные m и n будут по факту равны, потому что 999999975.0 сохраняется во внутреннем представлении приблизительно и превращается в 1000000000.0.

Числовые типы часто используются для расчетов по формулам, для них определен широкий набор операций (см. главу Выражения).  

Вычисления иногда могут приводить к некорректным результатам, то есть их невозможно представить числом. Например, корень из отрицательного числа или логарифм от нуля не определены. Для таких случаев вещественные типы умеют хранить специальную величину NaN (Not A Number). На самом деле таких величин — несколько разных видов, позволяющих, например, отличить плюс бесконечность от минус бесконечности. MQL5 предоставляет специальную функцию MathIsValidNumber, которая проверяет, является значение double числом или одним из NaN.