JavaRush /Курси /C++ SELF /Неявні перетворення: змішування int і double

Неявні перетворення: змішування int і double

C++ SELF
Рівень 6 , Лекція 4
Відкрита

1. Внутрішній тип обчислень виразу

Якщо ви лише починаєте, легко міркувати так: «Ну, числа й числа. Яка різниця: int чи double? Усе одно ж 7». На практиці різниця величезна. Тип впливає на те, як обчислюється вираз, які значення можливі і що може бути втрачено під час запису результату.

Неявні перетворення (implicit conversions) — це правила, за якими компілятор сам перетворює один тип на інший, щоб операція мала сенс. Це трохи схоже на автоперекладач у месенджері: зручно, доки він не «виправить» вашу думку на щось дивне. Тому наша мета сьогодні — навчитися передбачати, коли компілятор «перекладає» типи, у якому напрямку він це робить і де через це виникають помилки.

Коли ви пишете:

double x = 5 / 2;

Новачок часто уявляє це так: «Зліва double, отже й ділення буде дробовим». Але компілятор мислить інакше. Спочатку він обчислює те, що справа, за правилами типів справа, отримує результат, а вже потім намагається записати його в змінну зліва. А запис — це вже окреме перетворення.

Щоб не плутатися, корисно тримати в голові такий конвеєр:

flowchart TD
    A[Операнди виразу] --> B[Промоушени: малі типи піднімаються]
    B --> C[Звичайні арифм. перетворення: обирається спільний тип]
    C --> D[Обчислення операції]
    D --> E[Присвоювання: результат приводиться до типу змінної зліва]

І ось тут важливий психологічний момент: тип змінної зліва не «керує» обчисленням справа. Він впливає лише на фінальний запис результату.

2. Промоушени: піднімаємо малі типи

Промоушен — це автоматичне «підвищення» типу до зручнішого або стандартного для обчислень. Найпоширеніший випадок, який ви вже, ймовірно, бачили, навіть якщо не усвідомлювали цього: char в арифметиці поводиться як число. Це не магія і не підступ — це якраз промоушени.

У стандарті C++ поняття integral promotions (цілочисельних промоушенів) — окрема важлива частина правил. Навіть у редакторських правках до робочих чернеток видно, що формулювання й терміни навколо promotions опрацьовують дуже уважно.

Приклад: char перетворюється на число

Проведімо невеликий експеримент:

#include <iostream>

int main() {
    char c = 'A';
    int next = c + 1;

    std::cout << next << '\n'; // 66 (якщо 'A' = 65 в ASCII)
}

Ви могли очікувати «B», але отримали число. Чому? Тому що вираз c + 1 — це арифметика, а в арифметиці char зазвичай автоматично підвищується до int. Тому й результат теж стає цілим числом.

Якщо ж вам потрібен саме символ, слід чітко розуміти, що саме ви робите. Але явні приведення типів ми свідомо залишимо на наступні етапи курсу.

Приклад: bool в арифметиці — це теж число

Звучить трохи як жарт, але в обчисленнях true часто поводиться як 1, а false — як 0.

#include <iostream>

int main() {
    bool paid = true;
    int total = 100 + paid;

    std::cout << total << '\n'; // 101
}

Це працює, але зазвичай для читабельності це погана ідея. Внутрішньо компілятор упорається, а от людина, яка читатиме код, — зокрема й ви самі за тиждень, — навряд чи буде в захваті.

Звичайні арифметичні перетворення: як обирається спільний тип

Коли в операції беруть участь два числа різних типів, компілятор має вирішити: «У якому типі виконувати операцію?» Цей блок правил часто називають usual arithmetic conversions («звичайні арифметичні перетворення»). У матеріалах про робочі чернетки навіть окремо відзначали додавання перехресних посилань на цей термін, тож тема справді фундаментальна.

Для нашого поточного рівня достатньо запамʼятати просте практичне правило:

Якщо у виразі бере участь double, то майже завжди обчислення переходять у double, щоб не втратити дробову частину.

