JavaRush /Курсы /C++ SELF /Форматирование вывода: fixe...

Форматирование вывода: fixed, setprecision, setw

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

1. Введение

Если вы только начали программировать, кажется, что std::cout << x; — это честная печать числа. На практике это больше похоже на «как принтеру сегодня настроение подскажет». То 0.333333, то 0.33333333333333331, то внезапно 1e+06. Компьютер не врёт — он просто не обязан быть удобным для чтения. Мы же хотим, чтобы вывод был стабильным, аккуратным и одинаковым: в задачах, в отчётах, в табличках и в логах.

Представьте, что числа — это люди на фото для паспорта. Можно сфотографировать «как получилось»: кто-то моргнул, кто-то повернулся боком. А можно сделать нормально: свет, фон, одинаковый размер. Форматирование — это как раз «паспортный стол» для ваших double.

Начнём с мини-демонстрации проблемы: вывод «как есть» часто выглядит слишком по-разному.

#include <iostream>

int main() {
    double a = 1.0 / 3.0;
    double b = 1234567.0;
    double c = 0.0000123;

    std::cout << a << '\n'; // например: 0.333333
    std::cout << b << '\n'; // например: 1.23457e+06
    std::cout << c << '\n'; // например: 1.23e-05
}

Печать «по умолчанию» старается быть универсальной, но универсальность часто означает «неудобно для конкретной задачи».

2. Манипуляторы <iomanip>: настройка потока std::cout

Перед тем как накидывать fixed и setw, важно понять идею. std::cout — это не просто «функция печати», а объект-поток, у которого есть внутренние настройки: точность, формат (обычный/фиксированный/научный), выравнивание, ширина поля и так далее. Манипуляторы из <iomanip> — это такие «переключатели», которые вы отправляете в поток через <<, и поток запоминает режим печати (иногда надолго, иногда на один вывод).

Чтобы пользоваться форматированием, нам почти всегда нужен заголовок:

#include <iomanip>

Минимальная «рамка» для сегодняшней лекции такая:

#include <iostream>
#include <iomanip>

int main() {
    double x = 1.0 / 3.0;
    std::cout << std::fixed << std::setprecision(2) << x << '\n'; // 0.33
}

Идея манипулятора выглядит так: вы будто говорите std::cout: «Слушай, давай дальше печатай в фиксированном формате и с двумя знаками после точки». И он послушается.

Небольшая схема (просто чтобы мозгу было спокойнее), что происходит при выводе:

flowchart LR
    A[double value] --> B[std::cout]
    C[манипуляторы iomanip] --> B
    B --> D[текст в консоли]

И важный нюанс: некоторые манипуляторы «прилипают» к потоку, а некоторые действуют только на следующий вывод. Мы это сегодня обязательно потрогаем руками.

std::fixed: фиксированный формат вместо «как получится»

По умолчанию std::cout иногда выбирает научную запись (1.23e+06), особенно для очень больших или очень маленьких чисел. Это честно и компактно, но человеку, который ждёт «обычное число», становится слегка тревожно: «это точно не ошибка?». Манипулятор std::fixed заставляет поток печатать число в фиксированном десятичном виде. Это особенно полезно для денег, измерений, процентов и всего, где вы хотите видеть привычную запись.

Посмотрим на контраст:

#include <iostream>
#include <iomanip>

int main() {
    double x = 1234567.0;

    std::cout << x << '\n';                 // например: 1.23457e+06
    std::cout << std::fixed << x << '\n';   // 1234567.000000
}

У fixed есть побочный эффект: он начинает печатать «после точки» какое-то количество знаков (по умолчанию обычно 6). Поэтому fixed почти всегда идёт в паре с setprecision, иначе вы получите «шесть нулей», которые выглядят как бухгалтерия без чувства меры.

std::setprecision(n): точность и типичная ловушка

С std::setprecision(n) новички почти всегда попадают в одну и ту же ловушку: думают, что n — это «знаки после точки». Это верно только вместе с std::fixed. Без fixed setprecision(n) означает «количество значащих цифр» (то есть цифры вообще, включая до точки).

Сначала посмотрим setprecision без fixed:

#include <iostream>
#include <iomanip>

int main() {
    double x = 1234.56789;

    std::cout << std::setprecision(3) << x << '\n'; // например: 1.23e+03 или 1230 (зависит от вывода)
}

А теперь то же самое, но с fixed:

#include <iostream>
#include <iomanip>

int main() {
    double x = 1234.56789;

    std::cout << std::fixed << std::setprecision(3) << x << '\n'; // 1234.568
}

С fixed — это «3 знака после точки», и это обычно именно то, что нужно для табличек и отчётов.

