Урок №33. Типы данных с плавающей точкой: float, double и long double

В данном уроке мы изучим различные типы данных с плавающей точкой в языке программирования C++, их точность и диапазон. Мы узнаем, что представляет собой экспоненциальная запись и как её применяют, также мы обсудим проблемы округления и дадим определения для значений nan и inf .
Типы данных с плавающей точкой
Работа с целыми числами отлично подходит для целочисленных типов данных, но также существуют и дробные числа. Для работы с ними используется тип данных с плавающей точкой (или "тип данных с плавающей запятой", англ. "floating point"). Переменная такого типа способна хранить любые действительные дробные значения, например: 4320.0, -3.33 или 0.01226. Почему же точка "плавающая"? Это связано с тем, что точка или запятая перемещается ("плавает") между цифрами, разделяя целую и дробную части значения.
Существуют три вида данных с плавающей точкой: float, double и long double. В языке программирования C++ устанавливается лишь минимальный размер этих типов данных, аналогично целочисленным типам. Данные с плавающей точкой всегда являются знаковыми (то есть могут хранить как положительные, так и отрицательные числа).

Декларация переменных различных типов данных с использованием чисел с плавающей запятой:
float fValue ;
double dValue ;
long double dValue2 ;
Для того чтобы работать с целым числом в переменной типа с плавающей точкой, необходимо добавить к числу десятичную точку и ноль. Таким образом можно четко различить переменные целочисленного типа от переменных с плавающей точкой:
int n ( 5 ) ; // 5 - это целочисленный тип
double d ( 5.0 ) ; // 5.0 - это тип данных с плавающей точкой (по умолчанию double)
float f ( 5.0f ) ; // 5.0 - это тип данных с плавающей точкой ("f" от "float")
Важно помнить, что по умолчанию литералы с плавающей точкой относятся к типу double. Если добавить символ f в конце числа, это будет означать тип float.
Экспоненциальная запись
Использование экспоненциальной записи позволяет представить длинные числа в более компактной форме. Числа в экспоненциальной записи представляются как мантисса умножить на 10 в степени экспонента. Например, если взять выражение 1.2 × 10^4, то 1.2 будет мантиссой (или значащей частью числа), а 4 - экспонентой (или порядком числа). Результатом этого выражения будет число 12000.
Как правило, в экспоненциальной нотации, в целой части присутствует лишь одна цифра, в то время как все остальные идут после десятичной точки (в дробной части).
Давайте рассмотрим массу Земли. В десятичной системе она равна 5973600000000000000000000 килограмм. Это действительно огромное число, которое даже не поместится в целочисленную переменную размером 8 байт. Чтение такого числа может быть сложной задачей (сколько же нулей там?). Однако, если использовать экспоненциальную запись, массу Земли можно представить как 5.9736 × 10^24 кг, что намного удобнее для восприятия. Еще одним плюсом экспоненциальной записи является возможность сравнения двух очень больших или очень маленьких чисел путем сравнения их экспонент.
В языке программирования C++ символ е / Е используется для обозначения возведения числа 10 в степень, указанную после этого символа. Например, запись 1.2 × 10^4 можно представить как 1.2e4, а значение 5.9736 × 10^24 можно записать как 5.9736e24.
Если число меньше единицы, то экспонента может быть отрицательной. Например, запись 5e-2 можно переписать как 5 * 10-2, что равно 5 / 102 или 0.05. Масса электрона составляет 9.1093822e-31 килограмма.
В реальной жизни экспоненциальная нотация может быть применена при выполнении операций присваивания вот так:
Объявлена переменная d1 типа double со значением 5000.0.
Объявлена переменная d2 типа double и ей присвоено значение 5000 с использованием другого способа.
Объявлена переменная d3 типа double со значением 0.05;
Объявляем переменную d4 и присваиваем ей значение 5 умножить на 10 в минус 2 степени. // Альтернативный способ установить значение 0.05
Конвертация чисел в экспоненциальную запись
Для преобразования чисел в научную запись нужно выполнить следующие шаги:
Давайте рассмотрим несколько примеров:
Начальное значение: 42030
Сдвигаем точку на 4 позиции влево: 4.2030e4
В целой части нет нулей слева: 4.2030e4
Убираем последний ноль после запятой: 4.203e4 (4 значимые цифры)
Начальное значение: 0.0078900
Сдвигаем точку на три позиции вправо: 0007.8900e-3
Избавляемся от нулей слева: 7.8900e-3
Сохраняем нули справа (исходное значение - дробное): 7.8900e-3 (5 значимых цифр)
Имеется начальное число: 600.410
Сдвигаем точку на две позиции влево: 6.00410e2
В начале нет нулей: 6.00410e2
Сохраняем нули в конце: 6.00410e2 (6 значимых цифр)
Одной из ключевых вещей, которую стоит усвоить, является то, что цифры в мантиссе (часть числа перед e) называются значимыми. Точность значения определяется количеством значимых цифр. Чем больше цифр в мантиссе, тем более точное значение получается.
Точность и диапазон типов с плавающей точкой
Давайте рассмотрим дробь 1/3. Если мы попытаемся представить это число в десятичном виде, мы получим 0.33333333333333... (с тройками до бесконечности). Однако бесконечное количество тройек требует бесконечного объема памяти для хранения, в то время как у нас обычно есть всего лишь 4 или 8 байт. Переменные с плавающей запятой могут сохранить только определенное количество значащих цифр, отбрасывая остальные. Точность числа определяется количеством значащих цифр, которые могут быть сохранены без потери данных.
При выводе переменных типа с плавающей точкой в стандартный поток вывода cout точность по умолчанию составляет 6 знаков после запятой. Это означает, что на экране будет отображено только 6 значащих цифр, остальные будут отброшены. Например:
#include
int main ( )
{
float f ;
f = 9.87654321f ;
std :: cout << f << std :: endl ;
f = 987.654321f ;
std :: cout << f << std :: endl ;
f = 987654.321f ;
std :: cout << f << std :: endl ;
f = 9876543.21f ;
std :: cout << f << std :: endl ;
f = 0.0000987654321f ;
std :: cout << f << std :: endl ;
return 0 ;
}
Результат работы программы:
9.87654
987.654
987654
9.87654e+06
9.87654e-05
Обратите внимание, что каждое из указанных выше значений содержит только 6 значимых цифр (цифры перед буквой e, а не перед точкой).
Иногда cout может выводить числа в экспоненциальной форме. В некоторых случаях экспонента может быть дополнена нулями в зависимости от компилятора. Например, 9.87654e+06 эквивалентно 9.87654e6 (с добавленным нулем и знаком +). Количество цифр в экспоненте определяется компилятором (например, Visual Studio использует 2 цифры, в то время как другие компиляторы могут использовать 3).
Дополнительно, есть возможность изменить точность вывода cout, используя метод std::setprecision(), который доступен в заголовочном файле iomanip:
#include
#include// для std::setprecision()
int main ( )
{
std :: cout << std :: setprecision ( 16 ) ; // задаем точность в 16 цифр
float f = 3.33333333333333333333333333333333333333f ;
std :: cout << f << std :: endl ;
double d = 3.3333333333333333333333333333333333333 ;
std :: cout << d << std :: endl ;
return 0 ;
}
Результат работы программы:
3.333333253860474
3.333333333333333
Поскольку мы повысили точность до 16 знаков, каждая переменная теперь отображается с 16 цифрами. Однако, как можно заметить, исходные числа содержат больше цифр!
Размер типа данных влияет на точность (например, тип float обладает меньшей точностью по сравнению с типом double), а также на точность значения, которое присваивается:
Точность чисел с плавающей запятой обычно составляет от 6 до 9 цифр, в основном 7.
Точность чисел с плавающей запятой типа double обычно составляет от 15 до 18 цифр (чаще всего 16).
Количество цифр после запятой в long double может составлять 15, 18 или 33 в зависимости от размера типа данных на конкретном компьютере.
Данный принцип применим не только к числам с плавающей запятой, но и ко всем данным, содержащим избыточное количество значащих цифр. Например:
#include
#include// для std::setprecision()
int main ( )
{
float f ( 123456789.0f ) ; // переменная f имеет 10 значащих цифр
std :: cout << std :: setprecision ( 9 ) ; // задаем точность в 9 цифр
std :: cout << f << std :: endl ;
return 0 ;
}
Итак, вот что у нас получилось:
123456792
Однако 123456792 превышает 123456789, верно? Значение 123456789.0 содержит 10 значимых цифр, но точность float составляет 7. Поэтому мы получили другое число из-за потери данных!
Согласно стандарту IEEE 754, описаны диапазон и точность типов данных с плавающей точкой:

