4.8. Побитовые операторы
Побитовые операторы (bitwise operator) получают операнды целочисленного типа, которые они используют как коллекции битов. Эти операторы позволяют проверять и устанавливать отдельные биты. Как будет описано в разделе 17.2, эти операторы можно также использовать для библиотечного типа bitset, представляющего коллекцию битов изменяемого размера.
Как обычно, если операнд — "малое целое число", его значение сначала преобразуется (раздел 4.11) в больший целочисленный тип. Операнды могут быть знаковыми или беззнаковыми.
Таблица 4.3. Побитовые операторы (левосторонний порядок)
Оператор Действие Применение ~ Побитовое NOT ~выражение << Сдвиг влево выражение1 << выражение2 >> Сдвиг вправо выражение1 >> выражение2 & Побитовое AND выражение1 & выражение2 ^ Побитовое XOR выражение1 ^ выражение2 | Побитовое OR выражение1 | выражение2Если операнд знаковый и имеет отрицательное значение, то способ обработки "знакового разряда" большинства битовых операций зависит от конкретной машины. Кроме того, результат сдвига влево, изменяющего знаковый разряд, непредсказуем.
Поскольку нет никаких гарантий однозначного выполнения побитовых операторов со знаковыми переменными на разных машинах, настоятельно рекомендуется использовать в них только беззнаковые целочисленные значения.
Побитовые операторы сдвига
Мы уже использовали перегруженные версии операторов >> и <<, которые библиотека IO определяет для ввода и вывода. Однако первоначальное значение этих операторов — побитовый сдвиг операндов. Они возвращают значение, являющееся копией (возможно преобразованной) левого операнда, биты которого сдвинуты. Правый операнд не должен быть отрицательным, и его значение должно быть меньше количества битов результата. В противном случае операция имеет неопределенный результат. Биты сдвигаются влево (<<) или право (>>), при этом вышедшие за пределы биты отбрасываются.
Оператор сдвига влево (<<) (left-shift operator) добавляет нулевые биты справа. Поведение оператора сдвига вправо (>>) (right-shift operator) зависит от типа левого операнда: если он беззнаковый, то оператор добавляет слева нулевые биты; если он знаковый, то результат зависит от конкретной реализации: слева вставляются либо копии знакового разряда, либо нули.
В этих примерах подразумевается, что младший бит расположен справа, тип char содержит 8 битов, а тип int — 32 бита
// 0233 - восьмеричный литерал (см. раздел 2.1.3)
unsigned char bits = 0233; 1 0 0 1 1 0 1 1
bits << 8 // bits преобразуется в int и сдвигается влево на 8 битов
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1 0 1 1 0 0 0 0 0 0 0 0
bits << 31 // сдвиг влево на 31 бит отбрасывает крайние левые биты
1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
bits >> 3 // сдвиг вправо на 3 бита отбрасывает 3 крайних правых бита
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 1
Побитовый оператор NOT
Побитовый оператор NOT (~) (bitwise NOT operator) создает новое значение с инвертированными битами своего операнда. Каждый бит, содержащий 1, превращается в 0; каждый бит, содержащий 0, — в 1.
unsigned char bits = 0227; 1 0 0 1 0 1 1 1
~bits
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 0 1 0 0 0
Здесь операнд типа char сначала преобразуется в тип int. Это оставляет значение неизменным, но добавляет нулевые биты в позиции старших разрядов. Таким образом, преобразование в тип int добавляет 24 бита старших разрядов, заполненных нулями. Биты преобразованного значения инвертируются.
Побитовые операторы AND, OR и XOR
Побитовые операторы AND (&), OR (|) и XOR (^) создают новые значения с битовым шаблоном, состоящим из двух их операндов.
unsigned char b1 = 0145; 0 1 1 0 0 1 0 1
unsigned char b2 = 0257; 1 0 1 0 1 1 1 1
b1 & b2 Все 24 старших бита 0 0 0 1 0 0 1 0 1
b1 | b2 Все 24 старших бита 0 1 1 1 0 1 1 1 1
b1 ^ b2 Все 24 старших бита 0 1 1 0 0 1 0 1 0
Каждая битовая позиция результата побитового оператора AND (&) содержит 1, если оба операнда содержат 1 в этой позиции; в противном случае результат — 0. У побитового оператора OR (|) бит содержит 1, если один или оба операнда содержат 1; в противном случае результат — 0. Для побитового оператора XOR (^) бит содержит 1, если любой, но не оба операнда содержат 1; в противном случае результат — 0.
Побитовые и логические (см. раздел 4.3) операторы нередко путают. Например, путают побитовый оператор & с логическим &&, побитовый | с логическим || и побитовый ~ с логическим !.
Использование побитовых операторов
Рассмотрим пример использования побитовых операторов. Предположим, что есть класс с 30 учениками. Каждую неделю класс отвечает на контрольные вопросы с оценкой "сдано/не сдано". Результаты всех контрольных записываются, по одному биту на ученика, чтобы представить успешную оценку или нет. Каждую контрольную можно представить в виде беззнакового целочисленного значения.
unsigned long quiz1 = 0; // это значение используется
// как коллекция битов
Переменная quiz1 определена как unsigned long. Таким образом, на любой машине она будет содержать по крайней мере 32 бита. Переменная quiz1 инициализируется явно, чтобы ее значение было определено изначально.
Учитель должен быть способен устанавливать и проверять отдельные биты. Например, должна быть возможность установить бит, соответствующий ученику номер 27, означающий, что этот ученик сдал контрольную. Чтобы указать, что ученик 27 прошел контрольную, создадим значение, у которого установлен только бит номер 27. Если затем применить побитовый оператор OR к этому значению и значению переменной quiz1, то все биты, кроме бита 27, останутся неизменными.
В данном примере счет битов переменной quiz1 начинается с 0, соответствующего младшему биту, 1 соответствует следующему биту и т.д.
Чтобы получить значение, означающее, что ученик 27 сдал контрольную, используется оператор сдвига влево и целочисленный литерал 1 типа unsigned long (см. раздел 2.1.3).
1UL << 27 // создает значение только с одним установленным битом
// в позиции 27
Первоначально переменная 1UL имеет 1 в самом младшем бите и по крайней мере 31 нулевой бит. Она определена как unsigned long, поскольку тип int гарантированно имеет только 16 битов, а необходимо по крайней мере 27. Это выражение сдвигает 1 на 27 битовых позиций, вставляя в биты позади 0.
К этому значению и значению переменной quiz1 применяется оператор OR. Поскольку необходимо изменить значение самой переменной quiz1, используем составной оператор присвоения (см. раздел 4.4):
quiz1 |= 1UL << 27; // указать, что ученик номер 27 сдал контрольную
Оператор |= выполняется аналогично оператору +=.
quiz1 = quiz1 | 1UL << 27; // эквивалент quiz1 |= 1UL << 21;
Предположим, что учитель пересмотрел контрольные и обнаружил, что ученик 27 фактически списал работу. Теперь учитель должен сбросить бит 27 в 0. На сей раз необходимо целое число, бит 27 которого сброшен, а все остальные установлены в 1. Применение побитового AND к этому значению и значению переменной quiz1 позволяет сбросить только данный бит:
quiz1 &= ~(1UL << 27); // ученик номер 27 не прошел контрольную
Мы получаем значение со всеми установленными битами, кроме бита 27, инвертируя предыдущее значение. У него все биты были сброшены в 0, кроме бита 27, который был установлен в 1. Применение побитового NOT к этому значению сбросит бит 27, а все другие установит. Применение побитового AND к этому значению и значению переменной quiz1 оставит неизменными все биты, кроме бита 27.
И наконец, можно узнать, как дела у ученика 27:
bool status = quiz1 & (1UL << 27); // как дела у ученика 27?
Здесь оператор AND применяется к значению с установленным битом 27 и значением переменной quiz1. Результат отличен от нуля (т.е. истинен), если бит 27 в значении переменной quiz1 установлен; в противном случае он нулевой.
Операторы сдвига (они же ввода и вывода) имеют левосторонний порядок
Хотя многие программисты никогда не используют побитовые операторы непосредственно, почти все они использует их перегруженные версии в виде операторов ввода и вывода. Перегруженный оператор имеет тот же приоритет и порядок, что и его встроенная версия. Поэтому программисты должны иметь понятие о приоритете и порядке операторов сдвига, даже если они никогда не используют их встроенные версии.
Поскольку операторы сдвига имеют левосторонний порядок, выражение
cout << "hi" << " there" << endl;
выполняется так:
( (cout << "hi") << " there" ) << endl;
В этом операторе операнд "hi" группируется с первым символом <<. Его результат группируется со вторым, а его результат с третьим символом.
Приоритет операторов сдвига средний: ниже, чем у арифметических операторов, но выше, чем у операторов отношения, присвоения и условных операторов. Эти различия в уровнях приоритета свидетельствуют о том, что для правильной группировки операторов с более низким приоритетом следует использовать круглые скобки.
cout << 42 + 10; // ok: приоритет + выше, поэтому выводится сумма
cout << (10 < 42); // ok: группировку определяют скобки; выводится 1
cout << 10 < 42; // ошибка: попытка сравнить cout с 42!
Последний оператор cout интерпретируется так
(cout << 10) < 42;
Он гласит: "записать 10 в поток cout, а затем сравнить результат (т.е. поток cout) со значением 42".
Упражнения раздела 4.8
Упражнение 4.25. Каково значение выражения ~'q' << 6 на машине с 32-битовыми целыми числами и 8-битовыми символами, с учетом, что символ 'q' имеет битовое представление 01110001?
Упражнение 4.26. Что будет, если в приведенном выше примере оценки учеников использовать для переменной quiz1 тип unsigned int?
Упражнение 4.27. Каков результат каждого из этих выражений?
unsigned long ul1 = 3, ul2 = 7;
(a) ul1 & ul2 (b) ul1 | ul2
(c) ul1 && ul2 (d) ul1 || ul2
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОК