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

Форматування виведення: 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] --> 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
Опитування
Дійсні числа та точність, рівень 10, лекція 5
Недоступний
Дійсні числа та точність
Дійсні числа та точність
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