O.1 – Битовые флаги и битовые манипуляции с помощью std::bitset
В современных компьютерных архитектурах наименьшей адресуемой единицей памяти является байт. Поскольку все объекты должны иметь уникальные адреса памяти, это означает, что объекты должны быть размером не менее одного байта. Для большинства типов переменных это нормально. Однако для логических значений это немного расточительно. Логические типы имеют только два состояния: true
(1) или false
(0). Для этого набора состояний требуется только один бит. Однако если переменная должна быть как минимум байтом, а байт равен 8 битам, это означает, что логическое значение использует 1 бит, а остальные 7 остаются неиспользованными.
В большинстве случаев это нормально – обычно мы не настолько ограничены в памяти, чтобы заботиться о 7 потерянных битах (мы лучше займемся оптимизацией для удобства понимания и поддержки). Однако в некоторых случаях с интенсивным хранением для повышения эффективности хранения может быть полезно «упаковать» 8 отдельных логических значений в один байт.
Для этого требуется, чтобы мы могли манипулировать объектами на битовом уровне. К счастью, C++ дает нам для этого инструменты. Изменение отдельных битов внутри объекта называется битовой манипуляцией.
Битовые манипуляции также полезны в алгоритмах шифрования и сжатия.
Примечание автора
Вся эта глава не обязательна для чтения. Не стесняйтесь её пропустить и вернуться позже.
Битовые флаги
До этого момента мы использовали переменные для хранения одиночных значений:
int foo { 5 }; // присваиваем foo значение 5 (вероятно, используется 32 бита памяти)
std::cout << foo; // выводим значение 5
Однако вместо того, чтобы рассматривать объекты как содержащие одно значение, мы можем вместо этого рассматривать их как набор отдельных битов. Когда отдельные биты объекта используются как логические значения, биты называются битовыми флагами.
В качестве отступления...
В вычислениях флаг – это значение, которое действует как сигнал для некоторой функции или процесса. Для аналогии, в реальной жизни флаг почтового ящика используется, чтобы сигнализировать о том, что внутри почтового ящика что-то есть, и поэтому почтовый ящик не нужно открывать для проверки.
Чтобы определить набор битовых флагов, мы обычно используем целочисленный тип без знака соответствующего размера (8 бит, 16 бит, 32 бита и т.д., в зависимости от того, сколько у нас флагов) или std::bitset
.
#include <bitset> // для std::bitset
std::bitset<8> mybitset {}; // размер 8 бит означает место для 8 флагов
Лучшая практика
Битовые манипуляции – это один из немногих случаев, когда вы однозначно должны использовать беззнаковые целочисленные типы (или std::bitset
).
В этом уроке мы покажем, как легко манипулировать битами с помощью std::bitset
. В следующем наборе уроков мы узнаем, как сделать это более сложным, но универсальным способом.
Нумерация битов и позиции битов
Учитывая последовательность битов, мы обычно нумеруем их справа налево, начиная с 0 (а не с 1). Каждое число обозначает позицию бита.
76543210 Позиция бита
00000101 Битовая последовательность
В данной последовательности битов 0000 0101, биты, которые находятся в позициях 0 и 2, имеют значение 1, а остальные биты имеют значение 0.
Управление битами через std::bitset
В уроке «4.13 – Литералы» мы уже показали, как использовать std::bitset
для печати значений в двоичном формате. Однако это не единственная полезная вещь, которую может делать std::bitset
.
std::bitset
предоставляет 4 ключевые функции, которые полезны для работы с битами:
test()
позволяет нам узнать, равен ли бит 0 или 1;set()
позволяет нам установить бит в 1 (она ничего не сделает, если бит уже равен 1);reset()
позволяет нам сбросить бит в 0 (она ничего не даст, если бит уже равен 0);flip()
позволяет нам инвертировать значение бита с 0 на 1, или наоборот.
Каждая из этих функций в качестве своего единственного аргумента принимает позицию бита, с которым мы хотим работать.
Вот пример:
#include <bitset>
#include <iostream>
int main()
{
std::bitset<8> bits{ 0b0000'0101 }; // нам нужно 8 бит, начнем с битовой комбинации 0000 0101
bits.set(3); // устанавливаем бит в позиции 3 в 1 (теперь мы имеем 0000 1101)
bits.flip(4); // инвертируем бит 4 (теперь мы имеем 0001 1101)
bits.reset(4); // сбрасываем бит в позиции 4 обратно в 0 (теперь мы имеем 0000 1101)
std::cout << "All the bits: " << bits << '\n';
std::cout << "Bit 3 has value: " << bits.test(3) << '\n';
std::cout << "Bit 4 has value: " << bits.test(4) << '\n';
return 0;
}
Эта программа напечатает:
All the bits: 00001101
Bit 3 has value: 1
Bit 4 has value: 0
Что, если мы хотим получить или установить сразу несколько битов
std::bitset
не упрощает эту задачу. Для этого или, если вместо std::bitset
мы хотим использовать битовые флаги в значениях целочисленных типов без знака, нам нужно обратиться к более традиционным методам. Мы рассмотрим их в следующих нескольких уроках.