O.2 – Побитовые операторы

Добавлено 8 мая 2021 в 23:55

Побитовые операторы

Для битовых манипуляций 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;
}

Теги

C++ / CppLearnCppstd::bitsetБитовые манипуляцииДля начинающихОбучениеОператор (программирование)Перегрузка (программирование)Программирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.