Зачем нам нужны Союзы C?

236

Когда следует использовать профсоюзы? Зачем они нам нужны?

timrau
источник

Ответы:

252

Объединения часто используются для преобразования между двоичными представлениями целых чисел и чисел с плавающей точкой:

union
{
  int i;
  float f;
} u;

// Convert floating-point bits to integer:
u.f = 3.14159f;
printf("As integer: %08x\n", u.i);

Хотя это технически неопределенное поведение в соответствии со стандартом C (вы должны только читать поле, которое было написано совсем недавно), оно будет действовать четко определенным образом практически в любом компиляторе.

Объединения также иногда используются для реализации псевдополиморфизма в C, давая структуре некоторый тег, указывающий, какой тип объекта он содержит, и затем объединяет возможные типы:

enum Type { INTS, FLOATS, DOUBLE };
struct S
{
  Type s_type;
  union
  {
    int s_ints[2];
    float s_floats[2];
    double s_double;
  };
};

void do_something(struct S *s)
{
  switch(s->s_type)
  {
    case INTS:  // do something with s->s_ints
      break;

    case FLOATS:  // do something with s->s_floats
      break;

    case DOUBLE:  // do something with s->s_double
      break;
  }
}

Это позволяет размер struct Sбыть только 12 байтов вместо 28.

Адам Розенфилд
источник
вместо uf должен быть uy
Амит Сингх Томар
1
Работает ли пример, который предполагает преобразовать float в целое число? Я так не думаю, поскольку int и float хранятся в памяти в разных форматах. Можете ли вы объяснить свой пример?
spin_eight
3
@spin_eight: это не «преобразование» из числа с плавающей точкой в ​​int. Больше похоже на «реинтерпретацию двоичного представления с плавающей точкой, как если бы оно было int». Вывод не 3: ideone.com/MKjwon Я не уверен, почему Адам печатает как шестнадцатеричный, хотя.
эндолит
@ Адам Розенфилд, я не совсем понял преобразование, я не получаю целое число в выводе: p
Зверь
2
Я чувствую, что отказ от ответственности за неопределенное поведение должен быть удален. Это, по сути, определенное поведение. См. Сноску 82 стандарта C99: если элемент, используемый для доступа к содержимому объекта объединения, не совпадает с элементом, который последний раз использовался для хранения значения в объекте, соответствующая часть представления объекта значения интерпретируется как представление объекта в новом типе, как описано в 6.2.6 (процесс, который иногда называют «типом наказания»). Это может быть представление ловушки.
Кристиан Гиббонс
136

Объединения особенно полезны во встроенном программировании или в ситуациях, когда необходим прямой доступ к оборудованию / памяти. Вот тривиальный пример:

typedef union
{
    struct {
        unsigned char byte1;
        unsigned char byte2;
        unsigned char byte3;
        unsigned char byte4;
    } bytes;
    unsigned int dword;
} HW_Register;
HW_Register reg;

Затем вы можете получить доступ к регистру следующим образом:

reg.dword = 0x12345678;
reg.bytes.byte3 = 4;

Endianness (порядок байтов) и архитектура процессора, конечно, важны.

Еще одна полезная функция - модификатор битов:

typedef union
{
    struct {
        unsigned char b1:1;
        unsigned char b2:1;
        unsigned char b3:1;
        unsigned char b4:1;
        unsigned char reserved:4;
    } bits;
    unsigned char byte;
} HW_RegisterB;
HW_RegisterB reg;

С помощью этого кода вы можете получить прямой доступ к одному биту в адресе регистра / памяти:

x = reg.bits.b2;
kgiannakakis
источник
3
Ваш ответ здесь вместе с ответом @Adam Rosenfield выше составляет идеальную комплементарную пару: вы демонстрируете использование структуры внутри объединения , а он демонстрирует использование объединения внутри структуры . Оказывается, мне нужно и то и другое одновременно: структура внутри объединения внутри структуры для реализации какого-то необычного полиморфизма передачи сообщений в C между потоками во встроенной системе, и я бы не понял, что если бы я не видел оба ваших ответа вместе ,
Габриэль Стейплз
1
Я был неправ: это объединение внутри структуры внутри объединения внутри структуры, вложенное в левую часть, чтобы написать, как я написал, от самого внутреннего вложения до самого внешнего уровня. Мне пришлось добавить еще одно объединение на самом внутреннем уровне, чтобы разрешить значения разных типов данных.
Габриэль Стейплз
64

Низкоуровневое системное программирование является разумным примером.

