Зачем вообще нужно манипулировать битами? Ведь многие из нас предпочли бы этого не делать. “Возня с битами” относится к низкому уровню и открывает возможности для ошибок, поэтому, если у нас есть альтернатива, следует использовать ее. Однако биты настолько важны и полезны, что многие программисты не могут их игнорировать. Это может звучать довольно грозным и обескураживающим предупреждением, но оно хорошо продумано. Некоторые люди действительно любят
возиться с битами и байтами, поэтому следует помнить, что работа с битами иногда необходима (и даже может принести удовольствие), но ею не следует злоупотреблять. Процитируем Джона Бентли: “Люди, развлекающиеся с битами, будут биты” (“People who play with bits will be bitten”).
Итак, когда мы должны манипулировать битами? Иногда они являются естественными объектами нашей предметной области, поэтому естественными операциями в таких приложениях являются операции над битами. Примерами таких приложений являются индикаторы аппаратного обеспечения (“влаги”), низкоуровневые коммуникации (в которых мы должны извлекать значения разных типов из потока байтов), графика (в которой мы должны составлять рисунки из нескольких уровней образов) и кодирование (подробнее о нем — в следующем разделе).
Для примера рассмотрим, как извлечь (низкоуровневую) информацию из целого числа (возможно, из-за того, что мы хотим передать его как набор байтов через двоичный механизм ввода-вывода).
void f(short val) // пусть число состоит из 16 битов, т.е. 2 байта
{
unsigned char left = val>>8; // крайний левый
// (самый старший) байт
unsigned char right = val&0xff; // крайний правый
// (самый младший) байт
// ...
bool negative = val&0x8000; // знаковый бит
// ...
}
Такие операции не редкость. Они известны как “сдвиг и наложение маски” (“shift and mask”). Мы выполняем сдвиг (“shift”), используя операторы
<<
или
>>
, чтобы переместить требуемые биты вправо (в младшую часть слова), где ними легко манипулировать. Мы накладываем маску (“mask”), используя оператор “и” (
&
) вместе с битовой комбинацией (в данном случае
0xff
), чтобы исключить (установить равными нулю) биты, нежелательные в результате.
При необходимости именовать биты часто используются перечисления. Рассмотрим пример.
enum Printer_flags {
acknowledge=1,
paper_empty=1<<1,
busy=1<<2,
out_of_black=1<<3,
out_of_color=1<<4,
// ...
};
Этот код определяет перечисление, в котором каждый элемент равен именно тому значению, которому соответствует его имя.
Такие значения полезны, потому что они комбинируются совершенно независимо друг от друга.
unsigned char x = out_of_color | out_of_black; // x = 24 (16+8)
x |= paper_empty; // x = 26 (24+2)
Отметим,
что оператор
|=
можно прочитать как “установить бит” (или “установить некоторый бит”). Значит, оператор
&
можно прочитать как “установлен ли бит?” Рассмотрим пример.
if (x& out_of_color) { // установлен ли out_of_color? (Да, если
// установлен)
// ...
}
Оператор
&
по-прежнему можно использовать для наложения маски.
unsigned char y = x &(out_of_color | out_of_black); // y = 24
Теперь переменная
y
содержит копию битов из позиций 4 и 4 числа
x
(
out_of_color
и
out_of_black
).
Очень часть переменные типа
enum
используются как набор битов. При этом необходимо выполнить обратное преобразование, чтобы результат имел вид перечисления. Рассмотрим пример.
// необходимо приведение
Flags z = Printer_flags(out_of_color | out_of_black);
Приведение необходимо потому, что компилятор не может знать, что результат выражения
out_of_color | out_of_black
является корректным значением переменной типа
Flags
. Скептицизм компилятора обоснован: помимо всего прочего, ни один из элементов перечисления не имеет значения, равного 24 (
out_of_color | out_of_black
), но в данном случае мы знаем, что выполненное присваивание имеет смысл (а компилятор — нет).
25.5.5. Битовые поля
Как указывалось ранее, биты часто встречаются при программировании интерфейсов аппаратного обеспечения. Как правило, такие интерфейсы определяются как смесь битов и чисел, имеющих разные размеры. Эти биты и числа обычно имеют имена и стоят на заданных позициях в слове, которое часто называют регистром устройства (device register). В языке C++ есть специальные конструкции для работы с такими фиксированными схемами: битовые поля (bitfields). Рассмотрим номер страницы, используемый менеджером страниц глубоко внутри операционной системы. Вот как выглядит диаграмма, приведенная в руководстве по работе с операционной системой.
З2-битовое слово состоит из двух числовых полей (одно длиной 22 бита и другое — 3 бита) и четырех флагов (длиной один бит каждый). Размеры и позиции этих фрагментов фиксированы. Внутри слова существует даже неиспользуемое (и неименованное) поле. Эту схему можно описать с помощью следующей структуры: