Урок №36. Типи даних з плаваючою крапкою: float, double і long double

  Юрій  | 

  Оновл. 27 Кві 2020  | 

 209

У цьому уроці ми розглянемо типи даних з плаваючою крапкою, їх точність і діапазон, що таке експоненціальний запис і як він використовується, а також розглянемо помилки округлення, що таке nan і inf.

Типи даних з плаваючою крапкою

Цілочисельні типи даних відмінно підходять для роботи з цілими числами, але є ще і дробові числа. І для роботи з ними ми можемо використовувати типи даних з плаваючою крапкою (або ще “типи даних з плаваючою комою“, англ. “floating point“). Змінна такого типу може зберігати будь-які дійсні дробові числа, наприклад: 4320.0, -3.33 чи 0.01226. Чому крапка «плаваюча»? Справа в тому, крапка/кома переміщається (ніби «плаває») між цифрами, розділяючи цілу і дробову частини значення.

Є три типи даних з плаваючою крапкою: float, double і long double. Як і з цілочисельними типами, C++ визначає тільки їх мінімальний розмір. Типи даних з плаваючою крапкою завжди є signed (тобто можуть зберігати як додатні, так і від’ємні числа).

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

Оголошення змінних різних типів даних з плаваючою крапкою:

Якщо потрібно використовувати ціле число зі змінною типу з плаваючою крапкою, то тоді потрібно вказати після розділової крапки нуль. Це дозволяє розрізняти змінні цілочисельних типів від змінних типів з плаваючою крапкою:

Зверніть увагу, літерали типу з плаваючою крапкою за замовчуванням відносяться до типу double. "f" в кінці числа означає тип float.

Експоненціальний запис

Експоненціальний запис дуже корисний для написання довгих чисел в короткій формі. Числа в експоненціальному запису мають вигляд мантиса х 10експонент. Наприклад, розглянемо вираз 1.2×104. Значення 1.2 — це мантиса (або ще “значуща частина числа”), а 4 — це експонент (або ще “порядок числа“). Результатом цього виразу є значення 12000.

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

Розглянемо масу Землі. У десятковій системі числення вона представлена ​​як 5973600000000000000000000 кг. Погодьтеся, дуже велике число (навіть занадто велике, щоб поміститися в цілочисельну змінну розміром 8 байт). Це число навіть важко читати (там 19 чи 20 нулів?). Але, використовуючи експоненціальний запис, масу Землі можна представити як 5.9736×1024кг (що набагато легше сприймається, погодьтеся). Ще однією перевагою експоненціального запису є порівняння двох дуже великих або дуже маленьких чисел — для цього досить просто порівняти їх експоненти.

У C++ буква е /Е означає, що число 10 потрібно піднести до степеня, який слідує за цією буквою. Наприклад, 1.2×104 еквівалентно 1.2e4, значення 5.9736×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 Зірок (2 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

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