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

  Юрий  | 

    | 

  Обновл. 30 Мар 2019  | 

 25635

 ǀ   23 

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

Типы данных с плавающей точкой

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

Есть три типы данных с плавающей точкой: floatdouble и long double. Как и с целочисленными типами, C++ определяет только их минимальный размер. Типы данных с плавающей точкой всегда являются signed (т.е. могут хранить как положительные, так и отрицательные числа).

Категория Тип Минимальный размер Типичный размер
Тип данных с плавающей точкой float 4 байта 4 байта
double 8 байт 8 байт
long double 8 байт 8, 12 или 16 байт

Объявление переменных разных типов данных с плавающей точкой:

Если нужно использовать целое число с переменной типа с плавающей точкой, то тогда нужно указать после разделительной точки нуль. Это позволяет различать переменные целочисленных типов от переменных типов с плавающей запятой:

Обратите внимание, литералы типа с плавающей точкой по умолчанию относятся к типу 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 значащих цифр, остальные — потеряются. Например:

Результат выполнения программы выше:

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:

Результат выполнения программы выше:

3.333333253860474
3.333333333333333

Так как мы увеличили точность до 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. Поэтому мы и получили другое число, произошла потеря данных!

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

Диапазон и точность типов данных с плавающей точкой, согласно стандарту 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, так как его точность выше.


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

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

Результат выполнения программы выше:

0.1
0.10000000000000001

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

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

Результат выполнения программы выше:

1
0.99999999999999989

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

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

nan и inf


Есть две специальные категории чисел типа с плавающей запятой:

  inf (или ещё «бесконечность», от англ «infinity»), которая может быть либо положительной, либо отрицательной.

  nan (или ещё «не число», от англ «not a number»). Их есть несколько видов (обсуждать их здесь мы не будем).

Рассмотрим примеры на практике:

Результат выполнения программы выше:

inf
-inf
-nan(ind)

inf означает «бесконечность», а ind означает «неопределённый» (от англ. «indeterminate»). Обратите внимание, результаты вывода inf и nan зависят от компилятора/архитектуры компьютера, поэтому ваш результат выполнения программы выше может отличаться от моего результата.

Заключение

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

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

Тест

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

   34.50;

   0.004000;

   123.005;

   146000;

   146000.001;

   0.0000000008;

   34500.0.

