Приоритет операций. Cложные математические выражения
В математических вычислениях важную роль играет порядок, в котором выполняются действия. Вспомним старый школьный прикол. Сколько будет 2 + 2 * 2? Конечно же шесть, т.к. сначала выполняется умножение.
В C используется знакомый нам со школы порядок действий. Но т.к. в программировании есть свои особенности, то кое-какие отличия всё же есть.
Выпишем приоритет для тех операций, которые мы уже знаем.
- явное приведение типов
- вызов и вычисление функций (например,
sqrt(),cos()и пр.) - умножение, деление, остаток от деления
- сложение, вычитание
- выполняется присваивание
Давайте себя проверим. Какое значение будет записано в переменную x после выполнения следующей строки кода int x = 8 / 4 * 2;?
Правильный ответ 4. Т.к все операторы / и *, присутствующие в выражении, имеют одинаковый приоритет, то действия выполняются слева направо. Другими словами данный код эквивалентен коду int x = (8 / 4) * 2;
Здесь я использовал круглые группирующие скобки () для того, чтобы явно указать, что деление должно быть выполнено первым действием. Использование скобок для изменения порядка действий скорее всего уже хорошо вам известно со школы.
Давайте дополним нашу таблицу приоритетов операций скобками:
- действия в скобках
() - явное приведение типов
- вызов и вычисление функций (например,
sqrt(),cos()и пр.) - умножение, деление, остаток от деления
- сложение, вычитание
- выполнение присваивания
Листинг 1.
2 + 2 * 2 = 6 // эквивалентно выражению 2 + (2 * 2)
(2 + 2) * 2 = 8 // действие в скобках будет выполнено прежде умноженияВажно!
Если в вашей программе вы написали довольно сложное выражение, в котором не сразу ясен порядок операций, то лучше добавить лишние скобки, чтобы явно задать последовательность операций. Это поможет избежать ошибок в вычислениях, которые потом будет сложно отловить.
Кстати, а чему равняется значение x для следующего выражения: int x = 1 / 2 * 2 + 1?
Кто сказал 2? Не правильно! Уже догадались почему? Нет, с приоритетами тут всё хорошо. Если расставить скобки, то получим следующее эквивалентное выражение int x = ((1 / 2) * 2) + 1.
Да, верно, проблема в том, что деление-то целочисленное, а значит 1 / 2 даст 0, а значит 0 * 2 = 0. И в итоге правильный ответ 1. Вывод: Не забывайте про особенности целочисленного деления.
Сложные математические выражения
Формулы для вычислений бывают довольно громоздкими.
При этом, когда мы пишем программу, любое выражение, даже если в нём присутствуют дроби (в том числе и многоуровневые), мы записываем в строчку. На начальном этапе это может вызвать некоторые затруднения. Чтобы от них избавиться, нужно овладеть навыком переводить формулу из стандартной математической записи в ту, которая используется в программировании и обратно.
В следующем небольшом видео на простом примере показаны несколько подходов, позволяющих не запутаться при записи сложных математических выражений.
Зеркало на RuTube, на VK.Видео
Практика
- Иногда из-за особенностей выполнения арифметических действий и преобразований некоторые математические истины "перестают работать". Разберитесь, почему следующие программы выдают различные результаты:
#include <stdio.h>
int main(void)
{
int a = 8, b = 16, c = 2;
int res_1 = a / b * c;
int res_2 = a * c / b;
printf("1: %d\n2: %d\n", res_1, res_2);
return 0;
}#include <stdio.h>
int main(void)
{
int a = 5, b = 7, c = 9;
double res_1 = (double) c / (b / a);
double res_2 = c / ((double) b / a);
printf("1: %.2f\n2: %.2f\n", res_1, res_2);
return 0;
}Ваши объяснения этих "парадоксов" пишите в комментариях к этому уроку.
Дополнительные материалы
1. Для понимания структуры выражений (да и всего кода в целом) компилятор строит специальную древовидную структуру, называемую абстрактным синтаксическим деревом (Abstract Syntax Tree, AST).
Например, для выражения int num = 3 + a * 5; такое дерево может выглядеть следующим образом:
=
/ \
num +
/ \
3 *
/ \
a 5Компилятор не рисует такие рисуночки (их называют графы), а использует для этого специальные структуры данных. Но нам, конечно, удобнее воспринимать визуальное представление.
Важно отметить, что структура AST напрямую отражает приоритет операций: операции с более высоким приоритетом (например, умножение) располагаются ближе к листьям дерева (значения и перменные), чем операции с меньшим приоритетом (например, сложение).
Такие деревья используются компилятором для:
- Оптимизации и упрощения выражений
- Удаления ненужных вычислений
- Переупорядочивания операций для большей эффективности
- Генерации эффективного машинного кода
- Обнаружения синтаксических ошибок
Подобные структуры изучаются в рамках Теоретической информатики (Computer Science) в курсах по "Формальным языкам и грамматикам".
2. Полная таблица с приоритетами всех операторов языка Си доступна здесь.