Perché abbiamo bisogno dei sindacati C?


236

Quando dovrebbero essere usati i sindacati? Perché ne abbiamo bisogno?

Risposte:


252

I sindacati vengono spesso utilizzati per convertire tra le rappresentazioni binarie di numeri interi e float:

union
{
  int i;
  float f;
} u;

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

Anche se questo è un comportamento tecnicamente indefinito secondo lo standard C (dovresti solo leggere il campo che è stato scritto più di recente), agirà in un modo ben definito praticamente in qualsiasi compilatore.

I sindacati vengono talvolta utilizzati anche per implementare lo pseudo-polimorfismo in C, dando una struttura un tag che indica il tipo di oggetto che contiene e quindi unendo insieme i possibili tipi:

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;
  }
}

Ciò consente alle dimensioni di struct Sessere solo 12 byte, anziché 28.


ci dovrebbe essere invece di te
Amit Singh Tomar

1
L'esempio che suppone di convertire float in numero intero funziona? Non credo, poiché int e float sono memorizzati in diversi formati in memoria. Puoi spiegare il tuo esempio?
spin_eight,

3
@spin_eight: non si sta "convertendo" da float a int. Più come "reinterpretare la rappresentazione binaria di un float come se fosse un int". L'output non è 3: ideone.com/MKjwon Non sono sicuro del perché Adam stia stampando come esadecimale.
Endolith,

@Adam Rosenfield non ho capito bene la conversione non ho un numero intero nell'output: p
The Beast

2
Ritengo che il disclaimer sull'essere un comportamento indefinito debba essere rimosso. È, infatti, un comportamento definito. Vedere la nota 82 della norma C99: se il membro utilizzato per accedere al contenuto di un oggetto unione non è lo stesso dell'ultimo membro utilizzato per memorizzare un valore nell'oggetto, la parte appropriata della rappresentazione dell'oggetto del valore viene reinterpretata come una rappresentazione di oggetto nel nuovo tipo come descritto in 6.2.6 (un processo talvolta chiamato "punzonatura di tipo"). Questa potrebbe essere una rappresentazione trappola.
Christian Gibbons,

136

I sindacati sono particolarmente utili nella programmazione integrata o in situazioni in cui è necessario l'accesso diretto all'hardware / alla memoria. Ecco un esempio banale:

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

Quindi è possibile accedere al registro come segue:

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

L'endianness (ordine dei byte) e l'architettura del processore sono ovviamente importanti.

Un'altra utile funzione è il modificatore di bit:

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;

Con questo codice è possibile accedere direttamente a un singolo bit nel registro / indirizzo di memoria:

x = reg.bits.b2;

3
La tua risposta qui in congiunzione con la risposta di @Adam Rosenfield sopra costituisce la perfetta coppia complementare: dimostrate di usare una struttura all'interno di un sindacato , e lui dimostra di usare una unione all'interno di una struttura . Si scopre che ho bisogno di entrambi allo stesso tempo: una struttura all'interno di un'unione all'interno di una struttura per implementare un polimorfismo di passaggio dei messaggi in C tra thread su un sistema incorporato, e non avrei realizzato che non avessi visto entrambe le tue risposte insieme .
Gabriel Staples,

1
Avevo torto: è un'unione all'interno di una struttura all'interno di un'unione all'interno di una struttura, annidata a sinistra per scrivere come ho scritto io, dal nido più interno al livello più esterno. Ho dovuto aggiungere un'altra unione al livello più interno per consentire valori di diversi tipi di dati.
Gabriel Staples,

64

La programmazione del sistema a basso livello è un esempio ragionevole.

IIRC, ho usato i sindacati per scomporre i registri hardware nei bit dei componenti. Quindi, puoi accedere a un registro a 8 bit (com'era, nel giorno in cui l'ho fatto ;-) nei bit del componente.

