Объединение (union) позволяет хранить несколько переменных в одной области памяти. Объявляется так же, как структура, но с использованием ключевого слова union
:
union Union {
int a; // 4 байта
float b; // 4 байта
};
Union u; // выделилась память 4 байта - ОДНА НА ВСЕХ
Отличие объединения от структуры состоит в том, что все переменные привязаны к одной общей области памяти: записав значение в одну переменную, мы автоматически записываем его во все остальные в бинарном виде. Объединения могут использоваться для "экономии места" - создали одну ячейку и используем её как разные типы по очереди:
u.a = 123;
u.b = 3.14;
Что по сути бессмысленно - сейчас даже у дешёвых и слабых МК достаточно памяти, чтобы так не изголяться
Интересное начинается, когда мы решим прочитать переменную, которая была записана не последней:
u.b = 3.14;
// здесь u.a == 1078523331
Таким образом мы получили доступ к памяти через другой тип, не тот, который был записан. В примере выше получилось целочисленное представление float
числа 3.14
.
Распаковка и запаковка #
Всё станет ещё интереснее, если подключить сюда структуры и/или массивы - можно очень быстро конвертироваться между любыми типами данных в бинарном виде!
union {
uint32_t u32;
uint8_t bytes[4];
} u;
u.u32 = 0xaabbccdd;
u.bytes[0]; // == 0xdd
u.bytes[1]; // == 0xcc
u.bytes[2]; // == 0xbb
u.bytes[3]; // == 0xaa
// порядок зависит от endianness архитектуры
Объединения позволяют писать очень эффективные по скорости выполнения, а самое главное - очень читаемые алгоритмы для разбора бинарных данных. Например, нам приходит пакет данных, запакованных следующим образом:
Данные приходят в виде условного массива - просто несколько байт. Чтобы распаковать такой пакет, конечно можно использовать битовые операции: посдвигать, повыделять по маске, поскладывать и в итоге записать в отдельные переменные, чтобы оно было читаемо в программе - отличная задачка для ума. Но довольно многословная и неэффективная. Более того, если протокол изменится - это всё придётся переписывать заново!
Гораздо красивее будет сделать структуру с битовыми полями, запаковать её, чтобы она не выравнивалась с дырками, создать на её основе union
в паре с массивом и... просто получить результат!
struct __attribute__((packed)) Packet {
uint8_t id : 3;
uint8_t address : 5;
uint8_t n1 : 2;
uint8_t n2 : 2;
uint8_t n3 : 4;
uint8_t f1 : 1;
uint8_t f2 : 1;
uint8_t f3 : 1;
uint8_t f4 : 1;
uint8_t f5 : 1;
uint8_t f6 : 1;
uint8_t f7 : 1;
uint8_t f8 : 1;
uint8_t crc;
};
union Unpack {
Packet p;
uint8_t bytes[4];
};
Обратите внимание на порядок полей - он обратный, так как поля перечисляются от младших к старшим битам
Unpack up;
// записываем байты в up.bytes. Для наглядности - вручную, смотрите таблицу протокола
up.bytes[0] = 0b11100000; // id: 111, address: 0
up.bytes[1] = 0b11001111; // n1: 11, n2: 0, n3: 1111
up.bytes[2] = 0b10100101;
up.bytes[3] = 0b11111111;
up.p.id; // 111
up.p.address; // 0
up.p.n1; // 11
up.p.n2; // 0
up.p.n3) // 111
up.p.f1; // 1
up.p.f2; // 0
up.p.crc; // 11111111
Это работает и в обратную сторону - можно записать нужные значения в структуру и получить её в виде набора байтов. Передаём на другое устройство - и оно точно так же их распаковывает.