Отдельно подчеркну важную мысль: форматирование не меняет значение переменной. Оно меняет только то, как число выглядит в тексте. То есть x остаётся тем же double, просто «одевается приличнее».

std::setw, std::left, std::right: ширина поля и выравнивание

Когда вы печатаете несколько строк с числами, очень быстро появляется желание сделать «как в табличке». В одну колонку — название, в другую — число. Если печатать без форматирования, колонки «плывут»: у кого-то число длиннее, у кого-то короче, и всё превращается в «кривой забор».

std::setw(w) задаёт ширину поля для следующего элемента вывода. Если строка/число короче, оно дополняется пробелами. По умолчанию выравнивание — вправо (для чисел это обычно удобно).

Ключевой момент: setw действует только один раз, на следующий вывод.

#include <iostream>
#include <iomanip>

int main() {
    std::cout << std::setw(10) << 5 << '\n';     // "         5"
    std::cout << 5 << '\n';                      // "5" (setw уже не действует)
}

std::left и std::right задают выравнивание внутри поля. И вот они как раз обычно «прилипают» к потоку, пока вы не переключите обратно.

#include <iostream>
#include <iomanip>

int main() {
    std::cout << std::left << std::setw(10) << "Cat" << "|\n";   // "Cat       |"
    std::cout << std::right << std::setw(10) << "Cat" << "|\n";  // "       Cat|"
}

Это особенно полезно в таблицах: текст — слева, числа — справа.

Что «липнет», а что «одноразовое»

С форматированием есть типичный психологический сюрприз: вы добавили setw(10), а потом удивляетесь, почему следующая строка не выровнялась. Или вы однажды включили fixed, а потом через 200 строк кода ловите странный вывод — потому что забыли, что fixed всё ещё включён.

Чтобы не превращать код в детектив, полезно запомнить простую модель поведения.

std::setw(w) действует только на следующий выводимый элемент. То есть его надо ставить перед каждой колонкой, если вы хотите таблицу. Это похоже на «ширина ячейки для следующего значения».

std::fixed, std::setprecision(n), std::left и std::right обычно остаются активными, пока вы их не поменяете. Это похоже на «режим печати по умолчанию для этого потока».

Небольшая демонстрация, чтобы почувствовать:

#include <iostream>
#include <iomanip>

int main() {
    double x = 1.0 / 3.0;

    std::cout << std::fixed << std::setprecision(2) << x << '\n'; // 0.33
    std::cout << x << '\n';                                       // 0.33 (режим всё ещё тот же!)
}

Если вы хотите, чтобы в середине программы форматирование было разным в разных местах, старайтесь включать манипуляторы «рядом с печатью», чтобы читатель кода не искал, где поток «переодели».

3. Табличный вывод в мини-приложении CircleLab

Сейчас сделаем то, ради чего всё затевалось: аккуратный табличный отчёт. Пусть наше мини-приложение CircleLab печатает таблицу для радиусов 1..5: радиус, длина окружности и площадь круга.

Почему это хорошая демонстрация: числа получаются «разной длины», и без форматирования таблица развалится. А ещё это классический сценарий, где fixed + setprecision(2) — почти «закон жанра».

Сначала — наивный вывод (чтобы увидеть боль):

#include <iostream>

int main() {
    const double pi = 3.141592653589793;

    for (int r = 1; r <= 5; ++r) {
        double c = 2.0 * pi * r;
        double s = pi * r * r;

        std::cout << r << " " << c << " " << s << '\n';
    }
}

Обычно это будет выглядеть как «три числа через пробел», но без визуальной структуры. Исправляем: подключаем <iomanip> и включаем форматирование.

Фиксируем формат и точность для чисел

В табличных отчётах почти всегда хочется одинаковое количество знаков после точки. Сделаем 2 знака — как у денег (и вообще это хороший компромисс для демонстрации).

#include <iostream>
#include <iomanip>

int main() {
    const double pi = 3.141592653589793;

    std::cout << std::fixed << std::setprecision(2);

    for (int r = 1; r <= 3; ++r) {
        double c = 2.0 * pi * r;
        double s = pi * r * r;

        std::cout << r << " " << c << " " << s << '\n';
    }
}

Да, теперь числа красивее, но колонки всё ещё не колонки: если радиусы станут двузначными, всё «поедет». Значит, пора setw.

Делаем заголовок и столбцы через setw

Сделаем три колонки: r, circumference, area. Текст выровняем влево, числа вправо. И добавим линию-разделитель (чисто для радости глаз).

#include <iostream>
#include <iomanip>

