1. Навіщо потрібні дійсні числа
Якщо досі ми жили у світі цілих чисел, то тепер виходимо в реальність, де буває 2.5 кілометра, 36.6 градуса, 0.75 коефіцієнта, 12.5 % знижки та інші «нецілі штуки». Дійсні типи в C++ саме для цього й призначені. Вони схожі на int тим, що теж дають змогу зберігати числа й виконувати арифметичні дії, але відрізняються обмеженою точністю. І це важливо розуміти заздалегідь.
Дійсні числа використовують тоді, коли важливі масштаб і дробова частина, а не точність «до останньої копійки». Наприклад, якщо ви обчислюєте середню швидкість, середню оцінку, відсоток прогресу, довжину, час, температуру чи ймовірності, — це майже завжди double.
Почнімо з дуже простого прикладу: якщо ми хочемо ділити «по-людськи», int швидко перестає бути зручним.
#include <iostream>
int main() {
int minutes = 5;
int people = 2;
int perPersonInt = minutes / people; // 2 (дріб загубився)
double perPersonDouble = minutes / people; // 2.0 (але це все одно int/int!)
std::cout << perPersonInt << '\n'; // 2
std::cout << perPersonDouble << '\n'; // 2
}
Зверніть увагу: навіть double праворуч не рятує, якщо вираз залишається цілочисельним. Чому так, розберемо далі.
2. float і double: чим вони відрізняються
float і double — це дійсні типи, але вони мають різну точність і зазвичай різний розмір у памʼяті. І хоча стандарт C++ залишає деякі деталі на розсуд реалізації, у реальному житті на більшості платформ це відповідає IEEE-754 (у документах стандарту C++ це зазвичай повʼязують із ISO/IEC/IEEE 60559).
Якщо говорити грубо, але практично, то float — це «трохи швидше й менше, але з гіршою точністю», а double — це «памʼяті трохи більше, зате жити зазвичай спокійніше». У навчальних і в багатьох реальних проєктах правило просте: за замовчуванням обираємо double, а float беремо лише тоді, коли чітко розуміємо навіщо.
Нижче — зручна табличка «на пальцях» (цифри типові для звичайних ПК і ноутбуків, але залежать від процесора):
| Тип | Зазвичай розмір | Зазвичай точність (десяткові цифри) | Коли застосовувати |
|---|---|---|---|
|
4 байти | ~6–7 цифр | графіка, великі масиви, коли важлива економія памʼяті |
|
8 байтів | ~15–16 цифр | обчислення, формули, середні значення, «звичайне життя» |
Перевірити характеристики на своїй платформі можна за допомогою std::numeric_limits. Це корисно, щоб не гадати й не сперечатися з однокурсником про те, «а скільки там цифр».
#include <iostream>
#include <limits>
int main() {
std::cout << std::numeric_limits<float>::digits10 << '\n'; // зазвичай 6
std::cout << std::numeric_limits<double>::digits10 << '\n'; // зазвичай 15
}
3. Літерали й тип виразів
Літерали: чому 3.14 — це double, а 3.14f — float
Тут новачки часто потрапляють у маленьку пастку: ви пишете float x = 3.14; і думаєте, що «3.14 — це float». Але за замовчуванням дійсний літерал на кшталт 3.14 — це double. А float-літерал задають суфіксом f або F.
Це не просто формальність: коли типи змішуються, компілятор виконує перетворення, і ви можете отримати або попередження, або зайву втрату точності, або просто нерозуміння того, що саме відбувається.
Подивімося на невеликому прикладі:
#include <iostream>
int main() {
float a = 1.0f / 3.0f; // float / float -> float (приблизно 0.33333334)
double b = 1.0 / 3.0; // double / double -> double
std::cout << a << '\n';
std::cout << b << '\n';
}
Ще один корисний запис — наукова нотація. Її читають як «помножити на 10 у степені».
#include <iostream>
int main() {
double tiny = 1e-3; // 0.001
double huge = 2e6; // 2000000
std::cout << tiny << '\n'; // 0.001
std::cout << huge << '\n'; // 2e+06 або 2000000 (залежить від виводу)
}
Тип результату задають операнди
Одна з ключових ідей тут така: тип результату виразу визначається тим, які типи стоять в самому виразі. Не зліва, не тим, «куди присвоюємо», а саме операндами, які беруть участь в операції.
Це особливо наочно видно під час ділення. Якщо ви ділите int / int, то отримаєте int (тобто дробова частина відкинеться), навіть якщо потім запишете результат у double.
#include <iostream>
int main() {
int a = 5;
int b = 2;
double x1 = a / b; // 2.0 (спочатку int/int -> 2)
double x2 = static_cast<double>(a) / b; // 2.5 (double/int -> double)
std::cout << x1 << '\n'; // 2
std::cout << x2 << '\n'; // 2.5
}
Є й другий спосіб «зробити вираз дійсним» — використати дійсний літерал:
#include <iostream>
int main() {
int sum = 5;
int count = 2;
double avg = sum / 2.0; // 2.5, тому що 2.0 — double
std::cout << avg << '\n';
}
Важлива звичка: коли ви бачите ділення, запитайте себе: «які типи мають операнди?». Це вбереже вас від величезної кількості дивних результатів.
Мінісхема: як не втратити дріб
Щоб закріпити правило про тип виразу, зручно тримати в голові просту схему.
Є ділення / ?
|
v
Обидва операнди int?
|
так ------------------> результат int (дріб відріжеться)
|
ні
|
v
Хоча б один операнд double/float?
|
так ------------------> результат дійсний (double/float)
Якщо ви хочете отримати дробовий результат, зробіть так, щоб хоча б один операнд був дійсним: 2.0, 0.1, static_cast<double>(x).
4. Похибка та її накопичення
Чому десяткові дроби «ламаються»
Дійсні числа в памʼяті компʼютера зберігаються не як «ідеальні математичні дроби», а як двійкове наближення. І тут маємо неприємну, але цілком чесну реальність: багато десяткових дробів у двійковій системі неможливо подати точно.
Найвідоміший приклад — 0.1. У десятковій системі це «одна десята», а в двійковій — нескінченний дріб (як 1/3 у десятковій: 0.3333333...). Тому компʼютер зберігає «найближче можливе значення». Зазвичай воно настільки близьке, що вас це не турбує… доки ви не почнете порівнювати через == або багато разів повторювати ту саму операцію.
Мінідемонстрація:
#include <iostream>
int main() {
double a = 0.1;
double b = 0.2;
double c = a + b;
std::cout << c << '\n'; // часто друкується як 0.3
std::cout << (c - 0.3) << '\n'; // але тут може бути щось на кшталт 5.55112e-17
}
Якщо ви бачите, що число «гарно вивелося» як 0.3, це не означає, що всередині воно «точно 0.3». Формат виведення може округлити число для друку. Саме значення в памʼяті залишається таким, яким було.
Чому в циклах похибка стає помітнішою
Одна маленька неточність зазвичай не страшна. Але якщо ви багато разів повторюєте операцію, наприклад додаєте 0.1 у циклі, то можете накопичити похибку настільки, що вона стане помітною. Це не «помилка вашого коду», а особливість обчислень з обмеженою точністю.
Особливо часто це проявляється в симуляціях, в обчисленнях за кроками часу та в накопиченні суми з великої кількості дробових доданків.
#include <iostream>
int main() {
float sf = 0.0f;
double sd = 0.0;
for (int i = 0; i < 10; ++i) {
sf += 0.1f;
sd += 0.1;
}
std::cout << (sf - 1.0f) << '\n'; // може бути не 0
std::cout << (sd - 1.0) << '\n'; // теж може бути не 0, але зазвичай менше за модулем
}
На практиці це одна з причин, чому з double часто спокійніше: він зазвичай має вищу точність, тому «тремтіння» проявляється пізніше й слабше.
5. Мінізастосунок: рахуємо темп і обираємо double
Зараз ми додамо дійсні числа в наш консольний застосунок: маленький калькулятор темпу бігу. Ідея дуже проста: користувач вводить дистанцію в кілометрах і час у хвилинах, а програма виводить темп — «хвилин на кілометр».
Важливо, що дистанція може бути дробовою: 2.5 км, 7.42 км тощо. Отже, int не підходить — беремо double.
#include <iostream>
int main() {
double distanceKm = 0.0;
int timeMinutes = 0;
std::cin >> distanceKm >> timeMinutes;
double pace = timeMinutes / distanceKm; // int/double -> double
std::cout << pace << '\n';
}
Тепер навмисно розгляньмо «антиприклад»: якби ми намагалися зберігати все в int, то довелося б або округляти дистанцію, або зберігати «метри» як ціле число. А це вже інший підхід — іноді правильний, але не сьогодні. У цій лекції нам важливо побачити: double робить задачу природною і простою.
Ще один корисний момент: якщо ви захочете обчислювати середній час на кілометр за кількома пробіжками, то майже напевно прийдете до підсумовування дробів і середніх значень. І знову double буде зручнішим варіантом за замовчуванням.
6. Чому частіше обирають double
Дуже хочеться мати «магічну кнопку» для вибору типу. В ідеалі — натиснули double, і все завжди правильно. Але так не буває. Зате є практичне правило, яке напрочуд часто спрацьовує: якщо ви не займаєтеся графікою, задачами для мобільних GPU або величезними масивами чисел, — беріть double.
Причини тут доволі прості. По-перше, точності float часом справді бракує, і помилки проявляються в найнесподіваніших місцях: значення «пливуть», порівняння на межі поводяться дивно, сума з багатьох дробів починає розходитися. По-друге, вартість double за швидкістю на більшості звичайних CPU сьогодні зазвичай не виглядає як «катастрофа», особливо для навчальних задач і більшості прикладних обчислень.
Коли float виправданий, це зазвичай повʼязано з тим, що у вас дуже багато чисел — мільйони чи десятки мільйонів, — і памʼять або кеш стають вузьким місцем. Або ви працюєте з API чи форматами, які вимагають саме float. У таких випадках float — чудовий інструмент. Але це вже усвідомлений вибір, а не «я просто так написав».
7. Типові помилки під час роботи з float і double
Помилка № 1: очікувати, що double x = a / b; дасть дріб, якщо a і b — int.
Це класика: змінна зліва дійсна, а результат однаково «цілий». Причина в тому, що ділення відбувається як int / int, і лише потім результат перетворюється на double. Виправити це просто: зробіть вираз дійсним — через 2.0 або static_cast<double>(a).
Помилка № 2: писати float x = 1.0 / 3.0; і думати, що обчислення будуть float.
Літерали 1.0 і 3.0 — це double, отже обчислення відбувається в double, а потім результат уже стискається до float. Якщо ви справді хочете float-арифметику (а частіше — ні), використовуйте 1.0f і 3.0f.
Помилка № 3: порівнювати результати обчислень через == і дивуватися «дивностям».
Після операцій із дробами число може зберігатися як «майже 0.3», але не рівно. Тому == інколи дає false, хоча за змістом ви очікуєте true. Ми докладно навчимося порівнювати дійсні числа коректно в наступній лекції про epsilon, а поки запамʼятайте: точна рівність для результатів обчислень — рідкісний гість.
Помилка № 4: думати, що «якщо виведення гарне, значить число точне».
std::cout часто друкує число округлено, щоб не лякати користувача. Це не гарантує точності зберігання. Ви можете побачити 0.3, а всередині буде значення, дуже близьке до 0.3, але не рівне йому «побітово».
Помилка № 5: бездумно обирати float, бо «він менший».
Так, він менший, але й точність у нього нижча. Для більшості навчальних і прикладних задач double — безпечніший вибір. float варто брати тоді, коли є причина: вимоги API, дуже великі масиви даних або чітке розуміння допустимої похибки.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