Тип с плавающей точкой (floating point type) хранит вещественное число как мантиссу и показатель степени — отсюда и «плавающая» точка: одно и то же количество бит обслуживает и 0.001, и 10¹⁵, но с разной абсолютной точностью.
Между нулём и единицей бесконечно много действительных чисел, а в фиксированном количестве бит — конечное число комбинаций. Значит, точно представить каждое действительное число невозможно в принципе: эти типы дают конечное множество значений, остальное приближается.
| Тип | Обычно | Значащих десятичных цифр |
|---|---|---|
float | 4 байта | ~7 |
double | 8 байт | ~15–16 |
long double | 10–16 байт | ~18–19 (зависит от платформы) |
По умолчанию берут double: у float точности хватает редко, а выигрыш
в памяти на одиночных переменных незаметен. Размер и возможности long double
целиком на совести реализации — считать его всегда шестнадцатибайтовым нельзя,
и более длинное название типа само по себе не превращает приближённую
арифметику в точную.
Суффиксы литералов
0.1 // double — по умолчанию
0.1F // float
0.1L // long doubleСуффиксы пишем заглавными, см. Литералы. Суффикса lf в C++ нет —
это спецификатор формата из printf.
Отсюда типичная нескладица: double x = 0.0f; — литерал сначала создаётся как
float, потом расширяется до double. Работает, но сообщает читателю неправду
о типе. Пишется просто double x = 0.0;.
Точность — главное, что нужно помнить
Дробные значения хранятся в двоичной системе, и большинство привычных десятичных дробей в ней непредставимы точно — как 1/3 непредставима в десятичной записи.
#include <iomanip>
#include <iostream>
int main() {
double sum = 0.1 + 0.2;
std::cout << std::setprecision(17) << sum << '\n'; // 0.30000000000000004
std::cout << (sum == 0.3) << '\n'; // 0
return 0;
}Отсюда правило: вещественные числа не сравнивают через ==. Сравнивают
модуль разности с допуском:
#include <cmath>
constexpr double kEpsilon = 1e-9;
if (std::abs(a - b) < kEpsilon) {
// считаем равными
}Величина допуска — не универсальная константа: для чисел порядка миллиона
1e-9 бессмысленно мал. Допустимая ошибка определяется задачей и масштабом
величин — иногда нужен абсолютный допуск, иногда относительный, иногда оба.
Формулировка, которую стоит запомнить точно: проблема не в том, что double
запрещено сравнивать на равенство, а в том, что результат приближённого
вычисления нельзя без обоснования считать точным. Слева в 0.1 + 0.2 == 0.3
складываются приближения к 0.1 и 0.2, и сумма тоже округляется; справа лежит
приближение к 0.3. Совпасть эти два пути не обязаны, а == честно проверяет
точное равенство того, что получилось.
Какие операции вообще есть
Сложение, вычитание, умножение, деление. Побитовые операции и % для дробных
типов не определены, а типа unsigned double в C++ не существует.
Взаимодействие с целыми
Присваивание double в int отбрасывает дробную часть (усечение к нулю,
не округление) и требует явного static_cast — иначе компилятор справедливо
предупреждает о потере данных:
double value = 2.9;
int truncated = static_cast<int>(value); // 2, а не 3Округление по правилам математики — std::round, std::floor, std::ceil
из <cmath>.
Тонкости
- Деление на ноль здесь не роняет программу, как у целых:
при арифметике IEEE 754 получаются
infиnan. Стандарт C++ этого не гарантирует, но на учебных платформах будет именно так. Переносить увиденный результат1.0 / 0.0на целочисленное1 / 0нельзя — там UB. nanне равен ничему, включая самого себя:x != x— способ его поймать.- Сложение чисел очень разного порядка теряет младшие разряды: прибавить
1e-10к1e10— всё равно что не прибавлять. - Порядок сложения влияет на результат:
(a + b) + cиa + (b + c)могут отличаться в последних разрядах.
Частые ошибки
floatтам, где нужна точность: накопленная ошибка вылезает уже на нескольких тысячах операций.- Сравнение через
==и удивление, почему условие не срабатывает. - Ожидание, что
static_cast<int>округлит, — он отбрасывает.
Углубление — АиСД L02
На АиСД дробные числа появились в бинарном поиске по ответу: корень ищут делением отрезка пополам, см. Бинарный поиск. На лекции было два варианта остановки:
float L = 0, R = N float L = 0, R = N
eps = 10^-5 for i = 0 ... 1000
пока (|L*L - N| > eps) { M = (L + R) / 2
M = (L + R) / 2 если M*M < N
если M*M < N L = M
L = M иначе
иначе R = M
R = M
}Правый вариант подписан на лекции как «гарантия количества итераций».
Не из лекции: почему вариант с eps опасен — проверено
Это та же история, что с допуском выше: он должен соответствовать масштабу чисел. У
floatоколо 7 значащих цифр. Рядом с числом 100 соседние значенияfloatотстоят примерно на , рядом с — на . Отрезок сжимается до двух соседнихfloat,Lперестаёт меняться, а так и остаётся больше — цикл не кончается.Прогон на GCC 16: при N = 2 и N = 10 вариант с eps останавливается за 19–22 итерации, при N = 100 и зависает. Вариант с
forостанавливается всегда. Из его 1000 итераций что-то меняют только первые 24–34, дальшеLиRуже соседние.