ПрограммированиеВстраиваемый C-инженер

Как работают побитовые операторы (&, |, ^, ~, <<, >>) в языке C? В чём их специфика при работе с типами разной длины и знаком, и какие распространённые ошибки делают разработчики при их использовании?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

Побитовые операторы управляют отдельными битами целых числовых типов:

  • & — побитовое И
  • | — побитовое ИЛИ
  • ^ — побитовое исключающее ИЛИ
  • ~ — побитовое НЕ
  • << — сдвиг влево
  • >> — сдвиг вправо

Особенности:

  • Операторы работают только с целочисленными типами (int, unsigned int, и т.п.).
  • Знаковые числа (signed) при сдвигах вправо (>>) могут порождать арифметическое или логическое смещение — зависит от компилятора.
  • При сдвигах на количество битов, превышающее разрядность переменной, происходит неопределенное поведение.
  • Для надёжной работы часто выбирают unsigned типы, чтобы избежать расширения знака.

Пример:

unsigned int flags = 0; flags |= 0x1; // Установить 0-й бит flags &= ~0x2; // Сбросить 1-й бит if ((flags & 0x4) != 0) { /* ... */ } // Проверить 2-й бит

Вопрос с подвохом.

Чем отличается операция сдвига вправо (>>) для типов signed int и unsigned int?

Часто ошибочный ответ: Считают, что сдвиг вправо всегда вставляет нули слева, независимо от знаковости.

Правильный ответ: Для типа unsigned int сдвиг вправо (>>) всегда вставляет нули. Для signed int вставляется либо знак (единицы, если число отрицательное), либо нули — зависит от реализации компилятора (архи­тек­тура и правила стандарта C).

Пример:

signed int a = -8; unsigned int b = (unsigned int)a; printf("%d ", a >> 1); printf("%u ", b >> 1);

В первом случае результат зависит от компилятора; во втором всегда будет логический сдвиг с нулями.

Примеры реальных ошибок из-за незнания тонкостей темы.


История

В коде обработки протокола сигнальные флаги сохранялись в типе char. Программист применил сдвиг на 8 бит (flag << 8), что из-за переполнения и правил повышения типов привело к потере всех данных — result был всегда равен нулю.


История

Чтение данных из сетевого протокола (big-endian). Использование побитовых операций для объединения байтов не сопровождалось приведением к unsigned, что иногда приводило к неожиданным отрицательным значениям при чтении поля структуры.


История

Использование ~ (побитового НЕ) для сброса битов в значении типа int (например, ~0x80) принималось за 0x7F, но в реальности получалось отрицательное число -129, что приводило к ошибкам при последующих вычислениях и логических проверках.