Ответ

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

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

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

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

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

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

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

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

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (125 оценок, среднее: 4,85 из 5)
Загрузка...

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

  1. Аватар Александр:

    поправка: std::setprecision это не функция… это такой вид аргумента (операнда) для оператора вывода в поток — манипулятор потока.

    есть функция-член для объекта std::cout — precision
    использование:

    преимущество — не нужно подключать никаких дополнительных библиотек. Этот метод реализован в любом объекте std::ostream

    1. Аватар Константин:

      Ой-ё-ёй, Александр, постойте! Позвольте два вопросика Вам задать: 1- Я отдельным файлом создаю namespace (ну само-собой подключаю его где нужно в программе) в котором пользователь в режиме RUNTIME заносит некие константы, востребованные на всём протяжении работы проги и практически во всех функциях (файлах). Так как их перечень довольно большой, легко перепутать в какую ячейку какое значение вносить, а выводить на экран предложение внести желаемое значение в ПОИМЕНОВАННУЮ ячейку я никак не могу сообразить способ… 2 — Как активировать debugging в Code::Bocks 17.12? Можешь подсказать?

  2. Аватар somebox:

    Я вот не понял, если мы определили переменную как float, зачем в литерале использовать f?

    1. Аватар vlAsTT:

      В С++, при инициализации переменных типа float в следующем стиле :

      f будет иметь тип double. Вы можете убедиться в этом, наведя курсор на переменную. Поэтому, чтобы иметь тип float, нужно добавить суффикс f и тогда переменная будет иметь тип float.

  3. Аватар Светлана:

    Здравствуйте.
    у меня не работает ни мой вариант программы, ни ваш. Делаю с обычными числами — всё ок. Экспоненциальная запись — вот такая вот штука происходит:

    Enter your number:
    3e3
    Enter your number:
    Your result is:-858993457
    Для продолжения нажмите любую клавишу . . .

    вот сама программа:

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

    1. Аватар Александр:

      Вы пытаетесь прочитать целое число, а вводите дробное…

  4. Аватар Константин:

    #include <Здравствуй, Юра>
    Подготавливая к представлению 1\10 в десятичной системе я слева пишу "1", далее слева на право ставлю уголок, справа вверху пишу "10", снизу уголка записываю результаты деления… В итоге вашему вниманию представлено число 0.1! А как в двоичной системе происходит подготовка числа к представлению?
    Ещё вопрос — в десятичной системе правило округления, например, 1.1 .2 .3 .4 округляется до 1, а 1.5 .6 .7 .8 .9 округляются до 2. В С++ тоже есть правила округления или лишнее просто отсекается?
    #endif

  5. Аватар Ярослав:

    А зачем нули в конце числа (уже после запятой и после остальных цифр, например, как последние 2 нуля в 0.0078900) оставлять? В математике-то их же отбрасывают обычно (то есть до 0.00789)

    1. Аватар Елена:

      Ноль тоже считается, так как 34.50 было плавающим числом. Поэтому 3.450e1, а не 3.45e1.

      1. Аватар S:

        Хахаха,умно конечно…(паходу типер я никрапастер*sad*)

  6. Аватар Виктор:

    В тесте задание "a", разве крайний правый значащий? В ответе написано 4 значащих цифры, но ведь их 3

  7. Аватар Андрей:

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

  8. Аватар Алибек:

    а в чем разница между int и long если оба 4 байта

    1. Юрий Юрий:

      Отличаются тем, что имеют разный размер на разных ОС/платформах. Т.е. по факту, int должен был бы занимать 2 байта, а long — 4, но в системе Windows они ничем не отличаются, так как оба занимают 4 байта. Однако если вы будете разрабатывать кроссплатформенную программу или приложение, то лучше использовать long вместо int, если требуется больший диапазон значений.

  9. Аватар Sergey Groysman:

    Юрий, прошу прощения, что "разбросал" вопросы, а не собрал вместе.
    Программа понимает только ввод "правильных" чисел или при вводе можно ввести 3е3 вместо 3000? Просто у меня при вводе экспоненциальной записи числа получается какая-то ерунда, программа её не понимает.
    Спасибо.

    1. Юрий Юрий:

      Вводить числа с указанием буквы «e» можно, только эта буква «e» пишется на английской раскладке. Если вы пишете «е» с русской раскладки, то получите ошибку.

      Ничего, так даже лучше (насчет разбросанных вопросов). Пожалуйста 🙂

  10. Аватар Sergey Groysman:

    Юрий, ещё раз здравствуйте.
    Я написал простую программу:

    поскольку переменные простые, то для проверки я ввёл: 3000000000 (3е9) для первого числа и на второе уже места не осталось. Просто вышла какая-то ерунда.
    Но я решил изменить "размер" места под большие числа и немного переделал программу:

    программа отладку прошла, но проблема осталась. Где я не правильно сделал (возможно не доделал) или я тогда не совсем понял, ГДЕ мы указываем переменным "размер" оперируемыми числами.
    Спасибо.

    1. Юрий Юрий:

      Привет. По порядку.

      1. Переменная int, скорее всего, на вашем компьютере занимает 4 байта (у меня так). Диапазон для 4 байтов — от -2 147 483 648 до 2 147 483 647 (из урока 31). Вы ввели 3 000 000 000 — это превышает допустимый диапазон для значений типа int, поэтому вы и получили ошибку.

      2. Тип long также занимает 4 байта, соответственно, диапазон тот же, что и для int. Тип long long занимает 8 байтов, его и следовало бы использовать. Диапазон long long — от -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807.

      3. Во второй программе вам нужно использовать тип long long и указывать его не только в main() при определении переменных x и y, но и для функций readNumber() и writeAnswer, а то получится, что указали long long для x и y, а функция, которая принимает данные от пользователя и затем возвращает их — имеет тип long (произойдет ошибка, так как число обрежется самой функцией).

      Рабочая программа:

  11. Аватар Sergey Groysman:

    Юрий, доброго дня.
    Подскажите, если для хранения переменных целочисленной и с плавающей точкой достаточно 4 байт и разбег чисел соответственно от+2е9 до — 2е9, то наиболее часто встречающиеся и используемые команды long и float, правильно? если уж понадобятся "запредельные" цифры то long long или doble (long double)?
    И ещё, а они не сокращаются например до f для float или l для long, например f x; при присвоении переменной с плавающей точкой?
    Спасибо.

    1. Юрий Юрий:

      Привет, Сергей.

      1. Наиболее часто встречающийся целочисленный тип — int. Тип с плавающей запятой — double. Но это ведь не значит, что нужно использовать только эти типы данных. Для каждой задачи отдельно подбирается наиболее подходящий тип.

      2. Если понадобятся запредельные цифры, то да — long long для целочисленного типа и long double для типа с плавающей запятой.

      3. Нет, типы данных не сокращаются по умолчанию вообще. Но вы можете использовать псевдонимы для типов (детальнее об этом в уроке 60).

      Пожалуйста 🙂

  12. Аватар aleksandr:

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

    1. Юрий Юрий:

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

  13. Аватар Виталий:

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

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

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