Урок 33. Тип данных с плавающей точкой. Floating point

   ⁄ 

 Обновлено 17 Апр 2017

  ⁄   

⁄  1

Целочисленные типы данных отлично подходят для работы с целыми числами, но иногда нам придется работать и с дробными числами. И тут на помощь приходит тип данных с плавающей точкой (floating point). Еще его называют тип данных с плавающей запятой. Переменная такого типа может хранить любые вещественные (действительные) числа, например: 4320.0, -3.33 или 0.01226. Почему точка «плавающая»? Дело в том, что точка или запятая может двигаться («плавать») между цифрами, разделяя таким образом целую и дробную части в значении.

Есть три типы данных с плавающей точкой: floatdouble и long double. Как и с целочисленными типами, C++ определяет только их минимальный размер. На современных архитектурах, floating point представляется в соответствии со стандартом IEEE 754. Таким образом, float занимает 4 байта, double — 8 байтов, long double — 8, 12 или 16 байтов.

Типы данных с плавающей точкой всегда signed (т.е. могут хранить как положительные, так и отрицательные числа).

Категория Тип Минимальный размер Типичный размер
floating point float 4 байта 4 байта
double 8 байтов 8 байтов
long double 8 байтов 8, 12 или 16 байтов

Объявление переменных:

Когда нам нужно указать целое число для типа с плавающей запятой, то принято использовать разделительную точку с, по крайней мере, одной цифрой в дробной части. Это позволяет различать переменные типов integer и floating point.

Обратите внимание, что литералы типа с плавающей запятой по умолчанию относятся к типу double. Суффикс f используется для обозначения литерала типа float.

Экспоненциальная запись

Как переменные данного типа хранят информацию – вне нашего урока, но это очень похоже на числа в экспоненциальной записи. Экспоненциальная запись очень полезна для написания длинных чисел в короткой форме. И хотя это может показаться на первый взгляд несколько необычно, понимание этого принципа поможет вам разобраться, как работает тип с плавающей запятой и, что еще более важно, каковы его ограничения.

Числа в экспоненциальной записи имеют следующий вид: мантисса х 10экспонент. Например, рассмотрим выражение 1.2 x 104. Значение 1.2 – это мантисса (значащая часть), а 4 – это экспонент (порядок числа). Результат этого выражения: 12 000.

Обычно, в экспоненциальной записи в целой части находится только одна цифра, все остальные пишутся после точки (в дробной части).

Рассмотрим массу Земли. В десятичной системе, мы бы записали это как 5973600000000000000000000 кг. Согласитесь, очень большое число (даже слишком большое, чтобы поместиться в целочисленную переменную 8 байтов). Его также трудно читать (там 19 или 20 нулей?). Но в экспоненциальной записи его можно записать как 5.9736 х 1024 кг, что гораздо легче прочитать. Еще одним преимуществом этого способа является сравнение величины двух очень больших или очень маленьких чисел – достаточно просто сравнить их экспоненты.

В C++ буква «е» или «Е» означает, все равно что «поднести число 10 к степени…». Например, 1.2 x 104 — эквивалентно 1.2e4 или 5.9736 x 1024 можно записать как 5.9736e24.

Для чисел, меньших единицы, экспонент может быть отрицательным. Например, 5e-2 эквивалентно 5 * 10-2, что в свою очередь 5 / 102 или 0,05. Масса электрона — 9.1093822e-31 кг.

На самом деле, мы можем использовать экспоненциальную запись для присвоения значений переменным типа с плавающей запятой.

Как конвертировать числа в экспоненциальную запись

Для этого следует выполнить следующую процедуру:

 ваш экспонент начинается с нуля.

 переместите точку, которая разделяет целую и дробную части так, чтобы слева от точки осталась только одна ненулевая цифра.

 каждый поворот точки влево увеличивает экспонент на 1.

 каждый поворот точки вправо уменьшает экспонент на 1.

 откиньте все нули слева перед одной ненулевой цифрой в целой части.

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