IIRC, я использовал объединения, чтобы разбить аппаратные регистры на биты компонентов. Таким образом, вы можете получить доступ к 8-битному регистру (как это было в тот день, когда я это сделал ;-) в биты компонента.

(Я забыл точный синтаксис, но ...) Эта структура позволила бы получить доступ к регистру управления как control_byte или через отдельные биты. Было бы важно обеспечить отображение битов на правильные регистровые биты для заданного порядка байтов.

typedef union {
    unsigned char control_byte;
    struct {
        unsigned int nibble  : 4;
        unsigned int nmi     : 1;
        unsigned int enabled : 1;
        unsigned int fired   : 1;
        unsigned int control : 1;
    };
} ControlRegister;
Snips
источник
3
Это отличный пример! Вот пример того , как можно использовать эту технику во встроенном программном обеспечении: edn.com/design/integrated-circuit-design/4394915/...
rzetterberg
34

Я видел это в нескольких библиотеках как замену объектно-ориентированного наследования.

Например

        Connection
     /       |       \
  Network   USB     VirtualConnection

Если вы хотите, чтобы класс «Connection» был одним из перечисленных выше, вы можете написать что-то вроде:

struct Connection
{
    int type;
    union
    {
        struct Network network;
        struct USB usb;
        struct Virtual virtual;
    }
};

Пример использования в libinfinity: http://git.0x539.de/?p=infinote.git;a=blob;f=libinfinity/common/inf-session.c;h=3e887f0d63bd754c6b5ec232948027cbbf4d61fc;hb=HEAD#l74

бб поколения
источник
33

Объединения позволяют членам данных, которые являются взаимоисключающими, использовать одну и ту же память. Это очень важно, когда памяти меньше, например во встроенных системах.

В следующем примере:

union {
   int a;
   int b;
   int c;
} myUnion;

Этот союз будет занимать пространство одного int, а не 3 отдельных значений int. Если пользователь установит значение a , а затем установит значение b , он перезапишет значение a, поскольку они оба совместно используют одну и ту же область памяти.

LeopardSkinPillBoxHat
источник
29

Много обычаев. Просто сделайте grep union /usr/include/*или в похожих каталогах. В большинстве случаев, unionобернутый в structи один член структуры сообщает, какой элемент в объединении получить доступ. Например оформить заказman elf на реальные реализации.

Это основной принцип:

struct _mydata {
    int which_one;
    union _data {
            int a;
            float b;
            char c;
    } foo;
} bar;

switch (bar.which_one)
{
   case INTEGER  :  /* access bar.foo.a;*/ break;
   case FLOATING :  /* access bar.foo.b;*/ break;
   case CHARACTER:  /* access bar.foo.c;*/ break;
}
phoxis
источник
Именно то, что я искал! Очень полезно заменить некоторый параметр многоточия :)
Николас Ворон
17

Вот пример объединения из моей собственной кодовой базы (из памяти и перефразировано, поэтому оно может быть неточным). Он использовался для хранения языковых элементов в интерпретаторе, который я построил. Например, следующий код:

set a to b times 7.

состоит из следующих языковых элементов:

  • Символ [набор]
  • Переменная [а]
  • Символ [к]
  • Переменная [Ь]
  • символ [раз]
  • постоянная [7]
  • условное обозначение[.]

Элементы языка были определены как ' #define' значения таким образом:

#define ELEM_SYM_SET        0
#define ELEM_SYM_TO         1
#define ELEM_SYM_TIMES      2
#define ELEM_SYM_FULLSTOP   3
#define ELEM_VARIABLE     100
#define ELEM_CONSTANT     101

и следующая структура была использована для хранения каждого элемента:

typedef struct {
    int typ;
    union {
        char *str;
        int   val;
    }
} tElem;

тогда размер каждого элемента был размером максимального объединения (4 байта для типа и 4 байта для объединения, хотя это типичные значения, фактические размеры зависят от реализации).

Чтобы создать элемент set, вы должны использовать:

tElem e;
e.typ = ELEM_SYM_SET;

Для создания элемента variable [b] вы должны использовать:

tElem e;
e.typ = ELEM_VARIABLE;
e.str = strdup ("b");   // make sure you free this later

Чтобы создать элемент «constant [7]», вы должны использовать:

tElem e;
e.typ = ELEM_CONSTANT;
e.val = 7;

и вы можете легко расширить его, чтобы включить floats ( float flt) или rationals ( struct ratnl {int num; int denom;}) и другие типы.

Основная предпосылка заключается в том, что strи valне являются смежными в памяти, они фактически перекрываются, поэтому это способ получить другое представление для одного и того же блока памяти, как показано здесь, где структура основана на ячейке памяти, 0x1010а целые и указатели являются 4 байта:

       +-----------+
