JavaRush /Курси /C++ SELF /<cmath> на пра...

<cmath> на практиці: abs, sqrt, pow

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

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:: в оточенні, де випадкового «підхоплення» не сталося.

Карта трьох функцій

Перш ніж занурюватися в деталі, корисно скласти «карту місцевості»: що це за функції, для чого вони потрібні та в чому їхні обмеження. Так менше шансів писати код у стилі «я натиснув кнопку, а воно само».

Функція Заголовок Ідея простими словами Що важливо памʼятати
std::abs(x)
<cmath>
Робить число невідʼємним (модуль) Чудовий помічник для порівняння за допомогою eps
std::sqrt(x)
<cmath>
Квадратний корінь Коректний результат очікуємо при x >= 0
std::pow(a, b)
<cmath>
Підносить 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, думає: «Ідеально», а потім перевіряє через == і отримує сюрприз. Виведення — це лише відображення, а не гарантія точного зберігання. Форматування ми розбиратимемо в наступній лекції, але вже зараз корисно памʼятати: спочатку обчислення, потім вивід. І вивід цілком може «сховати» дрібні хвостики дробів.

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