Для чого нам потрібні спілки C?


236

Коли слід застосовувати спілки? Навіщо вони нам потрібні?

Відповіді:


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 (ви повинні читати лише те поле, яке було нещодавно написано), воно буде діяти чітко визначеним чином практично у будь-якому компіляторі.

Союзи також іноді використовуються для реалізації псевдополіморфізму в С, надаючи структурі деякий тег із зазначенням того, який тип об'єкта він містить, а потім об'єднання можливих типів разом:

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: Це не "перетворення" з float в int. Більше схоже на "переінтерпретацію двійкового подання поплавця так, ніби це було int". Вихід не 3: ideone.com/MKjwon Я не впевнений, чому Адам друкує як шістнадцятковий.
ендоліт

@ Адам Розенфілд, я насправді не зазнав перетворення, я не отримую цілого числа у виході: p
The Beast

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;

Звичайно важливі ендіанси (порядок байтів) та архітектура процесора.

Ще одна корисна особливість - бітовий модифікатор:

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;

3
Ваша відповідь тут у поєднанні з відповіддю @Adam Розенфілд вище робить ідеальну додаткову пару: ви демонструєте використання структури в союзі , а він демонструє використання об'єднання в структурі . Виявляється, мені потрібно відразу і те, і інше: структура в союзі всередині структури, щоб реалізувати певний поліморфізм передачі повідомлень у С між потоками вбудованої системи, і я б не зрозумів, що якби я не бачив обох ваших відповідей разом .
Габріель Степлес

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;

3
Це відмінний приклад! Ось приклад того, як ви могли використовувати цю техніку у вбудованому програмному забезпеченні: edn.com/design/integrated-circuit-design/4394915/…
rzetterberg

34

Я бачив це в декількох бібліотеках як заміну об'єктно-орієнтованого успадкування.

Напр

        Connection
     /       |       \
  Network   USB     VirtualConnection

Якщо ви хочете, щоб клас "З'єднання" був одним із перерахованих вище, ви можете написати щось на зразок:

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, оскільки вони обидва ділять однакове місце пам'яті.


29

Багато звичок. Просто робіть grep union /usr/include/*або в подібних довідниках. У більшості випадків unionвкладений в a, 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;
}

Саме те, що я шукав! Дуже корисно замінити якийсь параметр еліпсиса :)
Ніколас Ворон

17

Ось приклад об'єднання з моєї власної кодової бази (з пам'яті та перефразоване, тому це може бути не точно). Він використовувався для зберігання мовних елементів у вбудованому мені інтерпретаторі. Наприклад, наступний код:

set a to b times 7.

складається з таких мовних елементів:

  • символ [набір]
  • змінна [a]
  • символ [до]
  • змінна [b]
  • символ [рази]
  • постійна [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;

Для створення елемента "змінний [b]" ви використовуєте:

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

Для створення елемента "константа [7]" ви використовуєте:

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

і ви можете легко розширити його, включаючи floats ( float flt) або раціоналі ( 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 |       |
       +-------+

Чи 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;

u тепер має інт, рівний 10;

u.MyFloat = 1.0;

u тепер містить поплавок, рівний 1,0. Він більше не містить int. Очевидно тепер, якщо ви спробуєте виконати printf ("MyInt =% d", u.MyInt); то, ймовірно, ви отримаєте помилку, хоча я не впевнений у конкретній поведінці.

Розмір об'єднання продиктований розміром його найбільшого поля, в даному випадку поплавця.


1
sizeof(int) == sizeof(float)( == 32) зазвичай.
Нік Т

1
Для запису присвоєння флоату та друку 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
Цей код не працює (в більшості випадків), якщо дані обмінюються на двох різних платформах через наступні причини: 1) Ендіанси можуть бути різними. 2) Прокладка в конструкціях.
Махорі

@Ravi Я погоджуюсь із занепокоєнням щодо витривалості та забиття. Однак слід знати, що я використовував це виключно у вбудованих проектах. Більшу частину з яких я контролював обидва кінці труб.
Адам Льюїс

1

Спілки чудові. Одне розумне використання спілок, які я бачив, - це використовувати їх під час визначення події. Наприклад, ви можете вирішити, що подія має 32 біти.

Тепер, у межах 32-х бітів, ви можете позначити перші 8 біт як ідентифікатор відправника події ... Іноді ви маєте справу з подією в цілому, іноді ви розчленовуєте її та порівнюєте її компоненти. профспілки дають вам можливість робити і те, і інше.

союзний захід
{
  неподписаний довгий eventCode;
  Непідписані події CharParts [4];
};

1

Що про VARIANTте, що використовується в COM-інтерфейсах? Він має два поля - "type" та об'єднання, що містить фактичне значення, яке обробляється залежно від поля "type".


1

У школі я використовував такі союзи:

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

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


1

Я використовував об'єднання, коли кодував вбудовані пристрої. У мене є C int, який довгий 16 біт. І мені потрібно отримати вищі 8 біт і нижчі 8 біт, коли мені потрібно читати з / зберігати в EEPROM. Тому я використав такий спосіб:

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

Це не вимагає зміщення, тому код легше читати.

З іншого боку, я побачив старий код C ++ stl, який використовував союз для виділення stl. Якщо вас цікавить, ви можете прочитати вихідний код sgi stl . Ось його фрагмент:

union _Obj {
    union _Obj* _M_free_list_link;
    char _M_client_data[1];    /* The client sees this.        */
};