Тобто:

  • int + int int
  • int + double double
  • int / int int (навіть якщо потім ви запишете результат у double)
  • double / int double

3. Змішування int і double

Пастка № 1: «я записав у double, отже буде дріб» — ні

Ось демонстрація, яка спершу збиває з пантелику майже всіх. І це нормально:

#include <iostream>

int main() {
    int sum = 5;
    int count = 2;

    double avg = sum / count; // обидва int -> ділення int
    std::cout << avg << '\n'; // 2
}

Чому 2, а не 2.5? Тому що sum / count обчислюється як int / int, тобто результат — ціле число, і дробова частина відкидається. А вже потім це ціле число спокійно записується в double як 2.0. Жодної помилки — просто не те, чого ви очікували.

Прийом № 1: як зробити ділення дробовим

Найпростіший спосіб на нашому рівні, без явного приведення типів, — зробити так, щоб хоча б один операнд був double. Наприклад, написати 2.0 замість 2.

#include <iostream>

int main() {
    int sum = 5;
    int count = 2;

    double avg = sum / 2.0;     // тепер вираз у double
    std::cout << avg << '\n';   // 2.5
}

Тут компілятор бачить int / double, приводить int до double, обчислює вираз у double й отримує дробовий результат.

Пастка № 2: «тиха» втрата даних під час double int

Коли ви присвоюєте дробове число цілій змінній, дробова частина просто зникає:

#include <iostream>

int main() {
    double price = 19.99;
    int euros = price;

    std::cout << euros << '\n'; // 19
}

Це не округлення, а саме відкидання дробової частини. Компілятор не зобовʼязаний рятувати вас. Для нього це цілком допустиме перетворення: «Ви так написали — я так і зробив».

Мінітаблиця-памʼятка

Іноді корисно мати коротку «карту місцевості», щоб не гадати.

Вираз Типи операндів Тип обчислення Приклад результату
7 / 2
int і int
int
3
7 / 2.0
int і double
double
3.5
2 + 0.5
int і double
double
2.5
double x = 7 / 2;
справа int/int справа
int
, зліва
double
3.0
int x = 7 / 2.0;
справа є double справа
double
, зліва
int
3

Тут добре видно головне: спочатку обчислюється права частина, а вже потім результат підганяється під тип зліва.

4. Мінікалькулятор чека: вбудовуємо тему в застосунок

Щоб тема не залишилася відірваною від практики, продовжимо розвивати умовний застосунок, з яким працюємо впродовж курсу: консольний «мінітермінал касира». Він не претендує на бухгалтерську точність — справжні гроші краще зберігати не в double, але це вже окрема розмова. Зате такий приклад ідеально показує типові помилки новачків.

Перший крок: обчислюємо середню ціну товару й ловимо помилку

Ми зчитуємо кількість товарів і загальну суму. Кількість — ціле число, сума — дробове.

#include <iostream>

int main() {
    int items = 0;
    double total = 0.0;

    std::cin >> items >> total;

    double avg = total / items;
    std::cout << avg << '\n'; // наприклад: 12.5
}

Тут усе добре, тому що totaldouble, отже ділення виконується в double.

А тепер подивімося на «поганий» варіант — саме той, який часто пишуть за звичкою:

#include <iostream>

int main() {
    int items = 0;
    int total = 0;

    std::cin >> items >> total;

    double avg = total / items;
    std::cout << avg << '\n'; // якщо total=25 items=2 -> 12
}

Якщо total = 25, а items = 2, то «справжня» середня ціна дорівнює 12.5, але ви отримаєте 12. І проблема тут не в тому, що double avg «поганий», а в тому, що total / items обчислилося як цілочисельне ділення.

Виправлення без приведення типів: робимо один операнд double

Якщо за змістом сума може бути дробовою, чесніше одразу зберігати її в double. Але навіть якщо у вас сума ціла, наприклад у євро без центів, ви все одно можете хотіти отримати дробове середнє значення. Тоді достатньо додати до виразу double:

