JavaRush /Курсы /C++ SELF /Дробные числа: float

Дробные числа: float vs double

C++ SELF
10 уровень , 1 лекция
Открыта

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 берём только когда понимаем, зачем.

Ниже — удобная табличка «на пальцах» (цифры типичные для обычных ПК/ноутбуков, но зависят от процессора):

Тип Обычно размер Обычно точность (десятичные цифры) Когда применять
float
4 байта ~6–7 цифр графика, большие массивы, когда важна память
double
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.14ffloat

Здесь у новичков часто случается «мини‑баг в голове»: вы пишете 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 и bint.
Это классика: переменная слева вещественная, а результат всё равно «целый». Причина в том, что деление происходит как 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, очень большие массивы данных или явное понимание допустимой погрешности.

1
Задача
C++ SELF, 10 уровень, 1 лекция
Недоступна
Беговой темп
Беговой темп
1
Задача
C++ SELF, 10 уровень, 1 лекция
Недоступна
Процент прогресса
Процент прогресса
1
Задача
C++ SELF, 10 уровень, 1 лекция
Недоступна
Память измерений
Память измерений
1
Задача
C++ SELF, 10 уровень, 1 лекция
Недоступна
Выбор формата
Выбор формата
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