1. Знайомство з <cmath>
Знайомство з математичними функціями часто виглядає так: студент пише формулу, компілятор свариться на sqrt, студент навмання додає щось іще, компілятор замовкає — і всі щасливі… до першого дивного результату. Проблема в тому, що математика в програмуванні — це не лише «формули зі школи». Це ще й акуратна робота з обмеженнями обчислень, типами та особливими значеннями.
<cmath> — це частина стандартної бібліотеки C++. У ньому зібрано перевірені реалізації математичних функцій. Це важливо з двох причин. По-перше, ви отримуєте коректні й оптимізовані алгоритми, а не «саморобний корінь», який «ніби працює, доки не зламається». По-друге, <cmath> задає спільний стиль: якщо інший програміст бачить std::sqrt(x), він одразу розуміє, що відбувається. Якщо ж він бачить mySuperRoot(x), то розуміє лише одне: хтось полюбляв пригоди.
У цій лекції мислитимемо прагматично: функція — це інструмент, а в кожного інструмента є інструкція із застосування. Якщо ви намагаєтеся забивати цвяхи мікроскопом, проблема не в мікроскопі.
Підключаємо <cmath> і звикаємо до std::
Перш ніж щось обчислювати, треба правильно підключити заголовок і викликати функції так, як це заведено в C++. Тут часто виникає невелика плутанина: хтось пише sqrt(x) без std::, десь це компілюється «випадково», десь — ні, а десь узагалі викликається не та перевантажена версія.
Базове правило просте: для математичних функцій використовуємо #include <cmath> і пишемо std::sqrt, std::abs, std::pow. Це не «занудство заради занудства», а спосіб зробити код переносним і передбачуваним.
Ось мінішаблон, який варто запамʼятати:
#include <iostream>
#include <cmath>
int main() {
double x = 9.0;
std::cout << std::sqrt(x) << '\n'; // 3
}
Якщо ви бачите помилку «sqrt was not declared in this scope», то причина майже завжди одна з двох: або ви забули #include <cmath>, або намагаєтеся викликати sqrt без std:: в оточенні, де випадкового «підхоплення» не сталося.
Карта трьох функцій
Перш ніж занурюватися в деталі, корисно скласти «карту місцевості»: що це за функції, для чого вони потрібні та в чому їхні обмеження. Так менше шансів писати код у стилі «я натиснув кнопку, а воно само».
| Функція | Заголовок | Ідея простими словами | Що важливо памʼятати |
|---|---|---|---|
|
|
Робить число невідʼємним (модуль) | Чудовий помічник для порівняння за допомогою eps |
|
|
Квадратний корінь | Коректний результат очікуємо при x >= 0 |
|
|
Підносить a до степеня b | Результат має тип double, тож можливі похибки |
З цією картою й підемо далі: спочатку abs (найбезпечніша), потім sqrt (у неї є домен), а далі pow (найгнучкіша, а отже, і найпідступніша).
2. std::abs: модуль як найкращий друг точності й умов
Модуль здається дитячою темою: «прибрати мінус». Але в обчисленнях із дійсними числами std::abs перетворюється на справжню робочу конячку. Адже похибка — це майже завжди різниця, яку зручно брати за модулем. Коли ви перевіряєте, чи числа близькі одне до одного, то майже напевно пишете abs(a - b) < eps.
Почнімо з простого: модуль різниці двох чисел. Це корисно навіть без жодного eps, наприклад коли ви хочете зрозуміти, «наскільки ми промахнулися».
#include <iostream>
#include <cmath>
int main() {
double planned = 10.0;
double actual = 9.6;
double diff = std::abs(planned - actual);
std::cout << diff << '\n'; // 0.4
}
А тепер — практичний шаблон порівняння через eps. Ми вже обговорювали ідею «майже дорівнює», а тут просто робимо її коротшою та охайнішою.
#include <iostream>
#include <cmath>
int main() {
double a = 0.1 + 0.2;
double b = 0.3;
const double eps = 1e-12;
bool equal = (std::abs(a - b) < eps);
std::cout << equal << '\n'; // 1 (true)
}
Важливо вловити головне: std::abs тут не «математична іграшка», а спосіб зробити логіку if стабільнішою. Ви більше не залежите від того, як саме число зберігається в памʼяті, — ви порівнюєте зміст, а не двійкову фотографію числа.
3. std::sqrt: корінь і домен функції
Коли ви вперше використовуєте sqrt, зазвичай усе добре: sqrt(9) дає 3, і настрій чудовий. Але в реальному коді sqrt частіше зʼявляється не з ідеально красивими числами, а з виразами: дискримінант, відстань, формула з фізики, обчислення стандартного відхилення. І саме тут зʼявляється ключове поняття — домен функції.
Домен — це набір вхідних значень, для яких операція має очікуваний сенс у вашій моделі. У звичайній дійсній арифметиці квадратний корінь очікуємо лише для x >= 0. Якщо ви викликаєте std::sqrt(-1.0), програма часто не падає, але ви майже напевно отримаєте NaN. А далі NaN може непомітно розповзтися по всіх розрахунках.
Найпростіший і найчесніший спосіб — перевіряти вхідні дані перед викликом:
#include <iostream>
#include <cmath>
int main() {
double x;
std::cin >> x;
if (x < 0.0) {
std::cout << "помилка домену sqrt\n";
} else {
std::cout << std::sqrt(x) << '\n';
}
}
Тут ми робимо важливу річ: не «лікуємо симптоми» після того, як уже отримали NaN, а запобігаємо проблемі ще до обчислення.
Схема безпечного використання sqrt
Іноді корисно подивитися на логіку не лише як на код, а й як на невелику блок-схему — так новачкам простіше.
flowchart TD
A[Маємо значення x] --> B{ x >= 0 ? }
B -- ні --> C[Повідомляємо про помилку домену]
B -- так --> D["Обчислюємо y = sqrt(x)"]
D --> E[Використовуємо y далі]
У реальному коді «повідомляємо про помилку» може означати будь-що: вивести текст, підставити значення за замовчуванням або завершити програму. Поки що обираємо найзрозуміліший варіант — просто друкуємо повідомлення.
Нюанс: трішки відʼємне через похибку
Ось типова життєва ситуація: за формулою у вас має вийти 0, але через округлення вийшло -1e-16. Математично це «майже нуль», а для sqrt це вже відʼємне число.
Тут стають у пригоді здоровий глузд і той самий eps. Можна зробити «мʼяку» перевірку: якщо число трохи менше за нуль, але його модуль менший за eps, вважаємо його нулем.
#include <iostream>
#include <cmath>
int main() {
double x = -1e-16;
const double eps = 1e-12;
if (x < 0.0 && std::abs(x) > eps) {
std::cout << "помилка домену sqrt\n";
} else {
if (x < 0.0) x = 0.0; // прирівняли до 0
std::cout << std::sqrt(x) << '\n'; // 0
}
}
Це не «обман математики», а акуратна інженерна домовленість: ми визнаємо, що обчислення неточні, і захищаємося від мікроскопічних артефактів.
Приклад: відстань між двома точками
Щоб не залишатися в абстракції, вбудуємо sqrt у невеликий практичний сюжет. Нехай програма вміє обчислювати відстань між точками (x1, y1) і (x2, y2):
\[ dist = \sqrt{(x2-x1)^2 + (y2-y1)^2} \]
#include <iostream>
#include <cmath>
int main() {
double x1, y1, x2, y2;
std::cin >> x1 >> y1 >> x2 >> y2;
double dx = x2 - x1;
double dy = y2 - y1;
double dist = std::sqrt(dx * dx + dy * dy);
std::cout << dist << '\n';
}
Зверніть увагу: для квадрата ми використали dx * dx, а не pow(dx, 2). Чому саме так, поговоримо в наступному розділі про pow. А поки просто запамʼятайте: квадрат числа часто записують множенням — це нормально й читабельно.
4. std::pow: степінь, яка виглядає красиво
std::pow(a, b) — функція потужна: вона вміє підносити до дробових і відʼємних степенів, до степенів на кшталт 0.5 (що схоже на корінь), і взагалі створює відчуття, ніби перед вами математичний калькулятор.
Але саме через цю універсальність є дві практичні особливості. По-перше, результат майже завжди має тип double, навіть якщо ви думаєте: «Я ж просто підніс число до квадрата — там усе має бути точно». По-друге, pow може бути «важчою» за прості операції, бо універсальне піднесення до степеня часто складніше за звичайне множення.
Частий сценарій: складні відсотки
Дуже життєва формула — зростання внеску з капіталізацією:
\[ S = P \cdot (1 + r)^{n} \]
де P — початкова сума, r — відсоток за період (наприклад, 0.05), n — кількість періодів.
#include <iostream>
#include <cmath>
int main() {
double p = 1000.0;
double r = 0.05;
int n = 3;
double s = p * std::pow(1.0 + r, n);
std::cout << s << '\n'; // 1157.63 (приблизно)
}
Це чудовий приклад ситуації, де pow справді на своєму місці: формула читається майже так само, як у підручнику.
pow(x, 2) vs x * x
Коли степінь мала й ціла, часто простіше та швидше написати множення. І, що важливо для новачка, іноді це ще й зрозуміліше: dx * dx мозок читає як «квадрат dx», навіть якщо ви ще не звикли до математичних позначень.
Порівняйте:
double a = dx * dx + dy * dy;
double b = std::pow(dx, 2.0) + std::pow(dy, 2.0);
У навчальних прикладах допустимі обидва варіанти, але для нашого рівня корисно памʼятати: pow варто брати тоді, коли степінь — це не просто 2 або 3, або коли формула має читатися саме як формула.
pow і домен
Із pow теж бувають доменні сюрпризи. Наприклад, pow(negative, 0.5) — це за змістом корінь із відʼємного числа, і ви знову ризикуєте отримати NaN. Ми не заглиблюватимемося в математичний аналіз і комплексні числа, але практичне правило тут таке: якщо ви використовуєте pow як заміну sqrt, то домен приблизно той самий — аргумент має бути невідʼємним (у звичній моделі дійсних чисел).
#include <iostream>
#include <cmath>
int main() {
double x = -9.0;
double y = std::pow(x, 0.5);
std::cout << y << '\n'; // nan (часто)
}
Це не «помилка компілятора», а просто сигнал: ви попросили виконати операцію, яка в межах дійсних чисел приводить до «не числа».
5. Збираємо мінізастосунок «MathLab»
Зараз зберемо невеликий каркас програми, у якій можна вибрати режим і виконати потрібний розрахунок. Ми не перетворюємо це на «величезний проєкт», але зберігаємо головну ідею нашого застосунку: один і той самий main.cpp, який ви поступово доповнюєте новими можливостями.
Почнімо з простої розвилки за командою. Ми свідомо використовуємо лише те, що ви вже проходили: рядки, if/else, введення і виведення, а також базові обчислення.
#include <iostream>
#include <string>
#include <cmath>
int main() {
std::string mode;
std::cin >> mode;
if (mode == "dist") {
double x1, y1, x2, y2;
std::cin >> x1 >> y1 >> x2 >> y2;
double dx = x2 - x1;
double dy = y2 - y1;
std::cout << std::sqrt(dx * dx + dy * dy) << '\n';
} else if (mode == "deposit") {
double p, r;
int n;
std::cin >> p >> r >> n;
std::cout << p * std::pow(1.0 + r, n) << '\n';
} else {
std::cout << "невідомий режим\n";
}
}
Приклад використання (умовно — як у Web‑IDE):
Ввід:
dist 0 0 3 4
Вивід:
// 5
Ввід:
deposit 1000 0.05 3
Вивід:
// 1157.63... (точне відображення залежить від форматування, цим займемося в наступній лекції)
Зверніть увагу на одну річ: ми підключили <cmath> один раз і використовуємо всі три функції як частини прикладних сценаріїв, а не як «функції заради функцій». Це важливий стиль: математика в коді має обслуговувати задачу, а не красуватися у вакуумі.
Додамо перевірку для sqrt
У формулі відстані під коренем стоїть сума квадратів, а вона математично невідʼємна. Та звичка бути акуратним усе одно корисна, бо ви переноситимете цей стиль і в інші місця, де під коренем уже може опинитися щось цікавіше.
#include <iostream>
#include <string>
#include <cmath>
int main() {
std::string mode;
std::cin >> mode;
if (mode == "dist") {
double x1, y1, x2, y2;
std::cin >> x1 >> y1 >> x2 >> y2;
double dx = x2 - x1;
double dy = y2 - y1;
double value = dx * dx + dy * dy;
if (value < 0.0) {
std::cout << "помилка домену sqrt\n";
} else {
std::cout << std::sqrt(value) << '\n';
}
}
}
Так, тут перевірка здається майже зайвою. Але це як пасок безпеки: більшу частину часу ви про нього не думаєте, зате в рідкісний невдалий день він дуже доречний.
6. Типові помилки під час роботи з <cmath> і цими трьома функціями
Помилка № 1: забули #include <cmath> і намагаються «полагодити» це дивними способами.
Коли компілятор пише, що sqrt або pow не оголошено, іноді хочеться додати випадковий заголовок «на удачу». Правильне рішення просте: математичні функції — це <cmath>. І краще одразу писати std::sqrt, std::pow, std::abs, щоб не залежати від випадкових підключень.
Помилка № 2: викликають sqrt без перевірки домену, а потім дивуються, що if працює дивно.
Якщо в обчислення потрапив NaN, порівняння починають поводитися неінтуїтивно: x < 0 і x > 0 можуть одночасно виявитися хибними. Тому хороша звичка — щоразу ставити собі запитання: «Що може опинитися під коренем?» І перевіряти вхід до sqrt, особливо якщо під коренем стоїть вираз із даних користувача.
Помилка № 3: віра в те, що pow дає «точно квадрат» і результат можна порівнювати через ==.
Навіть якщо математично результат має бути цілим, обчислення виконується в double, а там можливі округлення. Якщо потім порівнювати результат через ==, можна легко потрапити в несподівану гілку if. Якщо порівняння справді важливе, використовуйте підхід з eps: std::abs(a - b) < eps.
Помилка № 4: використання pow(x, 2) усюди підряд, зокрема там, де простіше написати x*x.
Це не катастрофа, але часто робить код важчим і менш прозорим. pow чудово підходить для «справжніх степенів» і формул на кшталт відсотків, а для квадрата чи куба зазвичай читабельніше й простіше написати множення. У навчальних задачах це не завжди критично, але звичка «не ускладнювати без причини» окупиться пізніше.
Помилка № 5: плутають «точність обчислень» і «гарний вивід».
Часто студент виводить число й бачить 1157.63, думає: «Ідеально», а потім перевіряє через == і отримує сюрприз. Виведення — це лише відображення, а не гарантія точного зберігання. Форматування ми розбиратимемо в наступній лекції, але вже зараз корисно памʼятати: спочатку обчислення, потім вивід. І вивід цілком може «сховати» дрібні хвостики дробів.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