0x1010 |           |
0x1011 |    typ    |
0x1012 |           |
0x1013 |           |
       +-----+-----+
0x1014 |     |     |
0x1015 | str | val |
0x1016 |     |     |
0x1017 |     |     |
       +-----+-----+

Если бы это было просто в структуре, это выглядело бы так:

       +-------+
0x1010 |       |
0x1011 |  typ  |
0x1012 |       |
0x1013 |       |
       +-------+
0x1014 |       |
0x1015 |  str  |
0x1016 |       |
0x1017 |       |
       +-------+
0x1018 |       |
0x1019 |  val  |
0x101A |       |
0x101B |       |
       +-------+
paxdiablo
источник
Следует make sure you free this laterли удалить комментарий из константного элемента?
Тревор
Да, @Trevor, хотя я не могу поверить, что вы первый, кто видел это за последние 4+ года :-) Исправлено, и спасибо за это.
paxdiablo
7

Я бы сказал, что это облегчает повторное использование памяти, которая может использоваться по-разному, то есть экономия памяти. Например, вы хотели бы создать некоторую «вариативную» структуру, способную сохранить как короткую строку, так и число:

struct variant {
    int type;
    double number;
    char *string;
};

В 32-битной системе это приведет к тому, что по меньшей мере 96 бит или 12 байтов будут использоваться для каждого экземпляра variant .

Используя объединение, вы можете уменьшить размер до 64 бит или 8 байт:

struct variant {
    int type;
    union {
        double number;
        char *string;
    } value;
};

Вы можете сэкономить еще больше, если хотите добавить больше различных типов переменных и т. Д. Возможно, это правда, что вы можете делать аналогичные вещи с использованием пустого указателя - но объединение делает его намного более доступным, чем и тип. сейф. Такая экономия не кажется огромной, но вы экономите одну треть памяти, используемой для всех экземпляров этой структуры.

Марио
источник
5

Трудно придумать конкретный случай, когда вам понадобится такая гибкая структура, возможно, в протоколе сообщений, где вы будете отправлять сообщения разных размеров, но даже тогда, возможно, есть более лучшие и более дружественные для программиста альтернативы.

Объединения немного похожи на типы вариантов в других языках - они могут содержать только одну вещь за раз, но это может быть int, float и т. Д., В зависимости от того, как вы объявляете это.

Например:

typedef union MyUnion MYUNION;
union MyUnion
{
   int MyInt;
   float MyFloat;
};

MyUnion будет содержать только int или float, в зависимости от того, что вы установили в последний раз . Так делаем это:

MYUNION u;
u.MyInt = 10;

теперь у вас есть целое число, равное 10;

u.MyFloat = 1.0;

Теперь у вас есть число с плавающей запятой, равное 1,0. Он больше не содержит int. Очевидно, теперь, если вы попытаетесь сделать printf ("MyInt =% d", u.MyInt); тогда вы, вероятно, получите ошибку, хотя я не уверен в конкретном поведении.

Размер объединения определяется размером его наибольшего поля, в данном случае поплавка.

Xiaofu
источник
1
sizeof(int) == sizeof(float)( == 32) обычно.
Ник Т
1
Для записи назначение значения с плавающей запятой и последующей печатью int не приведет к ошибке, поскольку ни компилятор, ни среда выполнения не знают, какое значение допустимо. Int, который печатается, будет, конечно, бессмысленным для большинства целей. Это будет просто представление памяти с плавающей точкой, интерпретируемое как int.
Джерри Б.
4

Объединения используются, когда вы хотите смоделировать структуры, определенные аппаратными средствами, устройствами или сетевыми протоколами, или когда вы создаете большое количество объектов и хотите сэкономить место. Они действительно вам не нужны в 95% случаев, придерживайтесь простого в отладке кода.

Ана Беттс
источник
4

Многие из этих ответов касаются преобразования из одного типа в другой. Я получаю наибольшую пользу от союзов с одними и теми же типами, только больше (например, при разборе последовательного потока данных). Они позволяют разбору / построению созданного пакета стать тривиальным.

typedef union
{
    UINT8 buffer[PACKET_SIZE]; // Where the packet size is large enough for
                               // the entire set of fields (including the payload)

    struct
    {
        UINT8 size;
        UINT8 cmd;
        UINT8 payload[PAYLOAD_SIZE];
        UINT8 crc;
    } fields;

}PACKET_T;