Возможно, кажется необычным, что переменная типа с плавающей точкой размером 12 байт имеет такой же диапазон, как и переменная размером 16 байт. Это объясняется тем, что у них одинаковое количество бит, выделенных для экспонента (однако точность у 16-байтовой переменной будет выше).
Рекомендация: Вместо типа float лучше использовать тип double по умолчанию, так как он обладает более высокой точностью.
Ошибки округления
Давайте рассмотрим дробь 1/10. Если мы переведем эту дробь в десятичную систему счисления, то получим число 0.1. Однако в двоичной системе счисления эта дробь будет представлена как бесконечная последовательность цифр: 0.00011001100110011… Именно из-за таких различий в представлении чисел в разных системах счисления мы можем столкнуться с проблемами точности. Например:
#include
#include// для std::setprecision()
int main ( )
{
double d ( 0.1 ) ;
std :: cout << d << std :: endl ; // используем точность cout по умолчанию (6 цифр)
std :: cout << std :: setprecision ( 17 ) ;
std :: cout << d << std :: endl ;
return 0 ;
}
Результат работы программы:
0.1
0.10000000000000001
Первый вывод оператора cout показывает значение 0.1, что ожидаемо. После того, как мы установили точность вывода для объекта cout на 17 цифр, мы обнаружили, что значение переменной d не точно равно 0.1! Это происходит из-за ограничений памяти, выделенной для переменных типа double, а также из-за необходимости округления чисел. Фактически, мы столкнулись с распространенной ошибкой округления.
Такие недочеты могут вызвать непредвиденные последствия:
#include
#include// для std::setprecision()
int main ( )
{
std :: cout << std :: setprecision ( 17 ) ;
double d1 ( 1.0 ) ;
std :: cout << d1 << std :: endl ;
double d2 ( 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 ) ; // должно получиться 1.0
std :: cout << d2 << std :: endl ;
}
Результат работы программы:
1
0.99999999999999989
Мы предполагали, что значения d1 и d2 будут одинаковыми, но оказалось иначе. Может быть, стоит сравнить эти переменные и в зависимости от результата выбрать определенное действие? Таким образом, мы сможем избежать ошибок.
Обычно математические действия (например, сложение или умножение) лишь усиливают эти неточности. Даже если погрешность числа 0.1 составляет 17-ю значащую цифру, то после десяти операций сложения ошибка округления сместится на 16-ю значащую цифру.
nan и inf
Существуют две особые группы чисел в формате с плавающей точкой:
Давайте проанализируем некоторые практические примеры:
#include
int main ( )
{
double zero = 0.0 ;
double posinf = 5.0 / zero ; // положительная бесконечность
std :: cout << posinf << "
" ;
double neginf = - 5.0 / zero ; // отрицательная бесконечность
std :: cout << neginf << "
" ;
double nan = zero / zero ; // не число (математически некорректно)
std :: cout << nan << "
" ;
return 0 ;
}
Результат работы программы:
inf
-inf
-nan(ind)
Символ inf обозначает "бесконечность", а символ ind означает "неопределенность" (по-английски "indeterminate"). Следует отметить, что результаты вывода inf и nan могут различаться в зависимости от компилятора и архитектуры компьютера, поэтому результат выполнения вашей программы может отличаться от моего.
Заключение
Идеальным вариантом для хранения очень крупных или крайне маленьких (включая дробные) чисел являются переменные с плавающей точкой, при условии, что они имеют ограниченное количество значащих цифр (не превышающее точность определенного типа данных).
Даже если точность типа не превышена, переменные с плавающей точкой могут содержать незначительные ошибки округления. В большинстве случаев эти ошибки остаются незамеченными из-за их незначительности. Однако важно помнить, что при сравнении переменных с плавающей точкой могут возникнуть неопределенные результаты, а выполнение математических операций с такими переменными может увеличить эти ошибки.
Тест
Преобразуйте данные числа в экспоненциальную форму, используя язык программирования C++ (используя букву "е" в качестве символа экспоненты), и определите количество значащих цифр в каждом из представленных чисел:
Comments