Урок №38. Приоритет операций и правила ассоциативности

  Юрий  | 

  |

  Обновл. 11 Сен 2021  | 

 119330

 ǀ   17 

Чтобы правильно вычислять выражения (например, 4 + 2 * 3), мы должны знать, что делают определенные операторы и в каком порядке они выполняются. Последовательность, в которой они выполняются, называется приоритетом операций. Следуя обычным правилам математики (в которой умножение следует перед сложением), выражение, приведенное выше, обрабатывается следующим образом: 4 + (2 * 3) = 10.

В языке C++ все операторы (операции) имеют свой уровень приоритета. Те, в которых он выше, выполняются первыми. В таблице, приведенной ниже, можно увидеть, что приоритет операций умножения и деления (5) выше, чем в операциях сложения и вычитания (6). Компилятор использует приоритет операторов для определения порядка обработки выражений.

А что делать, если у двух операторов в выражении одинаковый уровень приоритета, и они размещены рядом? Какую операцию компилятор выполнит первой? А здесь уже компилятор будет использовать правила ассоциативности, которые указывают направление выполнения операций: слева направо или справа налево. Например, в выражении 3 * 4 / 2 операции умножения и деления имеют одинаковый уровень приоритета (5-й уровень). А ассоциативность пятого уровня соответствует выполнению операций слева направо, таким образом: (3 * 4) / 2 = 6.

Таблица приоритета и ассоциативности операций

Несколько примечаний:

   1 означает самый высокий уровень приоритета, а 17 — самый низкий. Операции с более высоким уровнем приоритета выполняются первыми.

   L -> R означает слева направо.

   R -> L означает справа налево.

Ассоциативность Оператор Описание Пример
1. Нет :: Глобальная область видимости (унарный) ::имя
:: Область видимости класса (бинарный) имя_класса::имя_члена
2. L -> R () Круглые скобки (выражение)
() Вызов функции имя_функции(parameters)
() Инициализация имя_типа(выражение)
{} uniform-инициализация (C++11) имя_типа{выражение}
type() Конвертация типа новый_тип(выражение)
type{} Конвертация типа (C++11) новый_тип{выражение}
[] Индекс массива указатель[выражение]
. Доступ к члену объекта объект.имя_члена
-> Доступ к члену объекта через указатель указатель_объекта->имя_члена
++ Пост-инкремент lvalue++
–– Пост-декремент lvalue––
typeid Информация о типе во время выполнения typeid(тип) или typeid(выражение)
const_cast Cast away const const_cast(выражение)
dynamic_cast Type-checked cast во время выполнения dynamic_cast(выражение)
reinterpret_cast Конвертация одного типа в другой reinterpret_cast(выражение)
static_cast Type-checked cast во время компиляции static_cast(выражение)
3. R -> L + Унарный плюс +выражение
Унарный минус -выражение
++ Пре-инкремент ++lvalue
–– Пре-декремент ––lvalue
! Логическое НЕ (NOT) !выражение
~ Побитовое НЕ (NOT) ~выражение
(type) C-style cast (новый_тип)выражение
sizeof Размер в байтах sizeof(type) или sizeof(выражение)
& Адрес &lvalue
* Разыменование *выражение
new Динамическое выделение памяти new тип
new[] Динамическое выделение массива new тип[выражение]
delete Динамическое удаление памяти delete указатель
delete[] Динамическое удаление массива delete[] указатель
4. L -> R ->* Выбор члена через указатель указатель_объекта->*указатель_на_член
.* Выбор члена объекта объект.*указатель_на_член
5. L -> R * Умножение выражение * выражение
/ Деление выражение / выражение
% Деление с остатком выражение % выражение
6. L -> R + Сложение выражение + выражение
Вычитание выражение — выражение
7. L -> R << Побитовый сдвиг влево выражение << выражение
>> Побитовый сдвиг вправо выражение >> выражение
8. L -> R < Сравнение: меньше чем выражение < выражение
<= Сравнение: меньше чем или равно выражение <= выражение
> Сравнение: больше чем выражение > выражение
>= Сравнение: больше чем или равно выражение >= выражение
9. L -> R == Равно выражение == выражение
!= Не равно выражение != выражение
10. L -> R & Побитовое И (AND) выражение & выражение
11. L -> R ^ Побитовое исключающее ИЛИ (XOR) выражение ^ выражение
12. L -> R | Побитовое ИЛИ (OR) выражение | выражение
13. L -> R && Логическое И (AND) выражение && выражение
14. L -> R || Логическое ИЛИ (OR) выражение || выражение
15. R -> L ?: Тернарный условный оператор выражение ? выражение : выражение
= Присваивание lvalue = выражение
*= Умножение с присваиванием lvalue *= выражение
/= Деление с присваиванием lvalue /= выражение
%= Деление с остатком и с присваиванием lvalue %= выражение
+= Сложение с присваиванием lvalue += выражение
-= Вычитание с присваиванием lvalue -= выражение
<<= Присваивание с побитовым сдвигом влево lvalue <<= выражение
>>= Присваивание с побитовым сдвигом вправо lvalue >>= выражение
&= Присваивание с побитовой операцией И (AND) lvalue &= выражение
|= Присваивание с побитовой операцией ИЛИ (OR) lvalue |= выражение
^= Присваивание с побитовой операцией «Исключающее ИЛИ» (XOR) lvalue ^= выражение
16. R -> L throw Генерация исключения throw выражение
17. L -> R , Оператор Запятая выражение, выражение