#include <iostream>

int main() {
    int items = 0;
    int total = 0;

    std::cin >> items >> total;

    double avg = total / (items * 1.0);
    std::cout << avg << '\n'; // якщо total=25 items=2 -> 12.5
}

Зверніть увагу: (items * 1.0) стає double, отже й total / double теж стає double. Так, це виглядає трохи «хитро», але на нашому етапі це цілком нормальний прийом, доки ми не вивчили явні приведення типів.

Додаємо знижку у відсотках: знову змішуємо int і double

Знижку часто вводять як ціле число відсотків, а от підсумкова сума зазвичай дробова.

#include <iostream>

int main() {
    double total = 0.0;
    int discountPercent = 0;

    std::cin >> total >> discountPercent;

    double discount = total * discountPercent / 100.0;
    double finalTotal = total - discount;

    std::cout << finalTotal << '\n'; // наприклад: 899.1
}

Тут важливо, що / 100.0 — це ділення в double. Якби було / 100, то total * discountPercent уже мало б тип double, і ділення все одно виконувалося б як double / int, тобто теж у double. Але 100.0 робить намір максимально очевидним: ми працюємо з дробами.

5. Як думати про типи у формулах

Дуже корисна звичка: коли ви пишете формулу, подумки ставте собі два запитання.

Перше: «Де в цій формулі має зʼявитися дробова частина?» Якщо відповідь: «Має», перевірте, чи є у виразі хоча б один double. Якщо ні, то майже напевно ви випадково виконуєте цілочисельну арифметику.

Друге: «Куди я записую результат?» Якщо ви записуєте його в int, то маєте бути морально готові до того, що дробова частина зникне. Іноді це нормально, наприклад якщо ви зберігаєте «цілу кількість людей», а іноді це вже помилка, наприклад якщо йдеться про ціну.

6. Типові помилки

Помилка № 1: очікувати дробовий результат від int / int, бо зліва стоїть double.
Це одна з найпоширеніших логічних пасток. Ділення визначається типами операндів у виразі справа, а не типом змінної зліва. Тому double x = 5 / 2; дає 2, а не 2.5. Уникнути цього просто: якщо очікуєте дріб, забезпечте участь double безпосередньо у виразі.

Помилка № 2: намагатися виправити проблему дужками, коли насправді річ у типах.
Дужки змінюють порядок обчислень, але не змінюють тип операндів. Можна ідеально розставити дужки й однаково отримати неправильний результат через цілочисельне ділення. У таких ситуаціях потрібно змінювати не структуру формули, а типи даних або хоча б один літерал, наприклад 2.0 замість 2.

Помилка № 3: непомітно втратити дробову частину під час присвоювання double у int.
int a = 19.99; перетворюється на 19 без урахування того, важлива дробова частина чи ні. Компілятор вважає це допустимим перетворенням. Якщо дробова частина важлива, зберігайте значення в double. Якщо ні, краще будувати обчислення так, щоб це було очевидно з коду. Наприклад, округляти або обрізати значення свідомо. Але про це ми говоритимемо пізніше, коли зʼявляться додаткові інструменти.

Помилка № 4: використовувати char і bool в арифметиці «випадково».
char і bool легко потрапляють у вирази: додали прапорець, порахували символ як число — і раптом отримали загадковий результат. Формально це пояснюється промоушенами, але на практиці допомагає проста дисципліна: арифметика має виконуватися над числами, які ви справді вважаєте числами за змістом задачі, а не над типами «для тексту» чи «для логіки».

Помилка № 5: писати формули так, що читачеві незрозуміло, у якому типі відбувається розрахунок.
Навіть якщо компілятор усе порахує «як ви хотіли», людина може прочитати формулу інакше. Тому іноді краще написати 100.0 замість 100 або одразу зберігати суму в double, щоб формула читалася однозначно. Це не про «красу», а про зменшення кількості майбутніх помилок.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