int main() {
    std::cout << std::left
              << std::setw(6)  << "r"
              << std::setw(16) << "C"
              << std::setw(16) << "S" << '\n';

    std::cout << std::string(38, '-') << '\n'; // --------------------------------------
}

Здесь появляется std::string, которую мы уже проходили раньше (строки). Мы создаём строку из 38 дефисов, чтобы отделить заголовок от данных.

Теперь добавим строки данных. Важно: для чисел переключим выравнивание на right, а для заголовка оставим left. Переключение можно сделать прямо перед печатью чисел.

#include <iostream>
#include <iomanip>
#include <string>

int main() {
    const double pi = 3.141592653589793;

    std::cout << std::left
              << std::setw(6)  << "r"
              << std::setw(16) << "C"
              << std::setw(16) << "S" << '\n';
    std::cout << std::string(38, '-') << '\n';

    std::cout << std::fixed << std::setprecision(2);

    for (int r = 1; r <= 5; ++r) {
        double c = 2.0 * pi * r;
        double s = pi * r * r;

        std::cout << std::right
                  << std::setw(6)  << r
                  << std::setw(16) << c
                  << std::setw(16) << s << '\n';
    }
}

Теперь это реально похоже на отчёт. И, что важно, таблица будет выглядеть одинаково при каждом запуске (если числа одинаковые), а не «как повезёт».

4. Табличный вывод как интерфейс: приёмы читаемости

Когда вы делаете таблицу, вы фактически проектируете маленький пользовательский интерфейс в консоли. И у него есть те же проблемы, что и у настоящего UI: если всё пляшет, пользователю тяжело. Если столбцы ровные и предсказуемые — считывается мгновенно.

Два коротких приёма часто делают консольный вывод «дороже» на вид (в хорошем смысле).

Первый приём — печатать заголовок и разделитель. Это помогает глазу понять структуру, особенно когда строк много.

Второй приём — одинаковое число знаков после точки (fixed + setprecision). Это создаёт визуальную сетку: десятичные точки стоят примерно на одной вертикали, и сравнивать значения проще.

Например, вот минимальный «табличный кадр», который удобно копировать в разные программы:

#include <iostream>
#include <iomanip>

int main() {
    std::cout << std::left << std::setw(12) << "name"
              << std::right << std::setw(12) << "value" << '\n';

    std::cout << std::fixed << std::setprecision(2);
    std::cout << std::left << std::setw(12) << "pi"
              << std::right << std::setw(12) << 3.1415926 << '\n'; // pi          3.14
}

Обратите внимание: мы выравниваем текст влево, число вправо — это «классика жанра». Если сделать наоборот, таблица будет выглядеть странно, как будто числа — это подписи, а подписи — это числа.

5. Типичные ошибки

Ошибка №1: забыли подключить <iomanip> и удивляются, почему fixed и setprecision «не существуют».
Это одна из самых частых ситуаций: вы честно написали std::setprecision(2), а компилятор отвечает, что такого имени не знает. Манипуляторы форматирования живут в <iomanip>, и без этого заголовка они недоступны. Привыкайте воспринимать заголовки как «контракт»: хотите инструменты форматирования — подключите правильный набор.

Ошибка №2: считать, что setprecision(2) всегда означает «2 знака после точки».
Так работает только вместе с std::fixed. Без fixed setprecision задаёт число значащих цифр, и результат может внезапно уйти в научную запись или «обрезать» число не там, где вы ожидали. Если делаете таблицу или отчёт, почти всегда пишите связку std::fixed << std::setprecision(n).

Ошибка №3: ждать, что std::setw(w) действует «на всю строку дальше».
setw — одноразовый: он применяется только к следующему элементу вывода. В таблице это означает, что ширину нужно задавать для каждой колонки. Иначе получится первая колонка ровная, а остальные — как получится, потому что setw уже «потратился».

Ошибка №4: однажды включили fixed/setprecision/left и забыли, что поток живёт дальше в этом режиме.
Манипуляторы вроде fixed, setprecision, left/right часто «прилипают» к std::cout. Из-за этого форматирование начинает влиять на печать в неожиданных местах программы. Практичная привычка: задавайте форматирование рядом с тем выводом, где оно важно, и не рассчитывайте, что «где-то выше уже настроено».

Ошибка №5: думать, что форматирование меняет значение переменной.
Иногда после std::setprecision(2) студент ожидает, что число «округлится» и в памяти станет короче. Но форматирование — это только внешний вид текста. Сам double остаётся тем же приближением, с теми же погрешностями. Если нужно именно изменить значение (например, округлить математически), это уже отдельная операция и отдельная тема, а не задача iostream.

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