Содержание
Эта программа выводит «не равны»:
#include <iostream>
int main() {
double a = 0.1 + 0.2;
if (a == 0.3) {
std::cout << "равны\n";
} else {
std::cout << "не равны\n";
}
return 0;
}
Это не ошибка компилятора и не особенность C++. Так же ведут себя Python, Java, JavaScript и почти любой другой язык: все они хранят дробные числа одинаково. В статье — откуда берётся расхождение и как писать программы, которым оно не мешает.
Что хранится на самом деле
Попросим вывести числа с семнадцатью значащими цифрами:
#include <iomanip>
#include <iostream>
int main() {
std::cout << std::setprecision(17);
std::cout << 0.1 << '\n';
std::cout << 0.1 + 0.2 << '\n';
std::cout << 0.3 << '\n';
return 0;
}
0.10000000000000001
0.30000000000000004
0.29999999999999999
В переменной типа double лежит не 0,1, а ближайшее к нему число, которое можно записать в двоичном виде. Оно чуть больше одной десятой. Сумма двух таких приближений оказалась чуть больше 0,3, а само приближение числа 0,3 — чуть меньше. Отсюда «не равны».
Причина та же, по которой 1/3 нельзя точно записать десятичной дробью: 0,333… бесконечна. В двоичной системе конечной записи нет у большинства привычных десятичных дробей: 0,1, 0,2, 0,3, 0,7. А вот 0,5, 0,25 и 0,75 записываются точно, и для них 0.5 + 0.25 == 0.75 истинно. Подробно о том, как устроена такая запись, рассказывает урок учебника «Дробные числа: почему 0,1 + 0,2 не равно 0,3».
Обычный вывод эту разницу скрывает. По умолчанию std::cout показывает шесть значащих цифр, и оба числа выглядят как 0.3. Программа сравнивает одно, а Вы видите другое.
Пять ловушек
1. Сравнение через ==
Самая частая. Два числа, которые «должны быть равны», почти никогда не равны побитно, если хотя бы одно из них получено вычислением.
2. Цикл с дробным шагом
int steps = 0;
for (double x = 0.0; x < 1.0; x += 0.1) {
++steps;
}
std::cout << steps << '\n'; // 11
Ожидается десять шагов, выполняется одиннадцать. После десяти сложений x равен не 1, а 0,99999999999999989, и условие x < 1.0 ещё истинно.
С условием x != 1.0 цикл не остановится вообще: значение 1,0 будет «перепрыгнуто».
Надёжный вариант — целый счётчик, из которого вычисляется дробное значение:
for (int i = 0; i < 10; ++i) {
double x = i * 0.1;
// ...
}
3. Отбрасывание дробной части
int percent = 0.29 * 100;
std::cout << percent << '\n'; // 28
Произведение равно 28,999999999999996. Преобразование в int не округляет, а отбрасывает дробную часть — и получается 28. Когда нужно ближайшее целое, используйте std::lround:
#include <cmath>
long percent = std::lround(0.29 * 100); // 29
4. Накопление ошибки
double total = 0.0;
for (int i = 0; i < 100; ++i) {
total += 0.01;
}
// total == 1.0000000000000007
Каждое сложение добавляет крошечную погрешность. Сто сложений — и сумма уже отличается от единицы в шестнадцатом знаке. Миллион сложений сдвинут ошибку на несколько знаков ближе к запятой.
5. Большое плюс маленькое
double big = 1e16;
std::cout << (big + 1.0 == big) << '\n'; // 1, то есть «равны»
У double около пятнадцати значащих десятичных цифр. Число 10^16 занимает их все, и для единицы места не остаётся: она теряется при сложении. У типа float цифр всего шесть-семь, и там это случается гораздо раньше:
float f = 16777217.0f;
std::cout << std::setprecision(10) << f << '\n'; // 16777216
Как сравнивать правильно
Вместо «равны ли» спрашивайте «достаточно ли близки»:
#include <algorithm>
#include <cmath>
bool almost_equal(double a, double b, double eps = 1e-9) {
return std::fabs(a - b) <= eps * std::max({1.0, std::fabs(a), std::fabs(b)});
}
Функция сравнивает разность с допуском, который растёт вместе с самими числами. Для чисел около единицы допуск равен eps, для миллиардов — пропорционально больше. С ней almost_equal(0.1 + 0.2, 0.3) возвращает true.
В учебных и олимпиадных задачах числа обычно небольшие, и хватает простой формы:
if (std::fabs(a - b) < 1e-9) {
// считаем равными
}
Допуск выбирают по задаче. Если в условии сказано «ответ с точностью до 10^-6», берите на два-три порядка меньше: 1e-9.
То же относится к сравнениям «меньше» и «больше» на границе. Условие x <= 1.0 лучше писать как x < 1.0 + eps, если x получен вычислением и случай равенства важен.
Как считать деньги
Никогда не храните деньги в double. Копейка не представима точно, и погрешности рано или поздно дадут неверный итог или неверное сравнение.
Храните сумму в наименьших единицах — копейках — в целом типе:
long long price_kopecks = 19999; // 199 рублей 99 копеек
long long total = price_kopecks * 3; // 59997 — точно
std::cout << total / 100 << " руб. " << total % 100 << " коп.\n";
Целые числа складываются и умножаются без погрешности. Это правило шире денег: всё, что считается штуками, храните в целых.
Как выводить
#include <iomanip>
double average = 4.0 / 3.0;
std::cout << std::fixed << std::setprecision(2) << average << '\n'; // 1.33
std::fixed вместе с std::setprecision(2) дают ровно два знака после запятой с округлением. Без std::fixed число в setprecision означает количество значащих цифр, а не знаков после запятой.
Если проверяющая система требует точность 10^-6, выводите с запасом, например девять знаков. Вывод по умолчанию — шесть значащих цифр — для чисел больше десяти даст меньше знаков после запятой, чем нужно.
double или float
В учебных задачах берите double. Он занимает 8 байт и хранит около 15 значащих цифр. float занимает 4 байта и хранит 6–7 цифр: погрешность в нём заметна уже на числах в десятки миллионов. float нужен там, где важна память или скорость на миллионах значений, — в графике и обработке звука.
Пять правил
- Не сравнивайте дробные числа через
==и!=. Сравнивайте разность с допуском. - Счётчик цикла — целый. Дробное значение вычисляйте из него.
- Нужно целое из дробного — округляйте
std::lround, а не преобразованием вint. - Деньги и всё, что считается штуками, храните в целых числах.
- При выводе задавайте точность явно:
std::fixed << std::setprecision(n).
Погрешность double — не дефект, а цена компактного хранения. Тип, который помнит пятнадцать цифр и работает с числами от атомных размеров до галактических, не может при этом быть точным в каждой десятичной дроби. Достаточно помнить об этом и не требовать от него лишнего.
Что ещё может сделать ответ программы неожиданным, рассказывает статья «Мусор в переменных и неопределённое поведение в C++», а как типы данных устроены в языке — урок «Типы данных и их модификаторы».
Источники
- What Every Computer Scientist Should Know About Floating-Point Arithmetic — David Goldberg [Электронный ресурс]. — URL: https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html (дата обращения: 10.10.2026).
- Floating-point types — cppreference.com [Электронный ресурс]. — URL: https://en.cppreference.com/w/cpp/types/floating-point (дата обращения: 10.10.2026).
- std::numeric_limits::epsilon — cppreference.com [Электронный ресурс]. — URL: https://en.cppreference.com/w/cpp/types/numeric_limits/epsilon (дата обращения: 10.10.2026).