Некоторые операторы вы уже знаете из предыдущих уроков: +, -, *, /, (), =, < и >. Их значения одинаковы как в математике, так и в языке C++.

Однако, если у вас нет опыта работы с другими языками программирования, то большинство из этих операторов вам сейчас могут быть непонятны. Это нормально. Мы рассмотрим большую их часть на уроках этой главы, а об остальных расскажем по мере необходимости.

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

Как возвести число в степень в C++?


Вы уже должны были заметить, что оператор ^, который обычно используется для обозначения операции возведения в степень в обычной математике, не является таковым в языке C++. В языке С++ это побитовая операция XOR. А для возведения числа в степень в языке C++ используется функция pow(), которая находится в заголовочном файле cmath:

Обратите внимание, параметры и возвращаемые значения функции pow() являются типа double. А поскольку типы с плавающей точкой известны ошибками округления, то результаты pow() могут быть неточными (чуть меньше или чуть больше).

Если вам нужно возвести в степень целое число, то лучше использовать собственную функцию, например:

Не переживайте, если здесь что-то не понятно. Просто помните о проблеме переполнения, которая может произойти, если один из аргументов будет слишком большим.

Тест

Из школьной математики нам известно, что выражения внутри скобок выполняются первыми. Например, в выражении (2 + 3) * 4 часть (2 + 3) выполняется первой.

В этом задании есть 4 выражения, в которых отсутствуют какие-либо скобки. Используя приоритет операций и правила ассоциативности, приведенные выше, добавьте скобки в каждое выражение так, как если бы их обрабатывал компилятор.

Подсказка: Используйте колонку «Пример» в таблице приоритета и ассоциативности операций, чтобы определить, является ли оператор унарным (имеет один операнд) или бинарным (имеет два операнда).

Например: х = 2 + 3 % 4

Бинарный оператор % имеет более высокий приоритет, чем оператор + или =, поэтому он выполняется первым: х = 2 + (3 % 4). Затем выполняется бинарный оператор +, так как он имеет более высокий приоритет, чем оператор =.

Ответ: х = (2 + (3 % 4)).

Дальше нам уже не нужна таблица, чтобы понять ход обработки этого выражения компилятором.

Задания:

   Выражение №1: x = 3 + 4 + 5

   Выражение №2: x = y = z

   Выражение №3: z *= ++y + 5

   Выражение №4: a || b && c || d

Ответ

Выражение №1: x = 3 + 4 + 5

Уровень приоритета бинарного оператора + выше, чем оператора =, поэтому: х = (3 + 4 + 5). Ассоциативность бинарного оператора + слева направо, поэтому ответх = ((3 + 4) + 5).

Выражение №2: x = y = z

Ассоциативность бинарного оператора = справа налево, поэтому ответ: x = (y = z).

Выражение №3: z *= ++y + 5

Унарный оператор ++ имеет наивысший приоритет, поэтому: z *= (++y) + 5. Затем идет бинарный оператор +, поэтому ответz *= ((++y) + 5).

Выражение №4: a || b && c || d

Бинарный оператор && имеет приоритет выше, чем ||, поэтому: a || (b && c) || d. Ассоциативность бинарного оператора || слева направо, поэтому ответ: (a || (b && c)) || d.


Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (506 оценок, среднее: 4,89 из 5)
Загрузка...