// This should be called every time a new byte of data is ready 
// and point to the packet's buffer:
// packet_builder(packet.buffer, new_data);

void packet_builder(UINT8* buffer, UINT8 data)
{
    static UINT8 received_bytes = 0;

    // All range checking etc removed for brevity

    buffer[received_bytes] = data;
    received_bytes++;

    // Using the struc only way adds lots of logic that relates "byte 0" to size
    // "byte 1" to cmd, etc...
}

void packet_handler(PACKET_T* packet)
{
    // Process the fields in a readable manner
    if(packet->fields.size > TOO_BIG)
    {
        // handle error...
    }

    if(packet->fields.cmd == CMD_X)
    {
        // do stuff..
    }
}

Редактировать Комментарий о порядке байтов и структурных отступов является действительным и очень важным. Я почти полностью использовал эту часть кода во встроенном программном обеспечении, большую часть которого контролировал оба конца канала.

Адам Льюис
источник
1
Этот код не будет работать (в большинстве случаев), если обмен данными происходит на 2 разных платформах по следующим причинам: 1) Порядковый номер может отличаться. 2) Перетяжка в конструкциях.
Махори
@Ravi Я согласен с опасениями по поводу порядка байтов и заполнения. Однако следует знать, что я использовал это исключительно во встроенных проектах. Большинство из которых я контролировал оба конца труб.
Адам Льюис
1

Союзы великолепны. Одно умное использование союзов, которые я видел, состоит в том, чтобы использовать их при определении события. Например, вы можете решить, что событие 32-битное.

Теперь, в этих 32 битах, вы можете указать первые 8 бит в качестве идентификатора отправителя события ... Иногда вы имеете дело с событием в целом, иногда вы анализируете его и сравниваете его компоненты. профсоюзы дают вам гибкость, чтобы сделать оба.

Событие союза
{
  unsigned long eventCode;
  unsigned char eventParts [4];
};
dicroce
источник
1

Как насчет того, VARIANTчто используется в интерфейсах COM? Он имеет два поля - «тип» и объединение, содержащее фактическое значение, которое обрабатывается в зависимости от поля «тип».

Sharptooth
источник
1

В школе я использовал такие союзы:

typedef union
{
  unsigned char color[4];
  int       new_color;
}       u_color;

Я использовал его для более удобной обработки цветов, вместо использования операторов >> и <<, мне просто нужно было просмотреть другой индекс моего массива char.

Zoneur
источник
1

Я использовал union, когда писал код для встроенных устройств. У меня есть C int, это 16 бит. И мне нужно извлечь старшие 8 бит и младшие 8 бит, когда мне нужно читать из / store в EEPROM. Поэтому я использовал этот способ:

union data {
    int data;
    struct {
        unsigned char higher;
        unsigned char lower;
    } parts;
};

Это не требует сдвига, поэтому код легче читать.

С другой стороны, я видел какой-то старый код C ++ stl, который использовал union для stl allocator. Если вы заинтересованы, вы можете прочитать SGI STL исходный код. Вот часть этого:

union _Obj {
    union _Obj* _M_free_list_link;
    char _M_client_data[1];    /* The client sees this.        */
};
Му Цяо
источник
1
Разве вам не нужна группировка structвокруг вашего higher/ lower? Прямо сейчас оба должны указывать только на первый байт.
Марио
@ Марио, да, я просто пишу это от руки и забываю об этом, спасибо
Му Цяо
1
  • Файл, содержащий разные типы записей.
  • Сетевой интерфейс, содержащий разные типы запросов.

Взгляните на это: обработка команды буфера X.25

Одна из многих возможных команд X.25 принимается в буфер и обрабатывается на месте с использованием UNION всех возможных структур.

Джеймс Андерсон
источник
Не могли бы вы объяснить оба этих примера. Я имею в виду, как они связаны с объединением
Амит Сингх Томар
1

В ранних версиях C все объявления структуры имели общий набор полей. Дано:

struct x {int x_mode; int q; float x_f};
struct y {int y_mode; int q; int y_l};
struct z {int z_mode; char name[20];};

компилятор, по сути, будет создавать таблицу размеров структур (и, возможно, выравнивания), а также отдельную таблицу имен, типов и смещений элементов структур. Компилятор не уследить из которых члены принадлежали к какой структуре, и позволил бы две структуры , чтобы иметь элемент с таким же именем , только если типа и смещение согласованные (как с членом qв struct xи struct y). Если бы p был указателем на какой-либо тип структуры, p-> q добавило бы смещение «q» к указателю p и извлекло бы «int» из полученного адреса.

