O.2 – Побитовые операторы
Побитовые операторы
Для битовых манипуляций C++ предоставляет 6 операторов, часто называемых побитовыми операторами:
Оператор | Обозначение | Пример использования | Операция |
---|---|---|---|
Сдвиг влево | << | x << y | Все биты в x сдвигаются влево на количество бит, представленное значением y |
Сдвиг вправо | >> | x >> y | Все биты в x сдвигаются вправо на количество бит, представленное значением y |
Побитовое НЕ (NOT) | ~ | ~x | Все биты в x инвертируются |
Побитовое И (AND) | & | x & y | Выполняется операция И каждого бита в x с каждым соответствующим битом в y |
Побитовое ИЛИ (OR) | | | x | y | Выполняется операция ИЛИ каждого бита в x с каждым соответствующим битом в y |
Побитовое исключающее ИЛИ (XOR) | ^ | x ^ y | Выполняется операция исключающее ИЛИ каждого бита в x с каждым соответствующим битом в y |
Примечание автора
В следующих примерах мы в основном будем работать с 4-битными двоичными значениями. Это сделано для удобства и простоты примеров. В реальных программах количество используемых битов зависит от размера объекта (например, 2-байтовый объект будет хранить 16 бит).
Для удобства чтения мы также опустим префикс 0b
вне примеров кода (например, вместо 0b0101 мы будем писать просто 0101).
Операторы побитового сдвига влево (<<
) и побитового сдвига вправо (>>
)
Оператор побитового сдвига влево (<<
) сдвигает биты влево. Левый операнд – это выражение для сдвига битов, а правый операнд – это целое число, представляющее количество бит, на которое нужно выполнить сдвиг.
Поэтому, когда мы говорим x << 1
, мы говорим «сдвинуть биты в переменной x
влево на 1 место». Новые биты, сдвинутые с правой стороны, получают значение 0.
0011 << 1 равно 0110
0011 << 2 равно 1100
0011 << 3 равно 1000
Обратите внимание, что в третьем случае мы вытеснили бит с левого конца числа! Биты, сдвинутые с конца двоичного числа, теряются навсегда.
Оператор побитового сдвига вправо (>>
) сдвигает биты вправо.
1100 >> 1 равно 0110
1100 >> 2 равно 0011
1100 >> 3 равно 0001
Обратите внимание, что в третьем случае мы вытеснили бит с правого конца числа, поэтому он потерялся.
Вот пример выполнения побитового сдвига:
#include <bitset>
#include <iostream>
int main()
{
std::bitset<4> x { 0b1100 };
std::cout << x << '\n';
std::cout << (x >> 1) << '\n'; // сдвиг вправо на 1, получаем 0110
std::cout << (x << 1) << '\n'; // сдвиг влево на 1, получаем 1000
return 0;
}
Эта программа напечатает:
1100
0110
1000
Обратите внимание, что результаты применения операторов побитового сдвига к целому числу со знаком до C++20 зависели от компилятора.
Предупреждение
При использовании стандарта до C++20 не выполняйте сдвиг для значений целочисленных типов со знаком (и даже при новом стандарте, вероятно, всё же лучше использовать беззнаковые типы).
Что?! Разве operator<<
и operator>>
не используются для ввода и вывода?
Так и есть.
Сегодняшние программы обычно не очень часто используют операторы побитового сдвига влево и вправо для сдвига битов. Скорее, у вас больше шансов увидеть оператор побитового сдвига влево, используемый с std::cout
для вывода текста. Рассмотрим следующую программу:
#include <bitset>
#include <iostream>
int main()
{
unsigned int x { 0b0100 };
x = x << 1; // использует operator<< для сдвига влево
std::cout << std::bitset<4>{ x }; // использует operator<< для вывода
return 0;
}
Эта программа напечатает:
1000
Как в приведенной выше программе operator<<
знает, что в одном случае нужно сдвигать биты и выводить x
в другом случае? Ответ заключается в том, что std::cout
перегружает (предоставляет альтернативное определение) operator<<
, который выполняет вывод в консоль, а не сдвигает биты.
Когда компилятор видит, что левый операнд operator<<
– это std::cout
, он знает, что он должен вызвать версию operator<<
, которая перегружена std::cout
для вывода. Если левый операнд является целочисленным типом, то operator<<
знает, что он должен выполнять обычный сдвиг битов.
То же самое касается оператора operator>>
.
Обратите внимание, что если вы используете operator<<
как для вывода, так и для сдвига влево, необходимо использовать скобки:
#include <bitset>
#include <iostream>
int main()
{
std::bitset<4> x{ 0b0110 };
std::cout << x << 1 << '\n'; // выводим значение x (0110), затем 1
std::cout << (x << 1) << '\n'; // выводим x со сдвигом влево на 1 бит (1100)
return 0;
}
Эта программа печатает:
01101
1100
Первая строка печатает значение x
(0110), а затем литерал 1. Вторая строка печатает значение x
, сдвинутое влево на 1 бит (1100).
Мы поговорим больше о перегрузке операторов в следующем разделе, включая обсуждение того, как перегружать операторы для ваших собственных целей.
Побитовое НЕ (NOT)
Оператор побитовое НЕ (~
), пожалуй, самый простой для понимания из всех побитовых операторов. Он просто инвертирует каждый бит с 0 на 1, или наоборот. Обратите внимание, что результат побитового НЕ зависит от размера вашего типа данных.
Инвертирование 4-битного значения:
~0100 равно 1011
Инвертирование 8-битного значения:
~0000 0100 равно 1111 1011
И в 4-битном, и в 8-битном случаях мы начинаем с одного и того же числа (двоичное 0100 соответствует 0000 0100 точно так же, как десятичное 7 соответствует 07), но в конечном итоге мы получаем другой результат.
Мы можем увидеть это в действии в следующей программе:
#include <bitset>
#include <iostream>
int main()
{
std::cout << std::bitset<4>{ ~0b0100u } << ' ' << std::bitset<8>{ ~0b0100u };
return 0;
}
Эта программа напечатает:
1011 11111011
Побитовое ИЛИ (OR)
Побитовое ИЛИ (|
) работает аналогично логическому ИЛИ. Однако вместо применения ИЛИ к операндам для получения единственного результата побитовое ИЛИ применяется к каждому биту! Например, рассмотрим выражение 0b0101 | 0b0110.
Для выполнения (любых) побитовых операций проще всего выровнять два операнда следующим образом:
0 1 0 1 ИЛИ
0 1 1 0
а затем применить операцию к каждому столбцу битов.
Если вы помните, логическое ИЛИ вычисляется как true
(1), если левый, правый или оба операнда равны true
(1), и 0 в противном случае. Побитовое ИЛИ вычисляется как 1, если левый, правый или оба бита равны 1, и 0 в противном случае. Следовательно, выражение вычисляется так:
0 1 0 1 ИЛИ
0 1 1 0
-------
0 1 1 1
Наш результат – 0111 в двоичном формате.
#include <bitset>
#include <iostream>
int main()
{
std::cout << (std::bitset<4>{ 0b0101 } | std::bitset<4>{ 0b0110 });
return 0;
}
Эта программа напечатает:
0111
То же самое можно сделать и с составными выражениями ИЛИ, например 0b0111 | 0b0011 | 0b0001. Если какой-либо из битов в столбце равен 1, результатом этого столбца будет 1.
0 1 1 1 ИЛИ
0 0 1 1 ИЛИ
0 0 0 1
--------
0 1 1 1
А вот код для этого примера:
#include <bitset>
#include <iostream>
int main()
{
std::cout << (std::bitset<4>{ 0b0111 } | std::bitset<4>{ 0b0011 } | std::bitset<4>{ 0b0001 });
return 0;
}
Эта программа напечатает:
0111
Побитовое И (AND)
Побитовое И (&
) работает аналогично предыдущему оператору. Логическое И вычисляется как true
, если и левый, и правый операнды имеют значение true
. Побитовое И принимает значение true
(1), если оба бита в столбце равны 1. Рассмотрим выражение 0b0101 & 0b0110. Выравнивание каждого из битов и применение операции И к каждому столбцу битов:
0 1 0 1 И
0 1 1 0
--------
0 1 0 0
#include <bitset>
#include <iostream>
int main()
{
std::cout << (std::bitset<4>{ 0b0101 } & std::bitset<4>{ 0b0110 });
return 0;
}
Эта программа напечатает:
0100
Точно так же мы можем сделать то же самое с составными выражениями И, такими как 0b0001 & 0b0011 & 0b0111. Если все биты в столбце равны 1, результатом этого столбца будет 1.
0 0 0 1 И
0 0 1 1 И
0 1 1 1
--------
0 0 0 1
#include <bitset>
#include <iostream>
int main()
{
std::cout << (std::bitset<4>{ 0b0001 } & std::bitset<4>{ 0b0011 } & std::bitset<4>{ 0b0111 });
return 0;
}
Эта программа напечатает:
0001
Побитовое исключающее ИЛИ (XOR)
Последний оператор – это побитовое исключающее ИЛИ (^
), также известное как XOR.
При вычислении исключающего ИЛИ с двумя операндами оно вычисляется как true
(1), если один и только один из его операндов равен true
(1). Если ни один из них или оба равны true
, оно вычисляется как 0. Рассмотрим выражение 0b0110 ^ 0b0011:
0 1 1 0 XOR
0 0 1 1
-------
0 1 0 1
По столбцам также возможно вычислить составное выражение исключающее ИЛИ, например 0b0001 ^ 0b0011 ^ 0b0111. Если в столбце четное количество битов, равное 1, результатом будет 0. Если в столбце нечетное количество битов, равное 1, результатом будет 1.
0 0 0 1 XOR
0 0 1 1 XOR
0 1 1 1
--------
0 1 0 1
Побитовые операторы присваивания
Подобно арифметическим операторам присваивания, C++, чтобы упростить изменение переменных, предоставляет побитовые операторы присваивания.
Оператор | Обозначение | Пример использования | Операция |
---|---|---|---|
Присваивание со сдвигом влево | <<= | x <<= y | Сдвигает x влево на количество бит, представленное значением y |
Присваивание со сдвигом вправо | >>= | x >>= y | Сдвигает x вправо на количество бит, представленное значением y |
Присваивание с побитовым И (AND) | &= | x &= y | Присваивает результат выполнения x & y переменной x |
Присваивание с побитовым ИЛИ (OR) | |= | x |= y | Присваивает результат выполнения x | y переменной x |
Присваивание с побитовым исключающее ИЛИ (XOR) | ^= | x ^= y | Присваивает результат выполнения x ^ y переменной x |
Например, вместо записи x = x >> 1;
вы можете написать x >>= 1;
.
#include <bitset>
#include <iostream>
int main()
{
std::bitset<4> bits { 0b0100 };
bits >>= 1;
std::cout << bits;
return 0;
}
Эта программа напечатает:
0010
Резюме
Обобщая, как вычислять побитовые операции с использованием метода «в столбик»:
- при вычислении побитового ИЛИ, если какой-либо бит в столбце равен 1, результат для этого столбца равен 1;
- при вычислении побитового И, если все биты в столбце равны 1, результатом для этого столбца будет 1;
- при вычислении побитового исключающего ИЛИ, если в столбце нечетное количество битов, равных 1, результатом для этого столбца будет 1.
В следующем уроке мы рассмотрим, как для облегчения битовых манипуляций эти операторы могут использоваться в сочетании с битовыми масками.
Небольшой тест
Вопрос 1
a) Что дает выражение 0110 >> 2 в двоичном формате?
Ответ
0110 >> 2 вычисляется как 0001
b) Чему в двоичном формате равно следующее выражение: 0011 | 0101?
Ответ
0 0 1 1 ИЛИ
0 1 0 1
--------
0 1 1 1
c) Чему в двоичном формате равно следующее выражение: 0011 & 0101?
Ответ
0 0 1 1 И
0 1 0 1
--------
0 0 0 1
d) Чему в двоичном формате равно следующее выражение: (0011 | 0101) & 1001?
Ответ
В скобках:
0 0 1 1 ИЛИ
0 1 0 1
--------
0 1 1 1
Далее:
0 1 1 1 И
1 0 0 1
--------
0 0 0 1
Вопрос 2
Побитовое вращение похоже на побитовый сдвиг, за исключением того, что любые биты, сдвинутые с одного конца, добавляются обратно в другой конец. Например, 0b1001 << 1 будет равно 0b0010, но вращение влево на 1 бит приведет в результате к 0b0011. Реализуйте функцию, которая выполняет вращение влево на std::bitset<4>
. Для этого можно использовать функции test()
и set()
.
Должен выполняться следующий код:
#include <bitset>
#include <iostream>
// "rotl" означает "вращать влево"
std::bitset<4> rotl(std::bitset<4> bits)
{
// здесь идет ваш код
}
int main()
{
std::bitset<4> bits1{ 0b0001 };
std::cout << rotl(bits1) << '\n';
std::bitset<4> bits2{ 0b1001 };
std::cout << rotl(bits2) << '\n';
return 0;
}
и печататься следующее:
0010
0011
Ответ
#include <bitset>
#include <iostream>
std::bitset<4> rotl(std::bitset<4> bits)
{
const bool leftbit{ bits.test(3) };
bits <<= 1; // выполнить сдвиг влево
if (leftbit)
bits.set(0);
return bits;
}
int main()
{
std::bitset<4> bits1{ 0b0001 };
std::cout << rotl(bits1) << '\n';
std::bitset<4> bits2{ 0b1001 };
std::cout << rotl(bits2) << '\n';
return 0;
}
Мы назвали функцию "rotl
", а не "rotateLeft
", потому что "rotl
" – это хорошо известное в информатике имя, а также имя стандартной функции std::rotl
.
Вопрос 3
Дополнительное задание: повторите задание 2, но не используйте функции test
и set
.
Ответ
#include <bitset>
#include <iostream>
std::bitset<4> rotl(std::bitset<4> bits)
{
// bits << 1 выполняет сдвиг влево
// bits >> 3 обрабатывает перенос самого левого бита
return (bits<<1) | (bits>>3);
}
int main()
{
std::bitset<4> bits1{ 0b0001 };
std::cout << rotl(bits1) << '\n';
std::bitset<4> bits2{ 0b1001 };
std::cout << rotl(bits2) << '\n';
return 0;
}