Комментариев: 17

  1. Виктор:

    Чё-то нифига не понятно.
    while (exp)
    1. exp==false(0), цикл не выполняется и сразу return. Тут понятно.
    2. exp==true(1), то получаем бесконечный цикл. Не увидел условие выхода из цикла.
    3. exp==2, 3 и т.д , то есть при другом возможном значении?

    1. AHTOH:

      Если выражение exp >>= 1; вызывать много раз то оно в любом случае, когда то станет 0 т.е. FALSE. Но до тех пор пока оно не 0 оно TRUE.

    2. Богдан:

      Это так называемое "быстрое возведение в степень". Его ещё называют RPA(russian peasant algorithm(или алгоритм русского крестьянина)). На данном примере выигрыш во времени около нулевой, а вот если бы мы писали длинную арифметику, то там бы это нам серьёзно помогло.

    3. Ikincicy Bros.:

      Я попытаюсь объяснить.
      Как это работает? По умолчанию, результат равен 1. Попытаемся возвести 2 в степень 1. Проверяем условие: если 1 по битам сходится с 1 (буквально равны, ибо это 0001 в двоичном представлении), то result = 1 * 2 = 2. Дальше осуществляем сдвиг экспоненты вправо: exp = 1>>1 = 0. То есть сдвигаем вправо на 1 бит число 0001 – остается 0000 (1 отбрасывается без возможности восстановления), то есть 0. И так как экспонента не используется в цикле (нулевое значение – это автоматически false), то инструкция base = 2 * 2 не будет работать, потому что цикл больше не использует экспоненту, ибо равна нулю. Тогда результат равен 2. Коротко говоря, любое число в степени единицы равно само по себе без изменений.

      Теперь возведем двойку в квадрат. Проверяем условие и по битам сравним 2 и 1 (это 0010 и 0001 в двоичной системе соответственно). Условие ложно, значит пропускаем и продолжаем цикл. Далее идет смещение: exp = 2 >> 1 = 0001 = 1, то есть в 0010 отбросили последний 0, отсюда и 0001. Цикл все еще использует этот параметр (ненулевое значение – автоматически true), потому base = 2 * 2 = 4. Цикл начинается по новой, но exp = 1. Условие в этот раз истинно, то есть result = 1 * base = 1 * 4 = 4, так как base уже равен 4. Сдвигаем 1 на 1 бит – это 0. Цикл не использует переменную, потому последний стейтмент цикла не срабатывает. Выведется результат 4.

      Попытаемся возвести 2 в кубе. Проверяем условие побитового сравнения 3 и 1 (0011 и 0001), которое становится истинным. Значит result = 1 * 2 = 2. Далее сдвигаем 3 на 1 бит: exp = 3 >> 1 = 1. Параметр используется, значит: base = 2 * 2 = 4. Возобновляется цикл, где exp уже не 3, а 1. Условие вновь истинно, значит: result = result * base (полученные в прошлом цикле) = 2 * 4 = 8. Сдвиг 1 на 1 бит равен 0, значит цикл прекращается. Результат возвращает 8.

      Попытаемся возвести 2 в четвертой степени. Проверяем условие побитового сравнения 4 и 1 (0100 и 0001). Оно ложно. Идем дальше. Идет сдвиг вправо: exp = 4 >> 1 = 0100 >> 1 = 0010 = 2. Умножаем число само на себя: base = 2 * 2 = 4. Возобновляем цикл и снова проверяем условие побитового сравнения 2 и 1, что снова ложно. Далее сдвиг: exp = 2 >> 1 = 1. Параметр все еще используется, следовательно: base = base (полученный) на самого себя = 4 * 4 = 16. Возобновляем цикл снова, но на этот раз условие истинно. Значит: result = 1 * 16 = 16. Сдвиг 1 на 1 бит равен 0. Цикл прекращается. Возвращает результат 16.

  2. Георгий:

    «Если вам нужно возвести в степень целое число, то лучше использовать собственную функцию»
    Что-то я не понял, а зачем нам pow() тогда вообще? Нам привели пример как пользоваться pow, но сказали использовать какой то не понятный вариант. Вообще ничего не понял в этой главе с возведением в степень. Можно объяснить? Почему мы не можем возвести в степень целочисленный тип через pow()? Если через double лучше не делать, то зачем pow()?

    1. PP189:

      Использование собственной функции нужно для возведения целых чисел в степень. Например, степени десятки для перевода в различные системы счисления.
      Pow() применяется для чисел с плавающей точкой: он может принять аргументы int, но вернет их в возможно не точном виде типа double. При этом, даже такое неточное значение может пригодиться в математических уравнениях.

    2. Борис:

      Элементарно, Ватсон! Целые числа возводить в целую же степень — проще простого, поэтому легко реализовать такую функцию самому с целочисленными типами, где гарантированно всё будет без ошибок округления. А вот pow() корректно возводит в степень любые числа, даже дробные и отрицательные (как основание степени, так и показатель). Огромная разница.

  3. Андрей:

    Возвести целое число в целую степень разве не проще через обычный цикл?

    Зачем такая витиеватость?

    1. Андрей:

      Понятно. Количество умножений сократили.

      1. AHTOH:

        Количество умножений не может сократится. Иначе будет не верный результат. Сдается мне что если дисасемблировать ваш вариант и тот что в уроке, они будут практически одинаковыми.
        И скорость схожения у них одинаковая.

        1. Xakim:

          Пример. база 10, степень 20. В уроке степень считается битами, 20 это 10100, пять умножений плюс ещё два умножения на единичках, в цикле 20. Худший вариант это пять единиц(степень 31), тогда в урочном примере 10 умножений, а комментовом 31.
          Для степеней больше 31 количество умножений ещё больше различается, а на малых числах одинакова.
          Или я не так что-то понял?

        2. Srg:

          Мы работаем с нашим числом степеней в двоичной системе, считаем сколько единиц, двоек, четверок….. в степени. И это сокращает количество операций во много раз. А степени складываются умножением. base *= base — это и есть удвоение степеней, новый разряд в двоичной системе. И мы смотрим на крайнюю правую цифру в двоичном коде. Если разряд есть, то добавляем умножением его в сумму степеней. Затем убираем крайнюю правую цифру сдвигом, а слева появляется 0, и проверяем новую крайнюю цифру. Проверяем побитовым И, которое означает: " есть и первое, и второе" . Одно из них — это единица ( …0001). Там где есть 0, первого и второго точно не будет, получим 0, а у крайней правой может быть единица, если в нашем числе этот разряд тоже есть. И так сдвигаем, пока не выйдем на все нули,

        3. Bogdan Steh:

          На самом деле это вина исключительно отсталой математики, что люди не видят параллели между системами счисления. Привыкли, что круглое число называется не 8, а 10, и именно столько чисел между разрядами должно быть. 10-ичная система порочна тем, что кроме основы счисления в целых единицах, состоит из двух множителей, друг над руга не делящихся! То есть из 2 и 5. И это не позволяет отслеживать связь степеней. С простыми числами, тем более двойками легко работать — это обычный литерал, под которым хоть что можем понимать или подставлять, как и сделали в данном случае. Делаем мы там всегда! Если бы мы считали в жизни циклами, то миллиард возводили бы в квадрат не умножением, а сложением по единице. Экономия в целом не очевидна. Понятно, что когда нам нужно совершить определённое число действий, мы используем циклы, но сложение часто заменяем умножениям, а тут была двойная замена — и степени складывали умноженными на 2 и в степени отсчитывали через умножение. С единицей складывали, так как в единицах считали, считали бы в других количествах, в другие систему счисления переводили, могли бы и другое занулять по трафарету. А в десятках считали, могли бы просто нули нарисовать. Мне понравилось. Неожиданно концовка

    2. Сергей:

      Хм…. А и правда…
      При малых степенях нет смысла в извращении понятной логики, а при больших степенях и базах вероятность переполнения гораздо выше производительности функции.
      Но при последовательном умноженнии проще отследить owerflow, чем при перемножении хитроумных четных степеней базы.

  4. zashiki:

    здравствуйте, может, где то упоминалось, но не найду: операторы ввода вывода << и >> (к примеру в std::cout<<) — это и есть побитовый сдвиг влево вправо?
    Если нет, то какое место они занимают во всей этой системе операторов?

    1. Chestor:

      << и >> это всё те же операции битового сдвига влева и вправо соответсвенно, НО, в пространстве имён std эти две операции ПЕРЕОПРЕДЕЛЕНЫ. Думаю, о переопределении, речь пойдёт в след. уроках и Вы узнаете, что это такое. Но можете сейчас погуглить )))

      1. zashiki:

        Спасибо за ответ! "Гуглить сейчас" не стоило)

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *