Л2. Дані, вирази та вибір: умови й межі
Коротко про лекцію
Розберемо, як типи впливають на арифметику, як програма зберігає результат порівняння та як умова обирає одну з можливих дій. Окремо перевіримо точні межі правил.
У Л1 ми навчилися зберігати числа, будувати прості вирази й перевіряти результат ручним розрахунком. Тепер програма має навчитися приймати рішення. Перед цим треба точніше зрозуміти, як C++ обчислює вирази.
Практичний сенс. Помилка у типі або межі умови часто дає правдоподібний результат. Тому програміст перевіряє зміст виразу до запуску та тестує значення біля меж.
1. Типи впливають на результат виразу
1.1. Цілочисельне ділення
Нехай тривалість дорівнює 135 хвилинам. Треба отримати повні години та залишок хвилин.
135 / 60 = 2 повні години
135 % 60 = 15 хвилин
Коли обидва значення у діленні мають тип int, оператор / виконує цілочисельне ділення. Дробова частина результату відкидається.
Оператор % повертає остачу від ділення. Для додатних значень 135 % 60 дорівнює 15.
#include <iostream>
int main() {
const int total_minutes{135};
const int full_hours{total_minutes / 60};
const int remaining_minutes{total_minutes % 60};
std::cout << full_hours << " год "
<< remaining_minutes << " хв\n";
}
Результат описує час як 2 год 15 хв. Тут ми свідомо не обчислюємо десяткові години, бо для цього треба спочатку ввести перетворення типу.
1.2. Явне перетворення типу
Вираз total_minutes / 60 містить два значення типу int. Тому C++ спочатку отримує цілий результат.
Іноді потрібно тимчасово подати ціле значення як double. Така дія називається явним перетворенням типу. У C++ для цього використаємо запис static_cast<double>(...).
Наприклад:
static_cast<double>(total_minutes) / 60.0
Запис static_cast<double>(total_minutes) створює значення типу double для цього виразу. Початкова змінна total_minutes лишається типу int.
Наступний приклад показує типову помилку середнього значення.
#include <iostream>
int main() {
const int points_a{4};
const int points_b{3};
const int sum{points_a + points_b};
const double wrong_average{sum / 2};
const double correct_average{static_cast<double>(sum) / 2.0};
std::cout << "Без перетворення: " << wrong_average << "\n";
std::cout << "Після перетворення: " << correct_average << "\n";
}
Сума 7 і дільник 2 мають тип int. Вираз 7 / 2 дає 3, а потім це число записується у double як 3.0. Тип змінної-приймача не повертає вже втрачену дробову частину.
flowchart TD
A["sum = 7, тип int"] --> B{"Як виконуємо ділення?"}
B -->|"sum / 2"| C["int / int"]
C --> D["результат 3"]
D --> E["у double записується 3.0"]
B -->|"перетворили sum"| F["double / double"]
F --> G["результат 3.5"]
1.3. Інші поширені числові типи
int і double залишаються основними числовими типами цього курсу. У реальному C++ трапляються й інші фундаментальні числові типи. Їх треба впізнавати під час читання коду.
Цілі типи утворюють кілька поширених варіантів:
| Тип | Практичний зміст на цьому етапі | Приклад |
|---|---|---|
short |
ціле для невеликого діапазону; точний розмір залежить від платформи | short delta{-2}; |
int |
звичайний цілий тип для лічильників і кількостей | int students{28}; |
long |
ціле, розмір якого залежить від платформи | long code{380L}; |
long long |
ціле зі знаком щонайменше на 64 біти | long long views{3'000'000'000LL}; |
unsigned int |
невід’ємне ціле з іншими правилами арифметики біля нуля | unsigned int attempts{3U}; |
Для більшості звичайних цілих задач починайте з int. Тип long long доречний, коли потрібний значно більший діапазон зі знаком. unsigned int не варто вибирати механічно лише через невід’ємний зміст даних.
Дробові типи також мають кілька варіантів:
| Тип | Практичний зміст на цьому етапі | Приклад |
|---|---|---|
float |
дробове число з меншою точністю | float temperature{21.5F}; |
double |
основний дробовий тип для звичайних обчислень | double price{58.25}; |
long double |
дробовий тип із точністю не меншою за double |
long double ratio{2.5L}; |
Для навчальних обчислень курс за замовчуванням використовує double. Тип float важливо впізнавати, бо він часто трапляється в коді та форматах даних. Точні розміри й межі цих типів залежать від реалізації, тому їх не вгадують за назвою.
Суфікси в літералах роблять потрібний тип явним. F позначає float, LL — long long, U позначає беззнаковий цілий тип, а L після дробового літерала — long double.
#include <iostream>
int main() {
const short floor_delta{-2};
const int students{28};
const long legacy_code{380L};
const long long views{3'000'000'000LL};
const unsigned int attempts{3U};
const float temperature{21.5F};
const double price{58.25};
const long double ratio{2.5L};
std::cout << floor_delta << ' ' << students << ' ' << legacy_code << ' '
<< views << ' ' << attempts << ' ' << temperature << ' '
<< price << ' ' << ratio << "\n";
}
Цей приклад потрібний для читання типів, а не для запам’ятовування їхніх максимальних значень. Під час вибору типу спочатку визначайте зміст даних і потрібний діапазон.
1.4. Порядок обчислення
Множення, ділення та % виконуються раніше за додавання й віднімання. Дужки дозволяють явно показати потрібний порядок.
const double total{price * quantity + delivery};
Спочатку обчислюється price * quantity. Потім додається delivery.
Формулу з кількома змістовними етапами краще поділити на проміжні значення:
const double subtotal{price * quantity};
const double total{subtotal + delivery};
Такий запис полегшує читання й пошук першої неправильної величини.
1.5. Іменовані сталі замість прихованих чисел
Умови задачі часто містять числа з окремим змістом. Наприклад, 1000 може означати межу безкоштовної доставки. Якщо записати це число прямо в кількох формулах, його роль доведеться відновлювати з контексту.
const double free_delivery_from{1000.0};
const double regular_delivery_fee{80.0};
Імена показують, яку роль виконує кожне значення. Вони також спрощують зміну правила. Якщо межа стане 1200, достатньо змінити одне визначене значення.
Число, записане без пояснення ролі, часто називають магічним числом. На початку курсу корисно ставити просте питання: чи зрозуміє читач призначення цього числа без умови задачі? Якщо ні, значенню варто дати ім’я.
1.6. Дані можуть надходити зі стандартного вводу
У попередніх прикладах початкові значення записані прямо в коді. Такий підхід зручний для трасування, бо однаковий запуск отримує однакові дані. Консольна програма також може читати значення, які вводить користувач.
Стандартний вхідний потік C++ має ім’я std::cin. Оператор >> читає наступне значення відповідно до типу змінної.
Термін: std::cin
std::cin — стандартний вхідний потік C++. Запис std::cin >> value читає значення з консолі у вже створену змінну.
Мінімальна повна програма може одразу попросити значення у користувача. Після натискання «Запустити» поле вводу з’явиться в області результату. Введіть 19 або інше ціле число й натисніть Enter.
#include <iostream>
int main() {
int age{0};
std::cout << "Введіть вік: ";
std::cin >> age;
std::cout << "\nВи ввели: " << age << "\n";
}
Цей приклад використовує штатний стандартний ввід C++. Код не підставляє значення через JavaScript і не змінюється для браузера. Після читання age містить введене ціле число, якщо формат був коректним. Перевірку помилок вводу детально розглянемо разом із потоками в Л10.
2. Порівняння дає логічне значення
2.1. Тип bool
До цього моменту ми зберігали числа у int і double. Для рішення потрібен інший вид даних: відповідь на запитання «так чи ні?».
C++ має для цього тип bool. Він зберігає лише два логічні значення:
Термін: bool
bool зберігає логічну відповідь. Його значення true та false представляють істину й хибу умови.
true— умова істинна;false— умова хибна.
Наприклад, змінна може прямо зберігати готову відповідь:
const bool is_weekend{false};
Порівняння чисел теж дає результат типу bool. Тому можна записати:
const double subtotal{940.0};
const bool free_delivery{subtotal >= 1000.0};
Порівняння subtotal >= 1000.0 дає false, бо 940 менше за 1000. Отже free_delivery зберігає готову логічну відповідь.
| Оператор | Питання |
|---|---|
< |
ліве значення менше? |
<= |
менше або дорівнює? |
> |
більше? |
>= |
більше або дорівнює? |
== |
значення рівні? |
!= |
значення різні? |
Оператор = виконує присвоєння. Оператор == виконує порівняння. Один пропущений символ змінює зміст інструкції.
2.2. Іменована умова
Складну умову легше читати, коли її назва пояснює зміст.
const bool free_delivery{subtotal >= free_delivery_from};
Тепер if (free_delivery) читається як коротке правило задачі.
2.3. Результат порівняння теж можна зберігати
Порівняння є звичайним виразом із результатом true або false. Його можна обчислити один раз, дати результату ім’я та використати пізніше.
const bool free_delivery{subtotal >= free_delivery_from};
Такий запис розділяє два питання. Спочатку програма визначає, чи виконано правило. Потім інша інструкція вирішує, що робити з цим результатом.
Для простого правила окрема змінна bool не завжди потрібна. Вона корисна, коли ім’я робить умову зрозумілішою або результат перевіряється кілька разів.
3. if виконує дію за умовою
Нехай доставка коштує 80 грн, але стає безкоштовною від 1000 грн.
| Сума товарів | Вартість доставки |
|---|---|
менше 1000 |
80 |
від 1000 включно |
0 |
Це таблиця рішень. Вона показує гілки правила до написання коду.
flowchart TD
A["Маємо subtotal"] --> B{{"subtotal >= 1000?"}}
B -- "так" --> C["delivery_fee = 0"]
B -- "ні" --> D["delivery_fee = 80"]
C --> E["total = subtotal + delivery_fee"]
D --> E
3.1. Перша умовна інструкція
У C++ умовна інструкція починається словом if. Після нього в круглих дужках записують логічну умову. Блок у фігурних дужках виконується лише тоді, коли умова має значення true.
Мінімальна форма виглядає так:
if (condition) {
// Цей код виконується лише коли condition має значення true.
}
Тепер використаємо цю форму у повній програмі.
#include <iostream>
int main() {
const double subtotal{940.0};
const double free_delivery_from{1000.0};
const bool free_delivery{subtotal >= free_delivery_from};
double delivery_fee{80.0};
if (free_delivery) {
delivery_fee = 0.0;
}
const double total{subtotal + delivery_fee};
std::cout << "Доставка: " << delivery_fee << " грн\n";
std::cout << "Разом: " << total << " грн\n";
}
if перевіряє free_delivery. Блок у фігурних дужках виконується тільки для true.
Для subtotal = 940 умова хибна. delivery_fee лишається 80, тому загальна сума дорівнює 1020.
3.2. Дві гілки через else
Коли обидва результати мають окремі дії, використовуємо if разом з else. Слово else читаємо як «інакше»: його блок виконується, коли умова if хибна.
double delivery_fee{0.0};
if (subtotal >= free_delivery_from) {
delivery_fee = 0.0;
} else {
delivery_fee = 80.0;
}
Після такого блока виконується рівно одна гілка.
3.3. Після гілки виконання продовжується
if не завершує програму сам по собі. Після обраної гілки виконання переходить до наступної інструкції за всім блоком.
if (subtotal >= free_delivery_from) {
delivery_fee = 0.0;
} else {
delivery_fee = 80.0;
}
const double total{subtotal + delivery_fee};
До обчислення total програма приходить з одним із двох можливих значень delivery_fee. Тому важливо перевірити, що кожна гілка залишає потрібні дані у коректному стані.
flowchart TD
A["перевірити умову"] --> B{"умова?"}
B -- "так" --> C["гілка if"]
B -- "ні" --> D["гілка else"]
C --> E["наступна спільна інструкція"]
D --> E
4. Межа входить до умови
4.1. Різниця між > і >=
Граничне значення належить правилу лише тоді, коли оператор порівняння це дозволяє.
Межа умови
Для правила з >= перевіряйте значення перед межею, на межі та після неї. Така трійка швидко виявляє помилку > замість >=.
Правило говорить: «безкоштовна доставка від 1000 грн включно». Отже значення 1000 належить безкоштовній гілці.
#include <iostream>
int main() {
const double subtotal{1000.0};
const double free_delivery_from{1000.0};
double delivery_fee{80.0};
if (subtotal >= free_delivery_from) {
delivery_fee = 0.0;
}
std::cout << delivery_fee << "\n";
}
Умова subtotal > free_delivery_from помилково зробила б доставку платною для суми рівно 1000.
4.2. Три тести біля межі
subtotal |
Очікувана доставка | Що перевіряємо |
|---|---|---|
999.99 |
80 |
значення трохи нижче межі |
1000.00 |
0 |
точну межу |
1000.01 |
0 |
значення трохи вище межі |
Така трійка швидко викриває плутанину між > і >=.
flowchart LR
A["999.99"] --> B["платна доставка"]
C["1000.00"] --> D["безкоштовна доставка"]
E["1000.01"] --> D
5. Кілька порівнянь в одній умові
5.1. Оператор &&
Нехай комфортна температура лежить від 18 до 24 градусів включно.
Потрібно одночасно виконати дві умови: температура не нижча за мінімум і не вища за максимум. Для логічного і C++ використовує оператор &&. Результат дорівнює true лише тоді, коли обидві частини істинні.
Тепер можна записати умову:
const bool comfortable{
temperature >= min_comfort && temperature <= max_comfort
};
#include <iostream>
int main() {
const double temperature{22.0};
const double min_comfort{18.0};
const double max_comfort{24.0};
const bool comfortable{
temperature >= min_comfort && temperature <= max_comfort
};
if (comfortable) {
std::cout << "Комфортний діапазон\n";
} else {
std::cout << "Поза діапазоном\n";
}
}
Для 22 обидві частини істинні. Для 17 хибна нижня межа. Для 25 хибна верхня межа.
5.2. Оператор ||
Пільга може діяти для дітей молодших 12 років або людей від 65 років.
Для логічного або C++ використовує оператор ||. Результат дорівнює true, коли істинна хоча б одна частина.
const bool has_discount{age < 12 || age >= 65};
5.3. Оператор !
Оператор ! змінює логічне значення на протилежне.
const bool valid{quantity > 0};
if (!valid) {
std::cout << "Некоректна кількість\n";
}
Довгі умови краще розкладати на змістовні логічні значення. Так простіше перевіряти кожну частину окремо.
5.4. Як читати складену умову
Не намагайтеся охопити довгий логічний вираз одним поглядом. Спочатку обчисліть кожне порівняння окремо. Потім з’єднайте отримані true і false логічним оператором.
Для температури 22 отримаємо:
22 >= 18 → true
22 <= 24 → true
true && true → true
Для температури 25 друга частина стане false, тому весь вираз із && теж дасть false. Такий покроковий розбір корисний під час налагодження складених умов.
A |
B |
A && B |
A || B |
|---|---|---|---|
false |
false |
false |
false |
false |
true |
false |
true |
true |
false |
false |
true |
true |
true |
true |
true |
6. Кілька гілок через else if
Двох гілок інколи недостатньо. Після if можна додати else if — «інакше перевір ще одну умову». Наприкінці можна залишити else для всіх випадків, що не пройшли попередні перевірки.
Перевірки йдуть зверху вниз. Виконується перша гілка, умова якої стала true.
Нехай квиток має три тарифи:
| Вік | Ціна |
|---|---|
до 12 |
50 грн |
від 12 до 17 |
80 грн |
від 18 |
120 грн |
#include <iostream>
int main() {
int age{0};
double price{0.0};
std::cout << "Вік: ";
std::cin >> age;
// Перевіряємо вікові межі знизу вгору, щоб кожне значення потрапило лише в одну групу.
if (age < 12) {
price = 50.0;
} else if (age < 18) {
price = 80.0;
} else {
price = 120.0;
}
std::cout << "\nЦіна: " << price << " грн\n";
}
Введіть по черзі 11, 12, 17 і 18. Для 12 перша умова хибна, а age < 18 істинна. Тому ціна дорівнює 80 грн. Чотири запуски показують обидві межі без редагування коду.
Після виконання однієї гілки решта ланцюжка пропускається. Для цього правила ключові тести: 11, 12, 17 і 18 років.
6.1. Порядок умов має значення
У ланцюжку if → else if перевірки йдуть зверху вниз. Виконується перша гілка з істинною умовою. Решта умов після неї вже не перевіряються.
Тому зручно розташовувати вікові межі від меншої до більшої:
age < 12
інакше age < 18
інакше дорослий тариф
У другій умові вже відомо, що age >= 12. Через це не потрібно повторювати повний діапазон age >= 12 && age < 18.
flowchart TD
A["age"] --> B{"age < 12?"}
B -- "так" --> C["50 грн"]
B -- "ні" --> D{"age < 18?"}
D -- "так" --> E["80 грн"]
D -- "ні" --> F["120 грн"]
6.2. Умовний оператор ?: обирає одне з двох значень
Іноді умова визначає одне значення, яке треба записати у змінну. Для такого короткого вибору C++ має умовний оператор ?:. Його також називають тернарним оператором, бо він працює з трьома операндами.
Загальна форма читається зліва направо:
умова ? значення_якщо_true : значення_якщо_false
Спочатку обчислюється умова. Якщо вона дає true, результатом стає другий операнд. Для false результатом стає третій операнд. Обчислюється лише одна з двох альтернатив.
Наприклад, вартість доставки можна одразу вибрати за сумою товарів:
const double delivery_fee{subtotal >= 1000.0 ? 0.0 : 80.0};
Цей запис виражає вибір одного значення. Кілька послідовних дій у кожній гілці краще залишати у звичайному if / else. Так межа між обчисленням значення та керуванням діями залишається видимою.
#include <iostream>
int main() {
const double subtotal{940.0};
const double regular_delivery_fee{80.0};
const double delivery_fee{subtotal >= 1000.0 ? 0.0 : regular_delivery_fee};
std::cout << "Доставка: " << delivery_fee << " грн\n";
}
Для subtotal = 940.0 умова хибна, тому програма вибирає regular_delivery_fee. Результат виразу дорівнює 80.0, і саме це значення отримує delivery_fee.
Умовний оператор корисний також для безпечного вибору виразу. Наприклад, divisor != 0 ? dividend / divisor : 0 не обчислює ділення, коли divisor дорівнює нулю. Такий запис варто застосовувати лише тоді, коли обидві альтернативи лишаються короткими й читабельними.
6.3. switch зручно читається для фіксованих варіантів
Ланцюжок if добре працює з діапазонами та складеними умовами. Інша ситуація виникає, коли одна змінна може мати кілька наперед відомих кодів. C++ має для такого вибору інструкцію switch.
switch один раз обчислює вираз і порівнює його з мітками case. Інструкція break завершує вибрану гілку. Мітка default обробляє значення, для якого окремого case немає.
Термін: switch
switch вибирає гілку за одним дискретним значенням. Він доречний для фіксованих кодів або станів, а діапазони природніше описувати через if.
#include <iostream>
int main() {
int delivery_method{0};
std::cout << "Спосіб доставки (1 або 2): ";
std::cin >> delivery_method;
std::cout << "\n";
switch (delivery_method) {
case 1:
std::cout << "Самовивіз\n";
break;
case 2:
std::cout << "Кур'єр\n";
break;
// Значення поза меню теж має отримати визначену відповідь.
default:
std::cout << "Невідомий спосіб\n";
break;
}
}
Запустіть програму з 1, 2, а потім із 7. Перші два значення вибирають конкретні case, а 7 переходить до default. Так одна програма дозволяє перевірити всі три маршрути без редагування коду.
Типова помилка: пропущений break
Без break виконання може перейти до наступної мітки case. У початковому коді курсу кожна завершена гілка switch матиме явний break.
7. Обмеження вхідних даних
Умова задачі має визначати допустимі значення. Кількість товарів, наприклад, повинна бути додатною.
#include <iostream>
int main() {
const int quantity{0};
if (quantity <= 0) {
std::cout << "Некоректна кількість\n";
} else {
std::cout << "Кількість прийнято\n";
}
}
quantity = 0 не відповідає правилу «додатна кількість». Програма має обробити такий випадок явно, якщо він може потрапити на вхід.
Перед написанням умови сформулюйте:
- Які значення допустимі?
- Де проходять точні межі?
- Що робить програма для недопустимого значення?
7.1. Спочатку перевіряємо область допустимих даних
Якщо значення недопустиме, подальша класифікація може втратити сенс. Наприклад, від’ємний вік не треба передавати в тарифні гілки.
if (age < 0) {
std::cout << "Некоректний вік\n";
} else if (age < 12) {
std::cout << "Дитячий тариф\n";
} else {
std::cout << "Інший тариф\n";
}
Так структура коду повторює структуру правила: спочатку відсіюємо недопустимі дані, потім працюємо з допустимими.
8. Як перевіряти рішення
8.1. Нормальний випадок
Значення лежить далеко від межі. Для доставки це може бути subtotal = 940.
8.2. Граничний випадок
Значення лежить точно на межі. Для безкоштовної доставки це subtotal = 1000.
8.3. Крайовий випадок області даних
Перевіряємо край допустимого або недопустимого діапазону. Для додатної кількості корисні значення 0 і 1.
Хороший набір тестів походить із правил задачі. Кожний тест має пояснювати конкретну гілку або межу.
8.4. Один тест — одна причина
Тест корисний, коли зрозуміло, яке правило він перевіряє. Для тарифів за віком можна скласти коротку матрицю:
| Вік | Очікуваний тариф | Причина тесту |
|---|---|---|
-1 |
помилка | недопустиме значення |
0 |
50 |
нижній край допустимої області |
11 |
50 |
перед першою межею |
12 |
80 |
точно на першій межі |
17 |
80 |
перед другою межею |
18 |
120 |
точно на другій межі |
Такий набір дає більше інформації, ніж шість випадкових значень. Він напряму походить із меж, записаних у вимозі.
9. Робочий порядок для задачі з умовою
- Назвіть вхідні дані та потрібний результат.
- Запишіть формули без умовних конструкцій.
- Визначте допустимі значення.
- Запишіть таблицю рішень або правила гілок.
- Позначте точні межі через
<,<=,>або>=. - Перенесіть правило в
bool,if,elseабоelse if. - Підготуйте нормальний, граничний і крайовий тест.
- Перед запуском передбачте результат для кожного тесту.
- Після зміни умови повторіть тести, що перевіряють цю межу.
10. Інтерактивна самоперевірка лекції
Самоперевірка перевіряє обчислення, вибір типу, читання умов, межі й тестові випадки.