(Dimentico la sintassi esatta ma ...) Questa struttura consentirebbe l'accesso a un registro di controllo come control_byte o tramite i singoli bit. Sarebbe importante assicurarsi che i bit siano mappati sui bit di registro corretti per una data endianness.

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
Questo è un ottimo esempio! Ecco un esempio di come è possibile utilizzare questa tecnica nel software incorporato: edn.com/design/integrated-circuit-design/4394915/…
rzetterberg

34

L'ho visto in un paio di librerie in sostituzione dell'eredità orientata agli oggetti.

Per esempio

        Connection
     /       |       \
  Network   USB     VirtualConnection

Se vuoi che la "classe" di connessione sia una delle precedenti, puoi scrivere qualcosa del tipo:

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

Esempio di utilizzo in libinfinity: http://git.0x539.de/?p=infinote.git;a=blob;f=libinfinity/common/inf-session.c;h=3e887f0d63bd754c6b5ec232948027cbbf4d61fc;hb=HEAD#l74


33

I sindacati consentono ai membri di dati che si escludono a vicenda di condividere la stessa memoria. Questo è abbastanza importante quando la memoria è più scarsa, come nei sistemi embedded.

Nel seguente esempio:

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

Questa unione occuperà lo spazio di un singolo int, piuttosto che 3 valori int separati. Se l'utente imposta il valore di a , quindi imposta il valore di b , sovrascriverà il valore di a poiché entrambi condividono la stessa posizione di memoria.


29

