Тип double — вещественные числа

Для работы с вещественными (дробными) числами в Java используется тип double . В памяти он занимает 8 байт (в два раза больше, чем тип int ) и может хранить значения в диапазоне -1.7*10 308 до +1.7*10 308 . Для сравнения: тип int может хранить значение в диапазоне -2*10 9 до +2*10 9 .
В вещественных числах дробная часть записывается после точки. Например, 123.456, или 2.5, или 100.00 или 0.01. Такие числа еще называют числами с плавающей точкой — floating point number – компьютерное название для вещественных чисел.
Кстати, кроме типа double есть еще вещественный тип float (размером всего 4 байта). Его название как раз и происходит от floating point . Название же double происходит от double float . Тип double в два раза больше, чем float : 8 байт против 4 . Его еще называют вещественное число двойной точности .
2. Создание переменной типа double
Тип double используется для хранения вещественных чисел. Чтобы создать в коде переменную, которая будет способна хранить вещественные числа, нужно воспользоваться командой:
Где имя — это имя переменной. Примеры:
| Команда | Описание |
|---|---|
| Создается вещественная переменная price | |
| Создается вещественная переменная weight | |
| Создается вещественная переменная lightSpeed |
Так же, как и с типом int , можно использовать краткую запись для создания нескольких переменных типа double :
И даже сразу присваивать им значения:
| Команда | Примечание |
|---|---|
| В переменной хранится значение 5.0 | |
| В переменной хранится значение 2.0 |
3. Присваивание целых и вещественных чисел
Было бы плохо, если бы целые числа можно было присваивать только переменным типа int , а вещественные — только переменным типа double . Хотелось бы иметь возможность преобразовывать одни числа в другие. И в Java такая возможность есть.
Во-первых, переменным типа double можно присваивать как вещественные, так и целые числа. При присваивании целых чисел они просто преобразовываются в вещественные. Хотя иногда при этом возможна небольшая потеря точности.
| Команда | Примечание |
|---|---|
| В переменной хранится значение 5.0 | |
| В переменной хранится значение 2.0 | |
| В переменной x хранится значение 1000000.0 |
Во-вторых, если в каком-то выражении участвуют целое и вещественное число, целое сначала преобразуется в вещественное и только потом взаимодействует с другим вещественным числом.
| Команда | Примечание |
|---|---|
| В переменной x хранится значение 5000.0 | |
| На экран будет выведено число 10 | |
| На экран будет выведено число 10.0 |
И наконец, есть возможность присваивать переменным типа int вещественные числа. Дробная часть числа при этом отбрасывается — число округляется вниз до целого.
Также компилятор требует, чтобы этот факт программист задокументировал явно (чтобы другие программисты понимали, что тут происходит отбрасывание дробной части). Общий вид этого выражения в коде такой:
Вещественные типы (double, float)
Вещественные типы (или типы с плавающей точкой) представляют значения, имеющие дробную часть. В языке MQL5 есть два типа для чисел с плавающей точкой. Способ представления вещественных чисел в машинной памяти определен стандартом IEEE 754 и не зависит от платформ, операционных систем и языков программирования.
Размер в байтах
Минимальное положительное значение
Имя double означает, что точность этих чисел вдвое превышает точность чисел типа float . В большинстве случаев тип double является наиболее удобным. Ограниченной точности чисел float во многих случаях попросту недостаточно. Причина, по которой тип float все еще используется, — экономия памяти при хранении (это важно для больших массивов вещественных чисел).
Константы с плавающей точкой состоят из целой части, точки (.) и дробной части. Целая и дробная части представляют собой последовательности десятичных цифр.
double a= 12.111 ;
double b=- 956.1007 ;
float c = 0.0001 ;
float d = 16 ;
Существует научный способ записи вещественных констант, зачастую этот способ записи более компактный, чем традиционный.
double c1=1.12123515e-25;
double c2=0.000000000000000000000000112123515; // 24 нуля после десятичной точки
Print ( "1. c1 = " , DoubleToString (c1,16));
// Результат: 1. c1 = 0.0000000000000000
Print ( "2. c1 = " , DoubleToString (c1,-16));
// Результат: 2. c1 = 1.1212351499999999e-025
Print ( "3. c2 = " , DoubleToString (c2,-16));
// Результат: 3. c2 = 1.1212351499999999e-025
Необходимо помнить, что вещественные числа хранятся в памяти компьютера с некоторой ограниченной точностью в двоичной системе счисления, в то время как общепринятой в использовании является десятичная система счисления. Поэтому многие числа, которые точно записываются в десятичной системе, в двоичной системе можно записать только в виде бесконечной дроби.
Например, числа 0.3 и 0.7 представлены в компьютере бесконечными дробями, в то время как число 0.25 хранится точно, так как представляет из себя степень двойки.
В связи с этим, категорически не рекомендуется сравнивать между собой два вещественных числа на равенство, так как такое сравнение не является корректным.
void OnStart ()
<
//—
double three=3.0;
double x,y,z;
x=1/three;
y=4/three;
z=5/three;
if (x+y==z)
Print ( "1/3 + 4/3 == 5/3" );
else
Print ( "1/3 + 4/3 != 5/3" );
// Результат: 1/3 + 4/3 != 5/3
>
Если все же необходимо сравнить на равенство два вещественных числа, то можно сделать это двумя разными способами. Первый способ заключается в сравнении разницы между двумя числами с какой-то малой величиной, задающей точность сравнения.
bool EqualDoubles( double d1, double d2, double epsilon)
<
if (epsilon<0)
epsilon=-epsilon;
//—
if (d1-d2>epsilon)
return false;
if (d1-d2<-epsilon)
return false;
//—
return true ;
>
void OnStart ()
<
double d_val=0.7;
float f_val=0.7;
if (EqualDoubles(d_val,f_val,0.000000000000001))
Print (d_val, "equals" ,f_val);
else
Print ( "Different: d_val = " , DoubleToString (d_val,16), " f_val = " , DoubleToString (f_val,16));
// Результат: Different: d_val= 0.7000000000000000 f_val= 0.6999999880790710
>
Необходимо отметить, что значение параметра epsilon в приведенном примере не может быть меньше предопределенной константы DBL_EPSILON. Значение этой константы 2.2204460492503131e-016. Для типа float соответствующая константа FLT_EPSILON = 1.192092896e-07. Смысл этих значений таков, что это наименьшее значение, удовлетворяющее условию 1.0+DBL_EPSILON != 1.0 (для чисел типа float 1.0+FLT_EPSILON != 1.0).
Второй способ предполагает сравнивать нормализованную разность двух вещественных чисел с нулевым значением. Сравнивать разность нормализованных чисел с нулём бесполезно, так как в результате любой математической операции с нормализованными числами результат получается ненормализованным.
bool CompareDoubles( double number1, double number2)
<
if ( NormalizeDouble (number1-number2,8)==0)
return ( true );
else
return ( false );
>
void OnStart ()
<
double d_val=0.3;
float f_val=0.3;
if (CompareDoubles(d_val,f_val))
Print (d_val, "equals" ,f_val);
else
Print ( "Different: d_val = " , DoubleToString (d_val,16), " f_val = " , DoubleToString (f_val,16));
// Результат: Different: d_val= 0.3000000000000000 f_val= 0.3000000119209290
>
В результате некоторых операций математического сопроцессора может получиться недействительное вещественное число, которое нельзя использовать в математических операциях и операциях сравнения, так как результат выполнения операций над недействительными вещественными числами неопределен. Например, при попытке вычислить арксинус от 2, результатом будет минус бесконечность.
double abnormal = MathArcsin (2.0);
Print ( "MathArcsin(2.0) =" ,abnormal);
// Результат: MathArcsin(2.0) = -1.#IND
Кроме минус бесконечности существуют плюс бесконечность и NaN (не число). Чтобы определить, что данное число недействительно, можно использовать функцию MathIsValidNumber(). По стандарту IEEE они имеют специальное машинное представление. Например, плюс бесконечность для типа double имеет битовое представление 0x7FF0 0000 0000 0000.
struct str1
<
double d;
>;
struct str2
<
long l;
>;
//— начнем
str1 s1;
str2 s2;
//—
s1.d= MathArcsin (2.0); // получим недействительное число -1.#IND
s2=s1;
printf ( "1. %f %I64X" ,s1.d,s2.l);
//—
s2.l=0xFFFF000000000000; // недействительное число -1.#QNAN
s1=s2;
printf ( "2. %f %I64X" ,s1.d,s2.l);
//—
s2.l=0x7FF7000000000000; // наиобльшее нечисло SNaN
s1=s2;
printf ( "3. %f %I64X" ,s1.d,s2.l);
//—
s2.l=0x7FF8000000000000; // наименьшее нечисло QNaN
s1=s2;
printf ( "4. %f %I64X" ,s1.d,s2.l);
//—
s2.l=0x7FFF000000000000; // наибольшее нечисло QNaN
s1=s2;
printf ( "5. %f %I64X" ,s1.d,s2.l);
//—
s2.l=0x7FF0000000000000; // плюс бесконечность 1.#INF и наименьшее нечисло SNaN
s1=s2;
printf ( "6. %f %I64X" ,s1.d,s2.l);
//—
s2.l=0xFFF0000000000000; // минус бесконечность -1.#INF
s1=s2;
printf ( "7. %f %I64X" ,s1.d,s2.l);
//—
s2.l=0x8000000000000000; // отрицательный ноль -0.0
s1=s2;
printf ( "8. %f %I64X" ,s1.d,s2.l);
//—
s2.l=0x3FE0000000000000; // 0.5
s1=s2;
printf ( "9. %f %I64X" ,s1.d,s2.l);
//—
s2.l=0x3FF0000000000000; // 1.0
s1=s2;
printf ( "10. %f %I64X" ,s1.d,s2.l);
//—
s2.l=0x7FEFFFFFFFFFFFFF; // наибольшее нормализованное число (MAX_DBL)
s1=s2;
printf ( "11. %.16e %I64X" ,s1.d,s2.l);
//—
s2.l=0x0010000000000000; // наименьшее положительное нормализованное (MIN_DBL)
s1=s2;
printf ( "12. %.16e %.16I64X" ,s1.d,s2.l);
//—
s1.d=0.7; // покажем, что число 0.7 – бесконечная дробь
s2=s1;
printf ( "13. %.16e %.16I64X" ,s1.d,s2.l);
/*
1. -1.#IND00 FFF8000000000000
2. -1.#QNAN0 FFFF000000000000
3. 1.#SNAN0 7FF7000000000000
4. 1.#QNAN0 7FF8000000000000
5. 1.#QNAN0 7FFF000000000000
6. 1.#INF00 7FF0000000000000
7. -1.#INF00 FFF0000000000000
8. -0.000000 8000000000000000
9. 0.500000 3FE0000000000000
10. 1.000000 3FF0000000000000
11. 1.7976931348623157e+308 7FEFFFFFFFFFFFFF
12. 2.2250738585072014e-308 0010000000000000
13. 6.9999999999999996e-001 3FE6666666666666
*/
4.8 – Числовые типы с плавающей точкой
Целочисленные типы отлично подходят для подсчета целых чисел, но иногда нам нужно хранить очень большие числа или числа с дробной частью. Переменная типа с плавающей точкой (запятой) – это переменная, которая может содержать действительное число, например 4320,0, -3,33 или 0,01226. «Плавающая» в названии «с плавающей точкой» указывает на то, что десятичная точка может «плавать»; то есть она может поддерживать переменное количество цифр до и после себя.
Существует три разных типа данных с плавающей точкой: float , double и long double . Как и в случае с целыми числами, C++ не определяет фактические размеры этих типов (но гарантирует минимальные размеры). В современных архитектурах представление с плавающей точкой почти всегда соответствует двоичному формату IEEE 754. В этом формате float составляет 4 байта, double – 8, а long double может быть эквивалентно double (8 байтов), 80 битам (часто с дополнением до 12 байтов) или 16 байтам.
Типы данных с плавающей запятой всегда идут со знаком (могут содержать положительные и отрицательные значения).
| Категория | Тип | Минимальный размер | Типовой размер |
|---|---|---|---|
| С плавающей запятой | float | 4 байта | 4 байта |
| double | 8 байт | 8 байт | |
| long double | 8 байт | 8, 12 или 16 байт |
Ниже показан пример определения чисел с плавающей запятой:
При использовании литералов с плавающей точкой всегда включайте хотя бы один знак после десятичной точки (даже если этот знак равен 0). Это помогает компилятору понять, что число принадлежит типу плавающей точкой, а не к целочисленному типу.
Обратите внимание, что по умолчанию литералы с плавающей точкой по умолчанию имеют тип double . Суффикс f используется для обозначения литерала типа float .
Лучшая практика
Всегда проверяйте, соответствует ли тип ваших литералов типу переменных, которым они назначаются или используются для инициализации. В противном случае произойдет ненужное преобразование, возможно, с потерей точности.
Предупреждение
Убедитесь, что вы не используете целочисленные литералы там, где должны использоваться литералы с плавающей точкой. Это включает в себя инициализацию или присвоение значений объектам с плавающей точкой, выполнение арифметических операций с плавающей точкой и вызов функций, ожидающих значений с плавающей точкой.
Печать чисел с плавающей точкой
Теперь рассмотрим следующую простую программу:
Результаты работы этой, казалось бы, простой программы могут вас удивить:
В первом случае std::cout напечатал 5, хотя мы ввели 5.0. По умолчанию std::cout не будет печатать дробную часть числа, если она равна 0.
Во втором случае число печатается так, как мы и ожидали.
В третьем случае напечаталось число в экспоненциальном представлении (если вам нужно освежить в памяти экспоненциальное представление, смотрите урок «4.7 – Введение в экспоненциальную запись»).
Диапазоны значений типов с плавающей точкой
Предполагая, что используется представление IEEE 754:
| Размер | Диапазон значений | Точность |
|---|---|---|
| 4 байта | от ±1,18 × 10 -38 до ±3,4 × 10 38 | 6-9 значащих цифр, обычно 7 |
| 8 байт | от ±2,23 × 10 -308 до ±1,80 × 10 308 | 15-18 значащих цифр, обычно 16 |
| 80 бит (обычно используется 12 или 16 байт) | от ±3,36 × 10 -4932 до ± 1,18 × 10 4932 | 18-21 значащая цифра |
| 16 байт | от ±3,36 × 10 -4932 до ± 1,18 × 10 4932 | 33-36 значащих цифр |
80-битный тип с плавающей запятой – это своего рода историческая аномалия. На современных процессорах он обычно реализуется с использованием 12 или 16 байтов (что является более естественным размером для обработки процессорами).
Может показаться немного странным, что 80-битный тип с плавающей запятой имеет тот же диапазон значений, что и 16-байтовый тип с плавающей запятой. Это связано с тем, что у них одинаковое количество бит, выделенных для показателя степени, однако 16-байтовое число может хранить больше значащих цифр.
Точность с типов плавающей запятой
Рассмотрим дробь 1/3. Десятичное представление этого числа – 0,33333333333333… с тройками, уходящими в бесконечность. Если бы вы писали это число на листе бумаги, ваша рука в какой-то момент устала бы, и вы, в конце концов, прекратили бы писать. И число, которое у вас осталось, будет близко к 0,3333333333…. (где 3-ки уходят в бесконечность), но не совсем.
На компьютере число бесконечной длины потребует для хранения бесконечной памяти, но обычно у нас есть только 4 или 8 байтов. Эта ограниченная память означает, что числа с плавающей запятой могут хранить только определенное количество значащих цифр – и что любые дополнительные значащие цифры теряются. Фактически сохраненное число будет близко к необходимому, но не точно.
Точность числа с плавающей запятой определяет, сколько значащих цифр оно может представлять без потери информации.
При выводе чисел с плавающей точкой std::cout по умолчанию имеет точность 6, то есть предполагает, что все переменные с плавающей точкой имеют только до 6 значащих цифр (минимальная точность с плавающей точкой), и, следовательно, он будет отсекать всё, что идет дальше.
Следующая программа показывает усечение std::cout до 6 цифр:
Эта программа выводит:
Обратите внимание, что каждое из напечатанных значений имеет только 6 значащих цифр.
Также обратите внимание, что std::cout в некоторых случаях переключился на вывод чисел в экспоненциальном представлении. В зависимости от компилятора показатель степени обычно дополняется до минимального количества цифр. Не беспокойтесь, 9.87654e+006 – это то же самое, что 9.87654e6 , только с некоторым количеством дополнительных нулей. Минимальное количество отображаемых цифр показателя степени зависит от компилятора (Visual Studio использует 3, некоторые другие в соответствии со стандартом C99 используют 2).
Число цифр точности переменной с плавающей запятой зависит как от размера (у float точность меньше, чем у double ), так и от конкретного сохраняемого значения (некоторые значения имеют большую точность, чем другие). Значения float имеют точность от 6 до 9 цифр, при этом большинство значений float имеют не менее 7 значащих цифр. Значения double имеют от 15 до 18 цифр точности, при этом большинство значений double имеют не менее 16 значащих цифр. Значения long double имеет минимальную точность 15, 18 или 33 значащих цифр в зависимости от того, сколько байтов этот тип занимает.
Мы можем переопределить точность по умолчанию, которую показывает std::cout , используя функцию манипулятора вывода с именем std::setprecision() . Манипуляторы вывода изменяют способ вывода данных и определяются в заголовке iomanip .
Поскольку с помощью std::setprecision() мы устанавливаем точность в 16 цифр, каждое из приведенных выше чисел печатается с 16 цифрами. Но, как видите, числа определенно неточны до 16 цифр! А поскольку числа float менее точны, чем числа double , число ошибок у float больше.
Проблемы с точностью влияют не только на дробные числа, они влияют на любое число со слишком большим количеством значащих цифр. Рассмотрим большое число:
123456792 больше, чем 123456789 . Значение 123456789.0 имеет 10 значащих цифр, но значения float обычно имеют точность 7 цифр (и результат 123456792 точен только до 7 значащих цифр). Мы потеряли точность! Когда теряется точность из-за того, что число не может быть точно сохранено, это называется ошибкой округления.
Следовательно, нужно быть осторожным при использовании чисел с плавающей запятой, которые требуют большей точности, чем могут содержать переменные.
Лучшая практика
Если нет ограничений по использованию памяти, отдавайте предпочтение использованию double вместо float , поскольку неточность float часто приводит к погрешностям.
Ошибки округления затрудняют сравнение чисел с плавающей запятой
С числами с плавающей запятой сложно работать из-за неочевидных различий между двоичными (как хранятся данные) и десятичными (как мы думаем) числами. Рассмотрим дробь 1/10. В десятичном формате ее легко представить как 0,1, и мы привыкли думать о 0,1 как о легко представимом числе с 1 значащей цифрой. Однако в двоичном формате 0,1 представлен бесконечной последовательностью: 0,00011001100110011… Из-за этого, когда мы присваиваем 0,1 числу с плавающей точкой, мы сталкиваемся с проблемами точности.
Эффект от этого можно увидеть в следующей программе:
Эта программ выводит следующее:
Как и ожидалось, в первой строке std::cout печатает 0,1 .
Во второй строке, где std::cout показывает нам 17-значную точность, мы видим, что d на самом деле не совсем равно 0,1 ! Это связано с тем, что из-за ограниченной памяти double пришлось усекать приближение. В результате получается число с точностью до 16 значащих цифр (что гарантирует тип double ), но это число не равно 0,1. Ошибки округления могут сделать число немного меньше или немного больше, в зависимости от того, где происходит усечение.
Ошибки округления также могут иметь неожиданные последствия:
Хотя можно было ожидать, что d1 и d2 должны быть равны, мы видим, что это не так. Если бы мы сравнивали d1 и d2 в программе, программа, вероятно, не работала бы так, как ожидалось. Поскольку числа с плавающей запятой имеют тенденцию быть неточными, их сравнение обычно проблематично – мы обсудим эту тему (и решения) подробнее в уроке «5.6 – Операторы отношения и сравнение значений с плавающей запятой».
Последнее замечание об ошибках округления: математические операции (такие как сложение и умножение), как правило, приводят к увеличению ошибок округления. Таким образом, даже несмотря на то, что 0,1 имеет ошибку округления в 17-й значащей цифре, когда мы складываем 0,1 десять раз, ошибка округления добралась бы и до 16-й значащей цифры. Продолжение операций приведет к тому, что эта ошибка станет всё более значительной.
Ключевые выводы
Ошибки округления возникают, когда число не может быть сохранено точно. Это может произойти даже с простыми числами, например, 0,1 . Таким образом, ошибки округления могут происходить постоянно. Ошибки округления – не исключение, а правило. Никогда не предполагайте, что ваши числа с плавающей запятой точны.
Следствие этого правила: будьте осторожны с использованием чисел с плавающей запятой для финансовых или валютных данных.
NaN и Inf
Существует две особые категории чисел с плавающей запятой. Первая – Inf , которая представляет бесконечность. Inf может быть положительной или отрицательной. Вторая – NaN , что означает «Not a Number» (не число). Существует несколько различных типов NaN (которые мы здесь обсуждать не будем). NaN и Inf доступны только в том случае, если компилятор для чисел с плавающей запятой использует определенный формат (IEEE 754). Если используется другой формат, следующий код приводит к неопределенному поведению.
Ниже приведена программа, показывающая все эти три категории чисел с плавающей точкой:
И результаты работы этой программы при использовании Visual Studio 2008 в Windows:
INF означает бесконечность, а IND означает неопределенность. Обратите внимание, что результаты печати Inf и NaN зависят от платформы, поэтому ваши результаты могут отличаться.
Лучшая практика
Вообще избегайте деления на 0, даже если ваш компилятор поддерживает это.