Операторы в C/C++
В этом уроке мы разберем операторы C++: арифметические, логические и операторы сравнения, а также инкремент и декремент. Отдельно разберем приоритет операций и ассоциативность, потому что именно они чаще всего становятся причиной "странных" багов. В конце есть практические задачи с решениями и чек-лист самопроверки.
Главная мысль урока: оператор -- это действие, а выражение -- это комбинация операторов и операндов, которая дает результат. Чтобы код был надежным, нужно понимать: (1) какие операторы делают вычисления, (2) какие делают сравнение и логические проверки, (3) в каком порядке они выполняются (приоритет и ассоциативность), и (4) почему числа с плавающей точкой нельзя сравнивать "в лоб" через ==.
Содержание
- 1. Цели урока
- 2. Что такое операторы, выражения и порядок вычислений
- 3. Арифметические операторы
- 4. Операторы сравнения и логические операторы
- 5. Инкремент и декремент
- 6. Приоритет операций и ассоциативность (практический минимум)
- 7. Практика: типовые задачи (с решениями)
- 8. Чек-лист самопроверки знаний
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 |