Учитывая вышеупомянутую семантику, можно было написать функцию, которая могла бы выполнять некоторые полезные операции над несколькими типами структур взаимозаменяемо, при условии, что все поля, используемые функцией, выровнены с полезными полями в рассматриваемых структурах. Это была полезная функция, и изменение C для проверки элементов, используемых для доступа к структуре, по отношению к типам рассматриваемых структур означало бы потерю его в отсутствие средства, имеющего структуру, которая может содержать несколько именованных полей по одному и тому же адресу. Добавление типов «объединение» в C помогло несколько заполнить этот пробел (хотя, не так, IMHO, как и следовало бы).

Существенной частью способности объединений восполнить этот пробел является тот факт, что указатель на член объединения может быть преобразован в указатель на любое объединение, содержащее этот член, а указатель на любой объединение может быть преобразован в указатель на любой член. В то время как Стандарт C89 прямо не сказал, что приведение a , а затем чтение объекта через эквивалентно написанию объединения через член типа и чтению как типT* непосредственно к a U*было эквивалентно приведению его к указателю на любой тип объединения, содержащему оба, Tи U, а затем приведение к этому U*, никакое определенное поведение последней последовательности преобразования не будет затронуто Используется тип объединения, и в стандарте не указывается какой-либо противоположной семантики для прямого приведения из Tв U. Кроме того, в случаях, когда функция получает указатель неизвестного происхождения, поведение записи объекта посредством T*преобразованияT*в aU*U*TU , что в некоторых случаях будет определено стандартом (например, при доступе к элементам Common Initial Sequence) и определено реализацией (а не неопределено) ) что касается прочего. В то время как программы редко использовали гарантии CIS с действительными объектами типа объединения, гораздо чаще использовался тот факт, что указатели на объекты неизвестного происхождения должны были вести себя как указатели на члены объединения и иметь связанные с этим поведенческие гарантии.

Supercat
источник
Можете ли вы привести пример этого: `было возможно написать функцию, которая могла бы выполнять некоторые полезные операции над несколькими типами структур взаимозаменяемо`. Как можно использовать несколько структур-членов с одинаковыми именами? Если две структуры имеют одинаковое выравнивание данных и, следовательно, элемент с тем же именем и тем же смещением, что и в вашем примере, то из какой структуры я получу фактические данные? (стоимость). Две структуры имеют одинаковое выравнивание и одинаковые элементы, но разные значения на них. Можете ли вы уточнить это
пастух
@Herdsman: в ранних версиях C имя члена структуры инкапсулировало тип и смещение. Два члена разных структур могут иметь одно и то же имя, если и только если их типы и смещения совпадают. Если член структуры fooявляется intсо смещением 8, то anyPointer->foo = 1234;означает «взять адрес в anyPointer, сместите его на 8 байт, а также выполнять целое хранилище значения 1234 для результирующего адреса. Компилятор не должен был бы знать , или ухода ли anyPointerидентифицированы любой тип конструкции, который был fooуказан среди ее членов.
Суперкат
С помощью указателя вы можете разыменовать любой адрес независимо от «источника» указателя, это верно, но тогда какой смысл компилятору хранить таблицы элементов конструкций и их имена (как вы сказали в своем посте), если я могу получить данные с любым указателем, просто зная адрес члена в определенной структуре? И если компилятор не знает, anyPointerидентифицирует ли это элемент struct, то как компилятор проверит эти условия to have a member with the same name only if the type and offset matchedвашего сообщения?
пастух
@Herdsman: Компилятор будет хранить список имен членов структуры, потому что точное поведение p->fooбудет зависеть от типа и смещения foo. По сути, p->fooбыла стенография для *(typeOfFoo*)((unsigned char*)p + offsetOfFoo). Что касается вашего последнего вопроса, когда компилятор встречает определение члена структуры, он требует, чтобы либо не было члена с таким именем, либо чтобы член с таким именем имел такой же тип и смещение; Я бы предположил, что они бы закричали, если бы существовало несоответствующее определение члена структуры, но я не знаю, как оно обрабатывает ошибки.
суперкат
0

Простой и очень полезный пример, это ....

Представить:

у вас есть uint32_t array[2]и вы хотите получить доступ к третьему и четвертому байту цепочки байтов. ты мог бы сделать*((uint16_t*) &array[1]) . Но это, к сожалению, нарушает строгие правила наложения имен!

Но известные компиляторы позволяют вам делать следующее:

union un
{
    uint16_t array16[4];
    uint32_t array32[2];
}

технически это все еще является нарушением правил. но все известные стандарты поддерживают это использование.

dhein
источник