¿Por qué necesitamos C Unions?


236

¿Cuándo deben usarse los sindicatos? ¿Por qué los necesitamos?

Respuestas:


252

Las uniones a menudo se usan para convertir entre las representaciones binarias de enteros y flotantes:

union
{
  int i;
  float f;
} u;

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

Aunque este es un comportamiento técnicamente indefinido de acuerdo con el estándar C (se supone que solo debe leer el campo que se escribió más recientemente), actuará de manera bien definida en prácticamente cualquier compilador.

Las uniones también se usan a veces para implementar pseudopolimorfismos en C, al dar a la estructura una etiqueta que indica qué tipo de objeto contiene y luego unir los tipos posibles:

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

Esto permite que el tamaño struct Ssea ​​de solo 12 bytes, en lugar de 28.


debería haber uy en lugar de uf
Amit Singh Tomar

1
¿Funciona el ejemplo que supone convertir flotante a entero? No lo creo, ya que int y float se almacenan en diferentes formatos en la memoria. ¿Puedes explicar tu ejemplo?
spin_eight

3
@spin_eight: No es "convertir" de flotante a int. Más como "reinterpretar la representación binaria de un flotante como si fuera un int". Sin embargo, el resultado no es 3: ideone.com/MKjwon No estoy seguro de por qué Adam está imprimiendo como hexadecimal.
endolito

@ Adam Rosenfield realmente no entendí la conversión, no obtengo un número entero en la salida: p
The Beast

2
Siento que el descargo de responsabilidad sobre el comportamiento indefinido debe eliminarse. Es, de hecho, un comportamiento definido. Consulte la nota al pie 82 del estándar C99: si el miembro utilizado para acceder al contenido de un objeto de unión no es el mismo que el miembro utilizado por última vez para almacenar un valor en el objeto, la parte apropiada de la representación del valor del objeto se reinterpreta como una representación de objeto en el nuevo tipo como se describe en 6.2.6 (un proceso a veces llamado "punteo de tipo"). Esto podría ser una representación trampa.
Christian Gibbons

136

Los sindicatos son particularmente útiles en la programación integrada o en situaciones en las que se necesita acceso directo al hardware / memoria. Aquí hay un ejemplo trivial:

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

Luego puede acceder al registro de la siguiente manera:

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

Endianness (orden de bytes) y la arquitectura del procesador son, por supuesto, importantes.

Otra característica útil es el modificador de bits:

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 este código puede acceder directamente a un solo bit en la dirección de registro / memoria:

x = reg.bits.b2;

3
Su respuesta aquí junto con la respuesta anterior de @Adam Rosenfield hacen el par complementario perfecto: usted demuestra el uso de una estructura dentro de una unión , y él demuestra el uso de una unión dentro de una estructura . Resulta que necesito ambas cosas a la vez: una estructura dentro de una unión dentro de una estructura para implementar un polimorfismo de transmisión de mensajes elegante en C entre hilos en un sistema incrustado, y no me habría dado cuenta de eso si no hubiera visto ambas respuestas juntas .
Gabriel Staples

1
Me equivoqué: es una unión dentro de una estructura dentro de una unión dentro de una estructura, anidada a la izquierda para escribir como lo escribí, desde el nivel de anidación más interno al más externo. Tuve que agregar otra unión en el nivel más interno para permitir valores de diferentes tipos de datos.
Gabriel Staples

64

La programación del sistema de bajo nivel es un ejemplo razonable.

IIRC, he usado sindicatos para desglosar registros de hardware en los bits componentes. Entonces, puede acceder a un registro de 8 bits (como estaba, el día que hice esto ;-) en los bits de componentes.

(Olvidé la sintaxis exacta pero ...) Esta estructura permitiría acceder a un registro de control como control_byte o mediante los bits individuales. Sería importante asegurarse de que los bits se correspondan con los bits de registro correctos para una endianidad dada.

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
Este es un excelente ejemplo! Aquí hay un ejemplo de cómo podría usar esta técnica en software embebido: edn.com/design/integrated-circuit-design/4394915/…
rzetterberg

34

Lo he visto en un par de bibliotecas como un reemplazo para la herencia orientada a objetos.

P.ej

        Connection
     /       |       \
  Network   USB     VirtualConnection

Si desea que la "clase" de Connection sea una de las anteriores, puede escribir algo como:

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

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


33

Los sindicatos permiten que los miembros de datos que se excluyen mutuamente compartan la misma memoria. Esto es bastante importante cuando la memoria es más escasa, como en los sistemas integrados.

En el siguiente ejemplo:

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

