Перейти к содержимому

Операторы в C/C++

Операторы в C/C++

В этом уроке мы разберем операторы C++: арифметические, логические и операторы сравнения, а также инкремент и декремент. Отдельно разберем приоритет операций и ассоциативность, потому что именно они чаще всего становятся причиной "странных" багов. В конце есть практические задачи с решениями и чек-лист самопроверки.

Главная мысль урока: оператор -- это действие, а выражение -- это комбинация операторов и операндов, которая дает результат. Чтобы код был надежным, нужно понимать: (1) какие операторы делают вычисления, (2) какие делают сравнение и логические проверки, (3) в каком порядке они выполняются (приоритет и ассоциативность), и (4) почему числа с плавающей точкой нельзя сравнивать "в лоб" через ==.

Содержание

1. Цели урока

  • Узнать про приоритет операций в C++ и правила ассоциативности.
  • Познакомиться с арифметическими операторами и составными присваиваниями.
  • Познакомиться с операторами сравнения и логическими операторами.
  • Познакомиться с операторами инкремента и декремента и понять разницу prefix/postfix.
  • Научиться правильно сравнивать числа с плавающей точкой (float/double) через допуск (epsilon).
Что важно запомнить: если выражение выглядит "сложно", почти всегда лучше добавить скобки. Это дешевле, чем искать баг в поведении робота.

2. Что такое операторы, выражения и порядок вычислений

Оператор -- это знак или ключевое слово, которое задает действие. Примеры операторов: +, *, ==, &&, =.

Операнд -- это значение, над которым выполняется оператор. В выражении a + b операнды -- a и b.

Выражение -- комбинация операторов и операндов, которая дает результат. Например, (a + 3) * (b + 1).

int a = 4;
int b = 9;
int c = (a + 3) * (b + 1); // выражение, результат 70

Приоритет и ассоциативность

Приоритет (precedence) отвечает на вопрос: "какой оператор выполняется раньше". Пример: умножение обычно выполняется раньше сложения, поэтому 2 + 3 * 4 это 14, а не 20.

Ассоциативность отвечает на вопрос: "если операторы одного приоритета стоят рядом, выполнять слева направо или справа налево". Например, вычитание ассоциативно слева направо: 10 - 3 - 2 это (10 - 3) - 2.

Побочные эффекты

Побочный эффект -- это изменение состояния программы при вычислении выражения. Самый частый пример: присваивание =, а также ++ и --.

Правило: не смешивайте несколько побочных эффектов в одном сложном выражении. Это ухудшает читаемость и может давать неожиданный порядок вычислений.
^ К оглавлению

3. Арифметические операторы

3.1. + - * / % и составные присваивания

Основные арифметические операторы работают так же, как в математике: + (сложение), - (вычитание), * (умножение), / (деление). Оператор % (остаток от деления) работает только для целых типов.

int a = 10;
int b = 3;

int s = a + b;  // 13
int d = a - b;  // 7
int m = a * b;  // 30
int q = a / b;  // 3  (целочисленное деление)
int r = a % b;  // 1  (остаток)

Составные присваивания сокращают запись: a += b означает a = a + b.

int x = 5;
x += 2; // x = 7
x *= 3; // x = 21

3.2. Целочисленное деление

Если оба операнда целые, результат деления тоже целый: дробная часть отбрасывается. Это часто становится неожиданностью.

int a = 5;
int b = 2;

int q1 = a / b;      // 2
double q2 = a / b;   // 2.0 (деление уже произошло как int)
double q3 = 1.0 * a / b; // 2.5 (один операнд double)

3.3. Типичные ошибки

Ошибка: деление на ноль

Для целых типов деление на 0 приводит к неопределенному поведению (часто аварийное завершение). Для double может появиться inf или nan, но на это нельзя полагаться в логике управления.

int a = 10;
int z = 0;
// int q = a / z; // нельзя

if (z != 0) {
    int q = a / z;
}

Ошибка: переполнение целого типа

Если значение выходит за диапазон типа, оно может "перевернуться". Это особенно опасно в счетчиках и таймерах. На практике используйте подходящий тип и проверяйте диапазоны.

^ К оглавлению

4. Операторы сравнения и логические операторы

4.1. Сравнение: == != < > <= >=

Операторы сравнения возвращают bool (true или false). Они используются в условиях if, while, в проверках датчиков и ограничений.

int dist = 15;

bool a = (dist == 10);  // равно
bool b = (dist != 10);  // не равно
bool c = (dist <  20);  // меньше
bool d = (dist >= 15);  // больше или равно
Важно: = и == -- разные операторы. = присваивает значение, а == сравнивает.
int x = 5;

// if (x = 0) { }   // ошибка логики: это присваивание, x станет 0, условие будет false
if (x == 0) {       // правильно: сравнение
    // ...
}

4.2. Логика: && || ! и short-circuit

Логические операторы работают с bool: && (И), || (ИЛИ), ! (НЕ).

bool sensorOk = true;
bool batteryOk = false;

bool canMove1 = sensorOk && batteryOk; // true только если оба true
bool canMove2 = sensorOk || batteryOk; // true если хотя бы один true
bool needStop = !batteryOk;            // инверсия

Short-circuit (короткое замыкание) означает: A && B не вычисляет B, если A уже false; A || B не вычисляет B, если A уже true. Это удобно для безопасных проверок.

int* p = nullptr;

// Безопасно: если p == nullptr, правую часть не проверяем
if (p != nullptr && *p > 0) {
    // ...
}

4.3. Типичные ошибки

Ошибка: путаница && и ||

