Объединение (union) позволяет хранить несколько переменных в одной области памяти. Объявляется так же, как структура, но с использованием ключевого слова union:
union Union {
uint32_t a; // 4 байта
float b; // 4 байта
};
Union u; // выделилась память 4 байта - ОДНА НА ВСЕХ
Отличие объединения от структуры состоит в том, что все переменные привязаны к одной общей области памяти: записав значение в одну переменную, мы автоматически записываем его во все остальные в бинарном виде. Объединения могут использоваться для "экономии места" - создали одну ячейку и используем её как разные типы по очереди:
u.a = 123;
u.b = 3.14;
Что по сути бессмысленно - сейчас даже у дешёвых и слабых МК достаточно памяти, чтобы так не изголяться
Интересное начинается, когда мы решим прочитать переменную, которая была записана не последней:
u.b = 3.14;
// здесь u.a == 1078523331
Таким образом мы попытались прочитать память через другой тип, не тот, который был записан. В C это разрешённый приём, а в стандартном C++ чтение неактивного члена union в общем случае приводит к неопределённому поведению. GCC обычно поддерживает такой type punning как расширение, поэтому на Arduino пример даёт целочисленное представление float числа 3.14.
То же самое можно сделать, просто преобразовав адрес переменной в другой нужный тип: float f = 3.14; uint32_t* raw = (uint32_t*)&f;. Любую подобную задачу с union можно решить без него, это просто вопрос стиля и оформления
Универсальный пакет #
На union можно удобно сделать лёгкий пакет данных под разные форматы - хранить сами данные разных типов в общем union и отдельно - тип пакета, например как enum. Это позволяет использовать одну и ту же память для хранения всех пакетов и не раздувать код ООП и полиморфизмом:
enum PType {
Array,
Packet1,
MyInt,
};
struct Packet1_t {
int a, b;
};
struct Packet {
PType type;
union {
int arr[10];
Packet1_t packet;
int myint;
} data;
};
Распаковка и запаковка #
На GCC для Arduino при помощи union часто раскладывают данные на байты, но для переносимого C++ безопаснее читать представление объекта через указатель на uint8_t или unsigned char:
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];
};
Следующий пример рассчитан именно на расширения и правила размещения GCC для выбранной платформы. Для переносимого протокола лучше явно собирать поля сдвигами и масками или копировать байты через memcpy, а формат закрепить тестами и документацией.
Порядок размещения битовых полей зависит от компилятора и архитектуры - такую структуру нельзя считать переносимым описанием внешнего протокола
Unpack up;
// записываем байты в up.bytes. Для наглядности - вручную, смотрите таблицу протокола
up.bytes[0] = 0b11100000; // для GCC AVR: id 0, address 28
up.bytes[1] = 0b11001111; // для GCC AVR: n1 3, n2 3, n3 12
up.bytes[2] = 0b10100101;
up.bytes[3] = 0b11111111;
up.p.id; // 0 для GCC AVR
up.p.address; // 28
up.p.n1; // 3
up.p.n2; // 3
up.p.n3; // 12
up.p.f1; // 1
up.p.f2; // 0
up.p.crc; // 11111111
Это работает и в обратную сторону - можно записать нужные значения в структуру и получить её в виде набора байтов. Передаём на другое устройство - и оно точно так же их распаковывает. С другой стороны, union в данном примере не очень то и нужен - можно ведь просто записать данные в экземпляр структуры, преобразовав его адрес к uint8_t*.
Дополнительно #
Дополнительный контент доступен владельцам набора GyverKIT и по подписке, подробнее читай здесь. Блок содержит:
- Тезисы, Примеры (Arduino)
- 1 блоков кода
Полезные страницы #
- Набор GyverKIT – наш большой стартовый набор Arduino, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту (alex@alexgyver.ru)
- Поддержать автора за работу над уроками