Тип с плавающей точкой (floating point type) хранит вещественное число как мантиссу и показатель степени — отсюда и «плавающая» точка: одно и то же количество бит обслуживает и 0.001, и 10¹⁵, но с разной абсолютной точностью.

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

ТипОбычноЗначащих десятичных цифр
float4 байта~7
double8 байт~15–16
long double10–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 уже соседние.