В управлении роботом это может означать "ехать можно только если все условия выполнены" (И), или "ехать можно если выполнено хотя бы одно условие" (ИЛИ). Эти режимы дают разное поведение.

Ошибка: забыли скобки в сложном условии

bool a = true;
bool b = false;
bool c = false;

// Без скобок может быть неочевидно, что выполняется первым
bool x1 = a || b && c;

// Яснее так:
bool x2 = a || (b && c);

Да, приоритет && выше, чем ||, но скобки делают смысл очевидным.

^ К оглавлению

5. Инкремент и декремент

5.1. Префикс и постфикс

Инкремент увеличивает значение на 1, декремент уменьшает на 1. Есть две формы:

  • ++i, --i -- префикс: сначала изменить, потом использовать значение.
  • i++, i-- -- постфикс: сначала использовать старое значение, потом изменить.
int i = 5;

int a = ++i; // i станет 6, a = 6
int b = i++; // b = 6, потом i станет 7

5.2. Где использовать

Самое типичное место -- циклы:

for (int i = 0; i < 10; ++i) {
    // ...
}
Практический совет: в простых циклах чаще используют ++i. Для int разницы почти нет, но это хорошая привычка (особенно когда позже встретятся итераторы и более сложные типы).

5.3. Типичные ошибки

Ошибка: несколько ++ в одном выражении

Такой код плохо читается и может вести себя по-разному, если порядок вычислений не очевиден. Лучше разнести по строкам.

int i = 1;

// Плохая идея:
// int x = i++ + ++i;

// Лучше так:
int x1 = i;
i = i + 1;
i = i + 1;
int x2 = x1 + i;
^ К оглавлению

6. Приоритет операций и ассоциативность (практический минимум)

6.1. Мини-таблица приоритетов

Полная таблица большая. Ниже минимум, который нужен каждый день. Чем выше строка, тем выше приоритет.

ГруппаОператорыКомментарий
Унарные ! ++ -- + - Применяются к одному операнду
Умножение * / % Выше, чем сложение
Сложение + - Ниже, чем умножение
Сравнение < > <= >= Дает bool
Равенство == != Дает bool
Логика && выше, чем || Short-circuit
Присваивание = += *= и т.д. Обычно ближе к концу выражения

6.2. Примеры с разбором

Пример 1: арифметика

int x = 2 + 3 * 4;     // 14, потому что * выше, чем +
int y = (2 + 3) * 4;   // 20, скобки меняют порядок

Пример 2: логика

bool a = true;
bool b = false;
bool c = true;

bool r1 = a || b && c;      // читается трудно
bool r2 = a || (b && c);    // то же самое, но понятно

6.3. Типичные ошибки

Ошибка: "я знаю приоритет, скобки не нужны"

Даже если вы знаете таблицу, следующий читатель может не знать. В робототехнике ошибки дорого стоят. Скобки повышают надежность и ускоряют проверку кода.

^ К оглавлению

7. Практика: типовые задачи (с решениями)

Базовые вычисления и условия

Задача 1: целочисленное деление

Покажите разницу между a/b как int и как double.

#include <iostream>

int main() {
    int a = 5;
    int b = 2;

    std::cout << "int: " << (a / b) << "\n";
    std::cout << "double: " << (1.0 * a / b) << "\n";
    return 0;
}

Задача 2: условие для движения робота

Движение разрешено, если датчик в норме и батарея в норме. Выведите результат.

#include <iostream>

int main() {
    bool sensorOk = true;
    bool batteryOk = false;

    bool canMove = sensorOk && batteryOk;
    std::cout << (canMove ? "move" : "stop") << "\n";
    return 0;
}

Инкремент/декремент и циклы

Задача 3: prefix vs postfix

Выведите i, a, b, чтобы увидеть разницу.

#include <iostream>

int main() {
    int i = 5;
    int a = ++i;
    int b = i++;

    std::cout << "i=" << i << " a=" << a << " b=" << b << "\n";
    return 0;
}

Сравнение float/double

Задача 4: сравнение double через epsilon

Проверьте, что сумма 0.1 десять раз не равна 1.0 через ==, и сравните через допуск.

#include <iostream>
#include <cmath>

bool almostEqual(double a, double b, double eps) {
    return std::abs(a - b) < eps;
}

int main() {
    double s = 0.0;
    for (int i = 0; i < 10; ++i) s += 0.1;

    std::cout << (s == 1.0 ? "equal" : "not equal") << "\n";
    std::cout << (almostEqual(s, 1.0, 1e-9) ? "almost equal" : "not almost") << "\n";
    return 0;
}
^ К оглавлению

8. Чек-лист самопроверки знаний

Отметьте пункты, которые вы понимаете и можете применить без подсказок.

+/-НавыкПроверка
Оператор и операнд Могу объяснить, что оператор делает действие, а операнд -- значение для действия
Выражение Могу привести пример выражения и сказать, что оно дает результат
Арифметика Понимаю работу + - * / %, знаю про целочисленное деление
Сравнение Использую == != < > <= >= и понимаю, что результат -- bool
Логика и short-circuit Понимаю &&, ||, ! и умею делать безопасные проверки через short-circuit
= vs == Могу объяснить, что = присваивает, а == сравнивает, и почему это важно в if
Инкремент/декремент Понимаю разницу ++i и i++ и использую их осознанно
Приоритет Знаю минимум приоритетов и ставлю скобки, когда выражение неочевидно
double сравнение Не сравниваю double через ==, использую abs(a-b) < eps
^ К оглавлению

Ссылки по теме

Вторник, 03 марта 2026
Операторы в C/C++