Битовые поля в C: компактное хранение флагов и малых целых чисел
Зачем нужны битовые поля?
Битовые поля (bit fields) позволяют упаковать несколько небольших целочисленных значений в одно машинное слово, экономя память. Часто используются для:
- Флагов (boolean: 0/1) — 1 бит каждый.
- Малых целых (0-7, 0-15 и т.д.) — 3, 4 бита.
- Сетевых протоколов, файловых форматов, регистров железа.
- Компактных структур данных (например, в ядре ОС).
Синтаксис
struct {
тип имя : ширина; // ширина в битах
};
- Тип:
int,signed int,unsigned int(C23: любой целочисленный тип). - Имя: идентификатор (может отсутствовать — padding).
- Ширина: константа времени компиляции, 1..размер типа в битах.
Пример: флаги состояния
struct FileFlags {
unsigned int readable : 1;
unsigned int writable : 1;
unsigned int executable : 1;
unsigned int is_dir : 1;
unsigned int reserved : 28; // заполнение до 32 бит
}; // размер: 4 байта (вместо 4х int = 16 байт)
struct FileFlags f = { .readable = 1, .writable = 1 };
if (f.readable && f.writable) { /* read-write */ }
Пример: компактное хранение даты
struct Date {
unsigned int day : 5; // 1-31 (5 бит = 0-31)
unsigned int month : 4; // 1-12 (4 бита = 0-15)
unsigned int year : 12; // 0-4095 (12 бит)
// всего 21 бит → упаковано в 4 байта (32 бита)
};
Правила и ограничения
- Нельзя взять адрес битового поля:
&f.readable— ошибка компиляции. - Массивы битовых полей запрещены.
- Порядок размещения бит в памяти — implementation-defined (обычно от младших к старшим или наоборот, зависит от endianness и ABI). Не переносим между платформами.
- Выравнивание: битовые поля не пересекают границу своего базового типа (обычно int/unsigned int). Новое поле начинается в следующем слове, если не помещается.
- Безымянные поля (пропуски):
unsigned int : 3;— 3 бита padding.: 0— выравнивание на следующую границу слова. - Знаковые битовые поля — поведение отрицательных значений implementation-defined (обычно two's complement). Лучше использовать
unsigned.
Пример: демонстрация размера и упаковки
#include <stdio.h>
struct Flags1 {
unsigned int a : 1;
unsigned int b : 1;
unsigned int c : 1;
};
struct Flags2 {
unsigned int a : 1;
unsigned int b : 1;
unsigned int c : 1;
unsigned int d : 1;
// ... 32 поля по 1 биту
unsigned int z : 1;
};
struct Flags3 {
unsigned int a : 1;
unsigned int b : 1;
unsigned int c : 1;
unsigned int : 0; // принудительное выравнивание на след. int
unsigned int d : 1;
};
int main() {
printf("Flags1 (3 бита): %zu байт\n", sizeof(struct Flags1)); // 4
printf("Flags2 (32 бита): %zu байт\n", sizeof(struct Flags2)); // 4
printf("Flags3 (с :0): %zu байт\n", sizeof(struct Flags3)); // 8 (два int)
return 0;
}
Инициализация и использование
struct Config {
unsigned int enabled : 1;
unsigned int level : 3; // 0-7
unsigned int mode : 2; // 0-3
} cfg = { .enabled = 1, .level = 5, .mode = 2 };
cfg.level = 7; // OK
// cfg.level = 8; // UB: значение не помещается в 3 бита (обычно обрезается модуль 8)
printf("enabled=%u, level=%u, mode=%u\n", cfg.enabled, cfg.level, cfg.mode);
Битовые поля vs битовые операции
| Подход | Плюсы | Минусы |
|---|---|---|
| Битовые поля | Читаемый код, именованные поля, типо-безопасность | Непереносимый layout, нет адреса, ограниченные типы |
Битовые маски (&, |, ^, <<) | Полный контроль, переносимо, работает с любыми типами | Магические числа,易出错, менее читаемо |
Рекомендация: для внутренних флагов структуры — битовые поля. Для протоколов/файлов/железа — явные маски и сдвиги (или packed struct + битовые поля с осторожностью).
C23 улучшения
- Битовые поля могут иметь любой целочисленный тип (
uint8_t,int16_tи т.д.), не толькоint. _BitInt(N)— целые типа произвольной битности (GCC/Clang extension, стандартизируется).
Пример: регистр периферии (embedded)
// Регистр управления UART (гипотетический)
union UART_CTRL {
uint32_t reg;
struct {
unsigned int enable : 1; // бит 0
unsigned int tx_enable : 1; // бит 1
unsigned int rx_enable : 1; // бит 2
unsigned int parity_en : 1; // бит 3
unsigned int parity_odd : 1; // бит 4
unsigned int stop_bits : 1; // бит 5 (0=1 stop, 1=2 stop)
unsigned int data_bits : 2; // биты 6-7 (0=5, 1=6, 2=7, 3=8)
unsigned int reserved : 24; // биты 8-31
} bits;
};
// Использование:
union UART_CTRL ctrl = { .reg = 0 };
ctrl.bits.enable = 1;
ctrl.bits.tx_enable = 1;
ctrl.bits.rx_enable = 1;
ctrl.bits.data_bits = 3; // 8 бит
// Запись в регистр: UART->CTRL = ctrl.reg;
Язык C
- Освоение побитовых и сдвиговых операторов в C#: полное руководство
- Четыре проверенных паттерна пользовательских функций в C
- Как передавать одномерные и двумерные массивы в функции в C — практическое руководство
- Освоение структур и указателей в C: практическое руководство
- Ключевое слово this в Java: практическое применение, конструкторы и примеры
- Инкапсуляция в Java: практическое руководство по скрытию данных
- Структуры (struct) в C++: объяснение с практическим примером
- Функции в C: объявление, определение, вызов, аргументы (по значению и по ссылке), рекурсия
- Функции с переменным числом аргументов в C: практическое руководство
- Интегрирование в MATLAB: от неопределенных интегралов к суммам Римана