Теперь немного практики:

Исходное число: 42030
Передвигаем точку на 4 цифры влево: 4.2030e4
Слева (в целой части) нет нулей: 4.2030e4
Откидаем нуль справа (конечный): 4.203e4 (4 значащих цифры)

Исходное число: 0.0078900
Передвигаем точку вправо на 3 цифры: 0007.8900e-3
Откидаем нули слева: 7.8900e-3
Не откидаем нули справа (исходное число - дробь): 7.8900e-3 (5 значащих цифр)

Исходное число: 600.410
Передвигаем точку влево на 2 цифры: 6.00410e2
Слева нет нулей: 6.00410e2
Справа нули оставляем: 6.00410e2 (6 значащих цифр)

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

Точность и диапазон

Рассмотрим дробь 1/3. Десятичное представление этого числа — 0,33333333333333… тройки так и продолжаются до бесконечности. Бесконечное число в свою очередь требует и бесконечной памяти для хранения, а мы, как правило, имеем только 4 или 8 байтов. Переменные типа с плавающей запятой могут хранить только определенное количество значащих цифр, остальные теряются. Точность и определяет количество значащих цифр, которые могут представлять число без потери информации.

Когда мы выводим числа с плавающей запятой, то cout, по умолчанию, имеет точность 6. То есть, в консольном окне мы увидим только 6 значащих цифр, всех остальных не будет.

Рассмотрим это в коде (точность cout — 6):

Результат

9.87654
987.654
987654
9.87654e+006
9.87654e-005

Обратите внимание, что в каждом числе осталось только 6 значащих цифр (часть перед Е, а не часть перед точкой).

Также, в некоторых случаях, cout сам может выводить числа в экспоненциальной записи. В зависимости от компилятора, экспонент, как правило, будет дополнен (нулями) до минимального количества цифр. Не бойтесь, 9.87654e+006 – это тоже самое, что 9.87654e6, просто с несколькими нуликами. Минимальное количество цифр экспонента определяется компилятором (Visual Studio использует 3, некоторые другие используют 2 в соответствии со стандартом C99).

Также, мы можем переопределить точность cout по умолчанию, используя функцию std::setprecision(), которая находится в заголовке iomanip.

Результат:

3.333333253860474
3.333333333333334

Так как мы установили точность до 16, то каждая переменная выводится с 16 цифрами. Но, как вы можете наблюдать, в исходных числах больше цифр!

Число точности зависит от размера (в float точность меньше, чем в double) и от самого числа, которое должно храниться (некоторые значения имеют более высокую точность, чем другие). Точность float — от 6 до 9 цифр (в основном 7), double — от 15 до 18 цифр (в основном 16), long double — 15, 18 или 33 (в зависимости от того, сколько байтов занимает тип).

И это относится не только к дробным числам, но и ко всем значениям, которые имеют слишком большое количество значащих цифр. Например:

Результат:

123456792

Но 123456792 больше чем 123456789. Значение 123456789.0 имеет 10 значащих цифр, но точность float, как правило, 7. Поэтому мы получили другое число – потеря в точности!

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

Рассмотрим floating point в стандарте IEEE 754:

Размер Диапазон Точность
4 байта от ±1.18 x 10-38 до ±3.4 x 1038 6-9 значащих цифр, в основном 7
8 байтов от ±2.23 x 10-308 до ±1.80 x 10308 15-18 значащих цифр, в основном 16
80 бит(12 байтов) от ±3.36 x 10-4932 до ±1.18 x 104932 18-21 значащих цифр
16 байтов от ±3.36 x 10-4932 до ±1.18 x 104932 33-36 значащих цифр

Может показаться немного странным, что 12-байтовое число типа с плавающей точкой имеет тот же диапазон, что и 16-байтовое число. Это потому, что они имеют одинаковое количество битов, выделенных для экспонента. Однако, в 16-байтовой переменной точность будет выше.

Правило: Используйте double, нежели float, если достаточно места, так как отсутствие полной точности в float очень часто приводит к ошибкам в программе.