Molti usi. Basta fare grep union /usr/include/*o in directory simili. La maggior parte dei casi unionè racchiusa in un structe un membro della struttura indica a quale elemento nell'unione accedere. Ad esempio, checkout man elfper implementazioni nella vita reale.

Questo è il principio base:

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;
}

Esattamente quello che stavo cercando! Molto utile per sostituire alcuni parametri dell'ellissi :)
Nicolas Voron,

17

Ecco un esempio di unione dalla mia base di codice (dalla memoria e parafrasata, quindi potrebbe non essere esatta). È stato usato per memorizzare elementi del linguaggio in un interprete che ho costruito. Ad esempio, il seguente codice:

set a to b times 7.

è costituito dai seguenti elementi del linguaggio:

  • simbolo [set]
  • variabile [a]
  • simbolo [a]
  • variabile [b]
  • Simbolo [volte]
  • costante [7]
  • simbolo[.]

Gli elementi del linguaggio sono stati definiti come ' #define' valori quindi:

#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

e la seguente struttura è stata utilizzata per memorizzare ogni elemento:

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

quindi la dimensione di ogni elemento era la dimensione dell'unione massima (4 byte per il tipo e 4 byte per l'unione, sebbene questi siano valori tipici, le dimensioni effettive dipendono dall'implementazione).

Per creare un elemento "set", dovresti usare:

tElem e;
e.typ = ELEM_SYM_SET;

Per creare un elemento "variabile [b]", devi usare:

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

Per creare un elemento "costante [7]", dovresti usare:

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

e puoi facilmente espanderlo per includere floats ( float flt) o rationals ( struct ratnl {int num; int denom;}) e altri tipi.

La premessa di base è che i stre valnon sono contigui nella memoria, in realtà si sovrappongono, quindi è un modo per ottenere una vista diversa sullo stesso blocco di memoria, illustrato qui, in cui la struttura si basa sulla posizione della memoria 0x1010e numeri interi e puntatori sono entrambi 4 byte:

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

Se fosse solo in una struttura, sarebbe simile a questo:

       +-------+
0x1010 |       |
0x1011 |  typ  |
0x1012 |       |
0x1013 |       |
       +-------+
0x1014 |       |
0x1015 |  str  |
0x1016 |       |
0x1017 |       |
       +-------+
0x1018 |       |
0x1019 |  val  |
0x101A |       |
0x101B |       |
       +-------+

Il make sure you free this latercommento deve essere rimosso dall'elemento costante?
Trevor,

Sì, @Trevor, anche se non riesco a credere che tu sia la prima persona a vederlo negli ultimi 4+ anni :-) Risolto, e grazie per quello.
paxdiablo,

7

Direi che è più semplice riutilizzare la memoria che potrebbe essere utilizzata in diversi modi, ad esempio risparmiando memoria. Ad esempio, ti piacerebbe fare qualche struttura "variante" in grado di salvare una stringa corta e un numero:

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

In un sistema a 32 bit ciò comporterebbe l'utilizzo di almeno 96 bit o 12 byte per ciascuna istanza di variant.

Utilizzando un'unione è possibile ridurre le dimensioni fino a 64 bit o 8 byte:

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

Puoi risparmiare ancora di più se desideri aggiungere più tipi di variabili diverse, ecc. Potrebbe essere vero che puoi fare cose simili lanciando un puntatore vuoto, ma l'unione lo rende molto più accessibile e digita sicuro. Tali risparmi non sembrano enormi, ma stai risparmiando un terzo della memoria utilizzata per tutte le istanze di questa struttura.


5

È difficile pensare a un'occasione specifica in cui avresti bisogno di questo tipo di struttura flessibile, forse in un protocollo di messaggio in cui invierai diverse dimensioni di messaggi, ma anche allora ci sono probabilmente alternative migliori e più programmabili.

I sindacati sono un po 'come i tipi di varianti in altre lingue: possono contenere solo una cosa alla volta, ma quella cosa potrebbe essere un int, un float ecc. A seconda di come la dichiarate.

Per esempio:

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

MyUnion conterrà solo un int o un float, a seconda dell'ultima impostazione impostata . Quindi facendo questo:

MYUNION u;
u.MyInt = 10;

ora hai un int uguale a 10;

u.MyFloat = 1.0;

ora hai un float pari a 1.0. Non contiene più un int. Ovviamente ora se provi a fare printf ("MyInt =% d", u.MyInt); allora probabilmente otterrai un errore, anche se non sono sicuro del comportamento specifico.

La dimensione dell'unione è dettata dalla dimensione del suo campo più grande, in questo caso il galleggiante.


1
sizeof(int) == sizeof(float)( == 32) di solito.
Nick T,

1
Per la cronaca, l'assegnazione al float e quindi la stampa di int non causerà un errore, poiché né il compilatore né l'ambiente di runtime sanno quale valore è valido. L'int che viene stampato sarà, ovviamente, insignificante per la maggior parte degli scopi. Sarà solo la rappresentazione in memoria del float, interpretata come int.
Jerry B,

4

I sindacati vengono utilizzati quando si desidera modellare strutture definite da hardware, dispositivi o protocolli di rete o quando si crea un numero elevato di oggetti e si desidera risparmiare spazio. In realtà non ne hai bisogno il 95% delle volte, mantieni il codice di facile debug.


4

Molte di queste risposte riguardano il casting da un tipo all'altro. Ottengo il massimo utilizzo dai sindacati con gli stessi tipi, solo di più (ad esempio quando analizzo un flusso di dati seriale). Consentono che l'analisi / costruzione di un pacchetto incorniciato diventi banale.

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..
    }
}

Modifica Il commento sull'endianness e il padding della struttura sono validi e grandi preoccupazioni. Ho usato questo codice quasi interamente nei software embedded, la maggior parte dei quali avevo il controllo di entrambe le estremità della pipe.


1
Questo codice non funzionerà (la maggior parte delle volte) se i dati vengono scambiati su 2 piattaforme diverse per i seguenti motivi: 1) L'endianness potrebbe essere diversa. 2) Imbottitura in strutture.
Mahori

@Ravi Concordo con le preoccupazioni su endianness e padding. Tuttavia, si dovrebbe sapere che l'ho usato esclusivamente in progetti integrati. La maggior parte dei quali ho controllato entrambe le estremità dei tubi.
Adam Lewis,

1

I sindacati sono fantastici. Un uso intelligente dei sindacati che ho visto è quello di usarli quando si definisce un evento. Ad esempio, potresti decidere che un evento è a 32 bit.

Ora, all'interno di quei 32 bit, potresti voler designare i primi 8 bit come per un identificatore del mittente dell'evento ... A volte hai a che fare con l'evento nel suo insieme, a volte lo sezioni e confronti i suoi componenti. i sindacati ti danno la flessibilità di fare entrambe le cose.

evento sindacale
{
  eventCode lungo senza segno;
  unsigned char eventParts [4];
};

1

Che dire di VARIANTciò è usato nelle interfacce COM? Ha due campi: "tipo" e un'unione contenente un valore effettivo che viene trattato in base al campo "tipo".


1

A scuola, ho usato i sindacati in questo modo:

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

L'ho usato per gestire i colori più facilmente, invece di usare gli operatori >> e <<, dovevo solo passare attraverso il diverso indice del mio array di caratteri.


1

Ho usato l'unione mentre stavo codificando per dispositivi embedded. Ho C int che è lungo 16 bit. E ho bisogno di recuperare gli 8 bit più alti e gli 8 bit più bassi quando devo leggere / memorizzare su EEPROM. Quindi ho usato in questo modo:

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

Non richiede spostamento, quindi il codice è più facile da leggere.

D'altra parte, ho visto un vecchio codice stl C ++ che utilizzava unione per allocatore stl. Se sei interessato, puoi leggere il codice sorgente sgi stl . Eccone un pezzo:

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

1
Non avresti bisogno di un raggruppamento structattorno al tuo higher/ lower? In questo momento entrambi dovrebbero puntare solo al primo byte.
Mario,

@Mario ah giusto, l'ho appena scritto a mano e non pensarci più, grazie
Mu Qiao

1
  • Un file contenente diversi tipi di record.
  • Un'interfaccia di rete contenente diversi tipi di richieste.

Dai un'occhiata a questo: Gestione dei comandi del buffer X.25

Uno dei tanti possibili comandi X.25 viene ricevuto in un buffer e gestito sul posto utilizzando un'UNIONE di tutte le possibili strutture.


potresti per favore spiegare entrambi questi esempi. Voglio dire come sono collegati all'unione
Amit Singh Tomar,

1

Nelle prime versioni di C, tutte le dichiarazioni di struttura condividevano un insieme comune di campi. Dato:

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];};

un compilatore produrrebbe essenzialmente una tabella delle dimensioni delle strutture (e possibilmente allineamenti) e una tabella separata dei nomi, dei tipi e degli offset dei membri delle strutture. Il compilatore non ha tenuto traccia di quali membri appartenessero a quali strutture e consentirebbe a due strutture di avere un membro con lo stesso nome solo se il tipo e l'offset corrispondessero (come con il membro qdi struct xestruct y ). Se p fosse un puntatore a qualsiasi tipo di struttura, p-> q aggiungerebbe l'offset di "q" al puntatore p e recupererebbe un "int" dall'indirizzo risultante.

Considerata la semantica di cui sopra, è stato possibile scrivere una funzione in grado di eseguire intercambiabili alcune utili operazioni su più tipi di struttura, a condizione che tutti i campi utilizzati dalla funzione siano allineati con campi utili all'interno delle strutture in questione. Questa era una caratteristica utile e cambiare C per convalidare i membri utilizzati per l'accesso alla struttura rispetto ai tipi delle strutture in questione avrebbe significato perderlo in assenza di un mezzo per avere una struttura che può contenere più campi con nome allo stesso indirizzo. L'aggiunta di tipi di "unione" a C ha contribuito a colmare un po 'il divario (anche se non, IMHO, così come avrebbe dovuto essere).

Una parte essenziale della capacità dei sindacati di colmare questa lacuna era il fatto che un puntatore a un membro sindacale poteva essere convertito in un puntatore a qualsiasi unione contenente quel membro e un puntatore a qualsiasi unione poteva essere convertito in un puntatore a qualsiasi membro. Mentre lo standard C89 non ha espressamente affermato che lanciare un T*direttamente su un U*equivale a lanciarlo su un puntatore a qualsiasi tipo di unione contenente entrambi Te U, quindi, lanciare quello su U*, nessun comportamento definito di quest'ultima sequenza di cast sarebbe influenzato dal tipo di unione utilizzato e lo Standard non specificava alcuna semantica contraria per un cast diretto da Ta U. Inoltre, nei casi in cui una funzione ha ricevuto un puntatore di origine sconosciuta, il comportamento della scrittura di un oggetto tramite la T*conversione diT*a a U*, e quindi leggere l'oggetto tramite U*equivarrebbe a scrivere un'unione tramite membro di tipo Te leggere come tipo U, che sarebbe definito in alcuni casi standard (ad esempio quando si accede a membri di sequenza iniziale comune) e definito dall'implementazione (piuttosto di Undefined) per il resto. Mentre era raro che i programmi sfruttassero le garanzie CIS con oggetti reali di tipo sindacale, era molto più comune sfruttare il fatto che i puntatori a oggetti di origine sconosciuta dovevano comportarsi come puntatori ai membri del sindacato e avere le garanzie comportamentali ad essi associate.


puoi fare un esempio di questo: `era possibile scrivere una funzione che potesse eseguire in modo intercambiabile alcune utili operazioni su più tipi di struttura`? Come si possono usare più strutture membro con lo stesso nome? Se due strutture hanno lo stesso allineamento dei dati e quindi un membro con lo stesso nome e lo stesso offset del tuo esempio, da quale struttura ricaverei i dati effettivi? (valore). Due strutture hanno lo stesso allineamento e gli stessi membri, ma valori diversi su di essi. Potete per favore elaborarlo
Herdsman

@Herdsman: nelle prime versioni di C, un nome di membro struct incapsulava un tipo e un offset. Due membri di strutture diverse potrebbero avere lo stesso nome se e solo se i loro tipi e offset corrispondessero. Se membro struttfoo è un intoffset 8, anyPointer->foo = 1234;significa "prendi l'indirizzo in qualunque puntatore, spostalo di 8 byte ed esegui un archivio intero del valore 1234 all'indirizzo risultante. Il compilatore non avrebbe bisogno di sapere o preoccuparsi se anyPointeridentificato qualsiasi tipo di struttura che fosse fooelencata tra i suoi membri
supercat,

Con il puntatore puoi riconoscere qualsiasi indirizzo indipendentemente dall'origine del puntatore, questo è vero, ma poi qual è il punto del compilatore che contiene le tabelle dei membri delle strutture e i loro nomi (come hai detto nel tuo post) se posso recuperare i dati con qualche puntatore semplicemente conoscendo l'indirizzo di un membro in una particolare struttura? E se il compilatore non sa se ilanyPointer identifica con un membro struct, come farà il compilatore a controllare queste condizioni to have a member with the same name only if the type and offset matcheddel tuo post?
Herdsman,

@Herdsman: il compilatore manterrà l'elenco dei nomi dei membri della struttura perché il comportamento preciso p->foodipenderà dal tipo e dall'offset di foo. Essenzialmente, p->fooera una scorciatoia per *(typeOfFoo*)((unsigned char*)p + offsetOfFoo). Per quanto riguarda la tua ultima domanda, quando un compilatore incontra una definizione di membro struct, richiede che non esista alcun membro con quel nome o che il membro con quel nome abbia lo stesso tipo e offset; Immagino che si sarebbe sballato se esistesse una definizione di membro strutt non corrispondente, ma non so come gestisse gli errori.
supercat

0

Un esempio semplice e molto utile è ....

Immaginare:

hai un uint32_t array[2]e vuoi accedere al 3o e 4o byte della catena di byte. potresti fare *((uint16_t*) &array[1]). Ma questo purtroppo infrange le rigide regole di aliasing!

Ma i compilatori noti ti consentono di fare quanto segue:

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

tecnicamente questa è ancora una violazione delle regole. ma tutti gli standard noti supportano questo utilizzo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.