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 domain error\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 domain error\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 << "unknown mode\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 domain error\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, думает «идеально», а потом делает проверку == и получает сюрприз. Печать — это отображение, а не гарантия точности хранения. Форматирование мы будем разбирать следующей лекцией, но уже сейчас полезно помнить: сначала вычисления, потом вывод, и вывод может «скрывать» мелкие хвосты дробей.

1
Задача
C++ SELF, 10 уровень, 4 лекция
Недоступна
Разница показаний
Разница показаний
1
Задача
C++ SELF, 10 уровень, 4 лекция
Недоступна
Квадратный корень
Квадратный корень
1
Задача
C++ SELF, 10 уровень, 4 лекция
Недоступна
Диагональ экрана
Диагональ экрана
1
Задача
C++ SELF, 10 уровень, 4 лекция
Недоступна
Сравнение цен
Сравнение цен
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