1
Вам не потрібно групування structнавколо вашого higher/ lower? Зараз обидва повинні вказувати лише на перший байт.
Маріо

@Mario ах, я просто пишу це від руки і забуду про це, дякую
Mu Qiao

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 допомогло дещо заповнити цю прогалину (хоча і ні, ІМХО, як і повинно було бути).

Важливою частиною здатності профспілок заповнити цей проміжок був той факт, що вказівник на члена профспілки може бути перетворений в покажчик на будь-який союз, що містить цього члена, а вказівник на будь-який союз може бути перетворений в покажчик на будь-який член. У той час як стандарт C89 прямо не сказав, що передача T*прямого на a U*еквівалентна переливанню його на вказівник на будь-який тип об'єднання, що містить Tі U, і потім підкидає це U*, жодна визначена поведінка останньої послідовності передачі не впливатиме на тип з'єднання, що використовується, а Стандарт не вказав жодної протилежної семантики для прямої Tпередачі від U. Далі, у випадках, коли функція отримала вказівник невідомого походження, поведінка запису об'єкта через аT* , перетворення наT*U*, а потім читання об'єкта через U*було б еквівалентно написанню об'єднання через член типу Tта читання як тип U, що було б стандартно визначено в кількох випадках (наприклад, при доступі до загальних членів початкової послідовності) та визначено реалізацією (а не невизначено) ) для решти. Хоча програми по використанню гарантій СНД фактичними об'єктами союзного типу були рідкісними, але значно частіше було використовувати той факт, що вказівники на об'єкти невідомого походження повинні поводитись як вказівники на членів профспілки та пов'язані з цим поведінкові гарантії.


Ви можете навести приклад цього: "можна було написати функцію, яка могла б виконувати деякі корисні операції на різних видах структури взаємозамінно". Як можна використовувати кілька членів структури з однаковою назвою? Якщо дві структури мають однакове вирівнювання даних і, таким чином, член з тим же ім'ям і таким же зрушенням, як у прикладі, як ви, наприклад, з якої структури я отримав би фактичні дані? (значення). Дві структури мають однакове вирівнювання і однакові члени, але різні значення на них. Чи можете ви, будь ласка, докладно
розібратися

@Herdsman: У ранніх версіях C ім'я члена структури містило тип та зсув. Два члени різних структур можуть мати однакову назву, якщо і лише тоді, коли їх типи та зсуви збігаються. Якщо членом структури fooє intзсув 8, то anyPointer->foo = 1234;мається на увазі "взяти адресу в будь-якомуPointer, перемістити її на 8 байт і виконати цілочисельне зберігання значення 1234 за отриманою адресою. Компілятору не потрібно знати чи дбати про те, чи anyPointerідентифікований будь-який тип структури, який був fooвказаний серед його членів.
supercat

За допомогою вказівника ви можете скасувати будь-яку адресу незалежно від "походження" вказівника, це правда, але тоді, який сенс компілятора, щоб містити таблиці членів структур та їх імена (як ви сказали у своєму дописі), якщо я можу отримати дані будь-який вказівник просто знає адресу члена в певній структурі? І якщо компілятор не знає, чи anyPointerвказується особа з членом структури, то як компілятор перевірятиме ці умови 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). Що стосується останнього запитання, коли компілятор стикається з визначенням члена структури, він вимагає, щоб або не було члена з цим іменем, або щоб член з цим іменем мав однаковий тип і зміщений; Я б здогадався, що це було б зірвано, якби існувало невідповідне визначення визначення структури, але я не знаю, як воно обробляло помилки.
supercat

0

Простий і дуже корисний приклад - це….

Уявіть собі:

Ви маєте uint32_t array[2]і хочете отримати доступ до 3-го та 4-го байтів ланцюжка "Байт". ви могли б зробити *((uint16_t*) &array[1]). Але це сумно порушує суворі правила псевдоніму!

Але відомі компілятори дозволяють зробити наступне:

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

технічно це все-таки порушення правил. але всі відомі стандарти підтримують це використання.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.