Esta unión ocupará el espacio de un único int, en lugar de 3 valores int separados. Si el usuario establece el valor de a , y luego establece el valor de b , sobrescribirá el valor de a ya que ambos comparten la misma ubicación de memoria.


29

Muchos usos. Solo hazlo grep union /usr/include/*o en directorios similares. La mayoría de los casos unionse envuelve en un structmiembro de la estructura y uno le dice a qué elemento de la unión acceder. Por ejemplo, pago man elfpara implementaciones de la vida real.

Este es el principio básico:

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

Exactamente lo que estaba buscando ! Muy útil para reemplazar algunos parámetros de puntos suspensivos :)
Nicolas Voron

17

Aquí hay un ejemplo de una unión de mi propia base de código (de memoria y parafraseada, por lo que puede no ser exacta). Fue utilizado para almacenar elementos del lenguaje en un intérprete que construí. Por ejemplo, el siguiente código:

set a to b times 7.

consta de los siguientes elementos del lenguaje:

  • símbolo [conjunto]
  • variable [a]
  • símbolo [a]
  • variable [b]
  • símbolo [veces]
  • constante [7]
  • símbolo[.]

Los elementos del lenguaje se definieron como #definevalores ' ' por lo tanto:

#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

y se utilizó la siguiente estructura para almacenar cada elemento:

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

entonces el tamaño de cada elemento era el tamaño de la unión máxima (4 bytes para el tipo y 4 bytes para la unión, aunque esos son valores típicos, el valor real tamaños dependen de la implementación).

Para crear un elemento "conjunto", usaría:

tElem e;
e.typ = ELEM_SYM_SET;

Para crear un elemento "variable [b]", usaría:

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

Para crear un elemento "constante [7]", usaría:

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

y podría expandirlo fácilmente para incluir flotantes ( float flt) o racionales ( struct ratnl {int num; int denom;}) y otros tipos.

La premisa básica es que stry valno son contiguos en la memoria, en realidad se superponen, por lo que es una forma de obtener una vista diferente en el mismo bloque de memoria, ilustrado aquí, donde la estructura se basa en la ubicación de la memoria 0x1010y los enteros y punteros son ambos 4 bytes:

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

Si solo estuviera en una estructura, se vería así:

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

¿Debería make sure you free this latereliminarse el comentario del elemento constante?
Trevor

Sí, @Trevor, aunque no puedo creer que seas la primera persona que lo vio en los últimos 4 años :-) Solucionado, y gracias por eso.
paxdiablo

7

Yo diría que hace que sea más fácil reutilizar la memoria que podría usarse de diferentes maneras, es decir, ahorrar memoria. Por ejemplo, le gustaría hacer una estructura "variante" que pueda guardar una cadena corta y un número:

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

En un sistema de 32 bits, esto daría como resultado al menos 96 bits o 12 bytes para cada instancia de variant.

Con una unión, puede reducir el tamaño a 64 bits u 8 bytes:

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

Puede ahorrar aún más si desea agregar más tipos de variables diferentes, etc. Podría ser cierto, que puede hacer cosas similares lanzando un puntero vacío, pero la unión lo hace mucho más accesible, así como escribir seguro. Tales ahorros no suenan masivos, pero está ahorrando un tercio de la memoria utilizada para todas las instancias de esta estructura.


5

Es difícil pensar en una ocasión específica en la que necesite este tipo de estructura flexible, tal vez en un protocolo de mensaje en el que enviaría mensajes de diferentes tamaños, pero incluso entonces probablemente haya alternativas mejores y más amigables para el programador.

Los sindicatos son un poco como los tipos de variantes en otros idiomas: solo pueden contener una cosa a la vez, pero esa cosa podría ser un int, un flotante, etc. dependiendo de cómo lo declare.

Por ejemplo:

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

MyUnion solo contendrá un int O un flotante, dependiendo de lo que haya configurado más recientemente . Entonces haciendo esto:

MYUNION u;
u.MyInt = 10;

u ahora tiene un int igual a 10;

u.MyFloat = 1.0;

Ahora tiene un flotador igual a 1.0. Ya no tiene un int. Obviamente ahora si intenta hacer printf ("MyInt =% d", u.MyInt); entonces probablemente obtendrá un error, aunque no estoy seguro del comportamiento específico.

El tamaño de la unión está dictado por el tamaño de su campo más grande, en este caso el flotador.


1
sizeof(int) == sizeof(float)( == 32) por lo general.
Nick T

1
Para el registro, asignar al flotante y luego imprimir el int no causará un error, ya que ni el compilador ni el entorno de tiempo de ejecución saben qué valor es válido. El int que se imprime, por supuesto, no tendrá sentido para la mayoría de los propósitos. Solo será la representación de memoria del flotador, interpretada como un int.
Jerry B

4

Las uniones se utilizan cuando desea modelar estructuras definidas por hardware, dispositivos o protocolos de red, o cuando está creando una gran cantidad de objetos y desea ahorrar espacio. Sin embargo, realmente no los necesita el 95% del tiempo, siga con el código fácil de depurar.


4

Muchas de estas respuestas se refieren a la transmisión de un tipo a otro. Aprovecho al máximo las uniones con los mismos tipos, solo un poco más (es decir, al analizar un flujo de datos en serie). Permiten que el análisis / construcción de un paquete enmarcado sea ​​trivial.

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

Editar El comentario sobre endianness y estructura de relleno son válidos y grandes preocupaciones. He usado este cuerpo de código casi por completo en software embebido, la mayoría de los cuales tenía control de ambos extremos de la tubería.


1
Este código no funcionará (la mayoría de las veces) si los datos se intercambian a través de 2 plataformas diferentes debido a las siguientes razones: 1) Endianness puede ser diferente. 2) Acolchado en estructuras.
Mahori

@ Ravi Estoy de acuerdo con las preocupaciones sobre endianness y relleno. Sin embargo, se debe saber que he usado esto exclusivamente en proyectos integrados. La mayoría de los cuales controlaba ambos extremos de las tuberías.
Adam Lewis

1

Los sindicatos son geniales. Un uso inteligente de los sindicatos que he visto es usarlos al definir un evento. Por ejemplo, puede decidir que un evento es de 32 bits.

Ahora, dentro de esos 32 bits, es posible que desee designar los primeros 8 bits como un identificador del remitente del evento ... A veces trata el evento como un todo, a veces lo disecciona y compara sus componentes. los sindicatos te dan la flexibilidad para hacer ambas cosas.

Evento sindical
{
  unsigned long eventCode;
  unsigned char eventParts [4];
};

1

¿Qué pasa con VARIANTeso que se usa en las interfaces COM? Tiene dos campos: "tipo" y una unión que contiene un valor real que se trata según el campo "tipo".


1

En la escuela, usaba sindicatos como este:

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

Lo usé para manejar los colores más fácilmente, en lugar de usar operadores >> y <<, solo tuve que pasar por el índice diferente de mi matriz de caracteres.


1

Usé union cuando estaba codificando para dispositivos integrados. Tengo C int que tiene 16 bits de largo. Y necesito recuperar los 8 bits más altos y los 8 bits más bajos cuando necesito leer / almacenar en EEPROM. Así que usé de esta manera:

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

No requiere cambios, por lo que el código es más fácil de leer.

Por otro lado, vi un código antiguo de C ++ stl que usaba union para stl allocator. Si está interesado, puede leer el código fuente sgi stl . Aquí hay una parte:

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

1
¿No necesitarías una agrupación structalrededor de tu higher/ lower? En este momento, ambos deberían apuntar solo al primer byte.
Mario

@ Mario ah cierto, solo lo escribo a mano y lo olvido, gracias
Mu Qiao

1
  • Un archivo que contiene diferentes tipos de registros.
  • Una interfaz de red que contiene diferentes tipos de solicitud.

Eche un vistazo a esto: manejo del comando de almacenamiento intermedio X.25

Uno de los muchos comandos X.25 posibles se recibe en un búfer y se maneja en su lugar utilizando una UNIÓN de todas las estructuras posibles.


¿podría explicar estos dos ejemplos? Me refiero a cómo están relacionados con la unión
Amit Singh Tomar

1

En las primeras versiones de C, todas las declaraciones de estructura compartirían un conjunto común de campos. Dado:

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 compilador esencialmente produciría una tabla de tamaños de estructuras (y posiblemente alineaciones), y una tabla separada de nombres, tipos y compensaciones de miembros de estructuras. El compilador no realizó un seguimiento de qué miembros pertenecían a qué estructuras, y permitiría que dos estructuras tengan un miembro con el mismo nombre solo si el tipo y el desplazamiento coinciden (como con el miembro qde struct xy struct y). Si p fuera un puntero a cualquier tipo de estructura, p-> q agregaría el desplazamiento de "q" al puntero p y buscaría un "int" de la dirección resultante.

Dada la semántica anterior, era posible escribir una función que pudiera realizar algunas operaciones útiles en múltiples tipos de estructura de manera intercambiable, siempre que todos los campos utilizados por la función se alinearan con campos útiles dentro de las estructuras en cuestión. Esta era una característica útil, y cambiar C para validar los miembros utilizados para el acceso a la estructura contra los tipos de estructuras en cuestión habría significado perderlo en ausencia de un medio para tener una estructura que pueda contener múltiples campos con nombre en la misma dirección. Agregar tipos de "unión" a C ayudó a llenar ese vacío (aunque no, en mi humilde opinión, como debería haber sido).

Una parte esencial de la capacidad de los sindicatos para llenar ese vacío fue el hecho de que un puntero a un miembro de la unión podría convertirse en un puntero a cualquier unión que contenga ese miembro, y un puntero a cualquier unión podría convertirse en un puntero a cualquier miembro. Si bien el Estándar C89 no dijo expresamente que lanzar un T*directamente a un U*era equivalente a lanzarlo a un puntero a cualquier tipo de unión que contenga ambos Ty U, y luego lanzarlo a U*, ningún comportamiento definido de la última secuencia de lanzamiento se vería afectado por el tipo de unión utilizado, y el Estándar no especificó ninguna semántica contraria para un lanzamiento directo de Ta U. Además, en los casos en que una función recibió un puntero de origen desconocido, el comportamiento de escribir un objeto a travésT* conversión deT*a a U*, y luego leer el objeto a través U*sería equivalente a escribir una unión a través de un miembro de tipo Ty leer como tipo U, que se definiría de forma estándar en algunos casos (por ejemplo, al acceder a miembros de secuencia inicial común) y se definirá por implementación (más bien que indefinido) para el resto. Si bien era raro que los programas explotaran las garantías de la CEI con objetos reales de tipo sindical, era mucho más común explotar el hecho de que los punteros a objetos de origen desconocido tenían que comportarse como punteros a los miembros del sindicato y tener las garantías de comportamiento asociadas con ellos.


¿Puede dar un ejemplo de esto: `era posible escribir una función que pudiera realizar algunas operaciones útiles en múltiples tipos de estructura de manera intercambiable '. ¿Cómo se podrían utilizar múltiples estructuras miembro con el mismo nombre? Si dos estructuras tienen la misma alineación de datos y, por lo tanto, un miembro con el mismo nombre y el mismo desplazamiento que en su ejemplo, ¿de qué estructura proporcionaría los datos reales? (valor). Dos estructuras tienen la misma alineación y los mismos miembros, pero diferentes valores en ellas. ¿Puedes por favor elaborarlo?
pastor

@Herdsman: en las primeras versiones de C, un nombre de miembro de estructura encapsulaba un tipo y un desplazamiento. Dos miembros de diferentes estructuras pueden tener el mismo nombre si y solo si sus tipos y compensaciones coinciden. Si el miembro struct fooes un intcon desplazamiento 8, anyPointer->foo = 1234;significa "tomar la dirección en cualquier puntero, desplazarla por 8 bytes y realizar un almacén de enteros del valor 1234 a la dirección resultante. El compilador no necesitaría saber o importar si se anyPointeridentifica cualquier tipo de estructura que se haya fooincluido entre sus miembros
supercat

Con el puntero, puede desreferenciar cualquier dirección independientemente del 'origen' del puntero, eso es cierto, pero entonces, ¿cuál es el punto del compilador para contener tablas de miembros de estructuras y sus nombres (como dijiste en tu publicación) si puedo obtener datos ¿con algún puntero simplemente sabiendo la dirección de un miembro en una estructura particular? Y si el compilador no sabe si se anyPointeridentifica con un miembro de estructura, ¿cómo comprobará el compilador estas condiciones to have a member with the same name only if the type and offset matchedde su publicación?
Pastor

@Herdsman: el compilador mantendría la lista de los nombres de los miembros de la estructura porque el comportamiento preciso de p->foodependería del tipo y la compensación de foo. Esencialmente, p->foofue taquigrafía para *(typeOfFoo*)((unsigned char*)p + offsetOfFoo). En cuanto a su última pregunta, cuando un compilador encuentra una definición de miembro de estructura, requiere que no exista ningún miembro con ese nombre o que el miembro con ese nombre tenga el mismo tipo y desplazamiento; Supongo que habría graznado si existiera una definición de miembro de estructura no coincidente, pero no sé cómo manejó los errores.
supercat

0

Un ejemplo simple y muy útil es ...

Imagina:

tiene un uint32_t array[2]y desea acceder al 3er y 4to byte de la cadena de bytes. que podría hacer *((uint16_t*) &array[1]). ¡Pero esto rompe tristemente las estrictas reglas de alias!

Pero los compiladores conocidos le permiten hacer lo siguiente:

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

técnicamente esto sigue siendo una violación de las reglas. pero todos los estándares conocidos admiten este uso.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.