Ошибки округления

Одна из причин, по которой числа типа с плавающей запятой могут быть каверзными– это неочевидные различия между бинарными (как хранятся данные) и десятичными (как мы думаем) числами. Рассмотрим дробь 1/10. В десятичной системе, это легко представить в виде 0.1 и мы привыкли думать, что 0.1 – это легко представляемое число. Тем не менее, в двоичной системе, 0.1 представлено бесконечной последовательностью: 0.00011001100110011… Из-за этого, когда мы присваиваем значение 0.1 переменной типа с плавающей запятой – у нас будут возникать проблемы с точностью.

Например:

Результат

0.1
0.10000000000000001

В верхней строке, cout выводит 0.1, как и ожидаемо.

В нижней же строке, после того, как мы изменили точность по умолчанию в cout до 17 цифр, мы видим, что переменная d – это не совсем 0.1! Подобное происходит из-за ограничений в количестве выделяемой памяти типу double, и в необходимости урезать (округлять) числа. Это называется — ошибка округления.

Подобные ошибки могут иметь неожиданные последствия:

Результат:

1
0.99999999999999989

Хоть мы и ожидали, что d1 и d2 окажутся равными, но всё произошло не так. А если бы в программе довелось бы сравнить эти переменные? В таком случае, ошибок не миновать.

Последнее замечание: математические операции (например, сложение или умножение), как правило, только увеличивают рост этих ошибок. Даже если 0.1 имеет погрешность в 17-й значащей цифре, когда мы добавим 0.1 десять раз, ошибка округления перенесется к 16-й значащей цифре.

NaN и Inf

Есть две специальные категории чисел типа с плавающей запятой. Первая — Inf, что значит infinity (бесконечность). Inf может быть положительной или отрицательной. Вторая — NaN, что значит “Not a Number” (не число). Есть несколько разных видов NaN (их мы здесь обсуждать не будем).

А вот программа, которая показывает все три случая:

Результат (Visual Studio 2008, Windows):

1.#INF
-1.#INF
1.#IND

INF означает бесконечность, а IND – неопределенность (indeterminate). Обратите внимание, что результаты вывода Inf и NaN зависят от платформы, поэтому они могут немного отличаться.

Выводы

Подводя итог, следует запомнить две главные вещи:

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

2. Переменные типа с плавающей запятой очень часто имеют небольшие ошибки округления, даже если их точность не превышена. В большинстве случаев такие ошибки остаются незамеченными, так как они столь незначительны. Но следует помнить, что сравнение таких переменных может иметь неожиданные результаты. А выполнение математических операций над ними только повлечет их рост.

Тест

Запишите следующие числа в экспоненциальной записи в стиле C++ (используя букву «е» как экспонент) и определите, сколько значащих цифр каждое из них имеет:

a) 34.50

b) 0.004000

c) 123.005

d) 146000

e) 146000.001

f) 0.0000000008

g) 34500.0

Ответ

Чтобы увидеть результат, нажмите на ссылку.

Ответ 1

а) 3.450e1 (4 значащие цифры)

б) 4.000e-3 (4 значащие цифры)

с) 1.23005e2 (6 значащих цифр)

d) 1.46e5 (3 значащие цифры)

е) 1.46000001e5 (9 значащих цифр)

е) 8e-10 (1 значащая цифра). Здесь мантисса не 8.0, а 8. Поэтому число имеет только 1 значащую цифру.

г) 3.45000e4 (6 значащих цифр). Здесь конечные нули мы не откидаем, так как в исходном числе есть точка, которая разделяет целую и дробную части. Хоть эта точка и никак не влияет на значение числа, но она влияет на точность. Если бы число было указано как 34500, тогда ответ был бы 3.45e4.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (15 оценок, среднее: 4,33 из 5)
Загрузка...
Поделиться в:
Подписаться на обновления:

Комментариев: 1

  1. Виталий:

    Пока что это самая тёжелая статья из всех 33 мной прочитанных.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *