Por que precisamos de sindicatos C?

236

Quando os sindicatos devem ser usados? Por que nós precisamos deles?

timrau
fonte

Respostas:

252

As uniões são frequentemente usadas para converter entre as representações binárias de números inteiros e flutuantes:

union
{
  int i;
  float f;
} u;

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

Embora esse seja um comportamento tecnicamente indefinido de acordo com o padrão C (você deve ler apenas o campo que foi escrito mais recentemente), ele atuará de maneira bem definida em praticamente qualquer compilador.

Às vezes, as uniões também são usadas para implementar o pseudo-polimorfismo em C, fornecendo uma estrutura para alguma tag indicando que tipo de objeto ele contém e, em seguida, unindo os tipos possíveis:

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

Isso permite que o tamanho struct Sseja de apenas 12 bytes, em vez de 28.

Adam Rosenfield
fonte
deve haver uy vez de uf
Amit Singh Tomar
1
O exemplo que supõe converter float em número inteiro funciona? Eu acho que não, pois int e float são armazenados em diferentes formatos na memória. Você pode explicar o seu exemplo?
spin_eight
3
@spin_eight: Não está "convertendo" de float para int. Mais como "reinterpretar a representação binária de um float como se fosse um int". A saída não é 3: ideone.com/MKjwon Não sei por que Adam está imprimindo como hexadecimal.
endolith 21/02
@ Adam Rosenfield i realmente não understund a conversão eu não obter um número inteiro na saída: p
The Beast
2
Sinto que o aviso sobre comportamento indeterminado deve ser removido. Na verdade, é um comportamento definido. Consulte a nota de rodapé 82 da norma C99: Se o membro usado para acessar o conteúdo de um objeto de união não for o mesmo que o membro usado pela última vez para armazenar um valor no objeto, a parte apropriada da representação do objeto do valor será reinterpretada como uma representação de objeto no novo tipo, conforme descrito em 6.2.6 (um processo às vezes chamado de "punção de tipo"). Esta pode ser uma representação de interceptação.
Christian Gibbons
136

As uniões são particularmente úteis na programação incorporada ou em situações em que é necessário acesso direto ao hardware / memória. Aqui está um exemplo 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;

Então você pode acessar o registro da seguinte maneira:

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

Endianness (ordem de bytes) e arquitetura do processador são obviamente importantes.

Outro recurso útil é o 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;

Com este código, você pode acessar diretamente um único bit no endereço do registro / memória:

x = reg.bits.b2;
kgiannakakis
fonte
3
Sua resposta aqui em conjunto com a resposta de @Adam Rosenfield acima faz o par complementar perfeito: você demonstra usando uma estrutura dentro de uma união e ele demonstra usando uma união dentro de uma estrutura . Acontece que eu preciso de ambos de uma vez: uma estrutura dentro de uma união dentro de uma estrutura para implementar algum polimorfismo de passagem de mensagem em C entre threads em um sistema incorporado, e eu não teria percebido que se eu não tivesse visto as duas respostas juntas .
Gabriel Staples
1
Eu estava errado: é uma união dentro de uma estrutura dentro de uma união dentro de uma estrutura, aninhada à esquerda para escrever como eu escrevi isso, do aninhamento mais interno ao nível mais externo. Eu tive que adicionar outra união no nível mais interno para permitir valores de diferentes tipos de dados.
Gabriel Staples
64

A programação do sistema de baixo nível é um exemplo razoável.

IIRC, usei sindicatos para quebrar os registros de hardware nos bits do componente. Portanto, você pode acessar um registro de 8 bits (como foi no dia em que fiz isso ;-) nos bits do componente.

(Eu esqueço a sintaxe exata, mas ...) Essa estrutura permitiria que um registro de controle fosse acessado como um control_byte ou através dos bits individuais. Seria importante garantir que os bits sejam mapeados para os bits de registro corretos para uma determinada endianidade.

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
fonte
3
Este é um excelente exemplo! Aqui está um exemplo de como você pode usar esta técnica em software incorporado: edn.com/design/integrated-circuit-design/4394915/...
rzetterberg
34

Eu já vi isso em algumas bibliotecas como um substituto para a herança orientada a objetos.

Por exemplo

        Connection
     /       |       \
  Network   USB     VirtualConnection

Se você deseja que a "classe" Connection seja uma das opções acima, escreva algo como:

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

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

bb-generation
fonte
33

As uniões permitem que membros de dados mutuamente exclusivos compartilhem a mesma memória. Isso é muito importante quando a memória é mais escassa, como em sistemas embarcados.

No exemplo a seguir:

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

Essa união ocupará o espaço de um único int, em vez de três valores int separados. Se o usuário definir o valor de a e, em seguida, definir o valor de b , ele substituirá o valor de a, pois ambos estão compartilhando o mesmo local de memória.

LeopardSkinPillBoxHat
fonte
29

Muitos usos. Basta fazer grep union /usr/include/*ou em diretórios semelhantes. Na maioria dos casos, o unionenvoltório é um structe um membro da estrutura informa qual elemento da união acessar. Por exemplo, check-out man elfpara implementações da vida real.

Este é o princípio 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;
}
phoxis
fonte
Exatamente o que eu estava procurando ! Muito útil para substituir algum parâmetro reticências :)
Nicolas Voron
17

Aqui está um exemplo de união da minha própria base de código (da memória e parafraseada para que possa não ser exata). Foi usado para armazenar elementos de linguagem em um intérprete que eu construí. Por exemplo, o seguinte código:

set a to b times 7.

consiste nos seguintes elementos de linguagem:

  • símbolo [conjunto]
  • variável [a]
  • símbolo [para]
  • variável [b]
  • símbolo [vezes]
  • constante [7]
  • símbolo[.]

Os elementos de linguagem foram definidos como #definevalores ' ', assim:

#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 a seguinte estrutura foi usada para armazenar cada elemento:

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

o tamanho de cada elemento era o tamanho da união máxima (4 bytes para o tipo e 4 bytes para a união, embora esses sejam valores típicos, os tamanhos reais dependem da implementação).

Para criar um elemento "set", você usaria:

tElem e;
e.typ = ELEM_SYM_SET;

Para criar um elemento "variable [b]", você usaria:

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

Para criar um elemento "constant [7]", você usaria:

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

e você pode expandi-lo facilmente para incluir floats ( float flt) ou racionals ( struct ratnl {int num; int denom;}) e outros tipos.

A premissa básica é que o stre valnão são contíguos na memória, eles realmente se sobrepõem, portanto é uma maneira de obter uma visão diferente no mesmo bloco de memória, ilustrado aqui, onde a estrutura é baseada no local da memória 0x1010e números inteiros e ponteiros são ambos 4 bytes:

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

Se fosse apenas uma estrutura, ficaria assim:

       +-------+
0x1010 |       |
0x1011 |  typ  |
0x1012 |       |
0x1013 |       |
       +-------+
0x1014 |       |
0x1015 |  str  |
0x1016 |       |
0x1017 |       |
       +-------+
0x1018 |       |
0x1019 |  val  |
0x101A |       |
0x101B |       |
       +-------+
paxdiablo
fonte
O make sure you free this latercomentário deve ser removido do elemento constante?
21413 Trevor
Sim, @Trevor, embora eu não possa acreditar que você é a primeira pessoa que viu nos últimos 4 anos :-) Corrigido, e obrigado por isso.
precisa
7

Eu diria que facilita reutilizar a memória que pode ser usada de diferentes maneiras, ou seja, economizando memória. Por exemplo, você gostaria de criar uma estrutura "variante" capaz de salvar uma string curta e um número:

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

Em um sistema de 32 bits, isso resultaria no uso de pelo menos 96 bits ou 12 bytes para cada instância de variant.

Usando uma união, você pode reduzir o tamanho para 64 bits ou 8 bytes:

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

Você pode economizar ainda mais se desejar adicionar mais tipos diferentes de variáveis, etc. Pode ser verdade que você pode fazer coisas semelhantes lançando um ponteiro nulo - mas a união torna muito mais acessível e digita seguro. Essas economias não parecem enormes, mas você está economizando um terço da memória usada para todas as instâncias dessa estrutura.

Mario
fonte
5

É difícil pensar em uma ocasião específica em que você precisaria desse tipo de estrutura flexível, talvez em um protocolo de mensagens em que enviasse tamanhos diferentes de mensagens, mas mesmo assim provavelmente existem alternativas melhores e mais amigáveis ​​ao programador.

Os sindicatos são um pouco como tipos variantes em outros idiomas - eles só podem conter uma coisa de cada vez, mas podem ser int, float etc., dependendo de como você a declara.

Por exemplo:

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

MyUnion conterá apenas um int OU um float, dependendo do que você definiu mais recentemente . Então, fazendo isso:

MYUNION u;
u.MyInt = 10;

u agora possui um int igual a 10;

u.MyFloat = 1.0;

u mantém agora um valor flutuante igual a 1,0. Não possui mais um int. Obviamente agora, se você tentar imprimir printf ("MyInt =% d", u.MyInt); provavelmente você receberá um erro, embora não tenha certeza do comportamento específico.

O tamanho da união é determinado pelo tamanho do seu maior campo, neste caso o flutuador.

Xiaofu
fonte
1
sizeof(int) == sizeof(float)( == 32) normalmente.
Nick T
1
Para o registro, atribuir ao float e imprimir o int não causará um erro, pois nem o compilador nem o ambiente de tempo de execução sabem qual valor é válido. O int que é impresso será, obviamente, sem sentido para a maioria dos propósitos. Será apenas a representação de memória do flutuador, interpretada como um int.
Jerry B
4

As uniões são usadas quando você deseja modelar estruturas definidas por hardware, dispositivos ou protocolos de rede ou quando você cria um grande número de objetos e deseja economizar espaço. Você realmente não precisa deles 95% do tempo, fique com o código fácil de depurar.

Ana Betts
fonte
4

Muitas dessas respostas tratam da transmissão de um tipo para outro. Eu uso mais de uniões com os mesmos tipos, apenas mais deles (ou seja, ao analisar um fluxo de dados serial). Eles permitem que a análise / construção de um pacote emoldurado se torne 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 O comentário sobre endianness e struct padding são válidos e grandes preocupações. Eu usei esse corpo de código quase inteiramente no software incorporado, a maioria dos quais tinha controle das duas extremidades do pipe.

Adam Lewis
fonte
1
Esse código não funcionará (na maioria das vezes) se os dados estiverem sendo trocados por duas plataformas diferentes por causa dos seguintes motivos: 1) Endianness pode ser diferente. 2) Preenchimento de estruturas.
Mahori 16/03/14
@ Ravi Eu concordo com as preocupações sobre endianness e padding. No entanto, deve-se saber que eu usei isso exclusivamente em projetos incorporados. A maioria dos quais eu controlava as duas extremidades dos tubos.
Adam Lewis
1

Os sindicatos são ótimos. Um uso inteligente dos sindicatos que eu já vi é usá-los ao definir um evento. Por exemplo, você pode decidir que um evento é de 32 bits.

Agora, dentro desses 32 bits, você pode designar os primeiros 8 bits como um identificador do remetente do evento ... Às vezes você lida com o evento como um todo, às vezes você o disseca e compara seus componentes. os sindicatos lhe dão flexibilidade para fazer as duas coisas.

Evento de união
{
  eventCode longo não assinado;
  char não assinado eventParts [4];
};
dicroce
fonte
1

O VARIANTque é isso usado nas interfaces COM? Ele tem dois campos - "tipo" e uma união que mantém um valor real que é tratado dependendo do campo "tipo".

dente afiado
fonte
1

Na escola, usei sindicatos como este:

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

Usei-o para lidar com cores mais facilmente, em vez de usar os operadores >> e <<, apenas tive que passar pelo índice diferente da minha matriz de caracteres.

Zoneur
fonte
1

Eu usei union quando estava codificando para dispositivos incorporados. Eu tenho C int que tem 16 bits de comprimento. E preciso recuperar os 8 bits mais altos e os 8 bits mais baixos quando precisar ler de / armazenar na EEPROM. Então eu usei desta maneira:

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

Não requer mudança, portanto, o código é mais fácil de ler.

Por outro lado, vi algum código stl C ++ antigo que usava union para alocador stl. Se você estiver interessado, pode ler o código fonte sgi stl . Aqui está um pedaço:

union _Obj {
    union _Obj* _M_free_list_link;
    char _M_client_data[1];    /* The client sees this.        */
};
Mu Qiao
fonte
1
Você não precisaria de um agrupamento em structtorno de seu higher/ lower? No momento, ambos devem apontar apenas para o primeiro byte.
Mario
@Mario ah certo, eu só escrever à mão e esquecê-la, graças
Mu Qiao
1
  • Um arquivo contendo diferentes tipos de registro.
  • Uma interface de rede contendo diferentes tipos de solicitação.

Dê uma olhada nisto: Manipulação de comando de buffer X.25

Um dos muitos comandos X.25 possíveis é recebido em um buffer e tratado no local usando um UNION de todas as estruturas possíveis.

James Anderson
fonte
você poderia explicar ambos os exemplos .I média como estes estão relacionados à união
Amit Singh Tomar
1

Nas versões anteriores de C, todas as declarações de estrutura compartilhavam um conjunto comum 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];};

um compilador produziria essencialmente uma tabela de tamanhos de estruturas (e possivelmente alinhamentos) e uma tabela separada de nomes, tipos e compensações de membros de estruturas. O compilador não controlava quais membros pertenciam a quais estruturas e permitiria que duas estruturas tivessem um membro com o mesmo nome somente se o tipo e o deslocamento correspondessem (como no membro qde struct xe struct y). Se p fosse um ponteiro para qualquer tipo de estrutura, p-> q adicionaria o deslocamento de "q" ao ponteiro p e buscaria um "int" no endereço resultante.

Dada a semântica acima, foi possível escrever uma função que pudesse executar algumas operações úteis em vários tipos de estrutura de forma intercambiável, desde que todos os campos usados ​​pela função estivessem alinhados com campos úteis nas estruturas em questão. Esse era um recurso útil, e alterar C para validar membros usados ​​para acesso à estrutura em relação aos tipos de estruturas em questão significaria perdê-lo na ausência de um meio de ter uma estrutura que possa conter vários campos nomeados no mesmo endereço. A adição de tipos de "união" a C ajudou a preencher um pouco essa lacuna (embora não o IMHO, como deveria ter sido).

Uma parte essencial da capacidade dos sindicatos de preencher essa lacuna era o fato de um ponteiro para um membro do sindicato poder ser convertido em um ponteiro para qualquer união que contenha esse membro, e um ponteiro para qualquer união poderia ser convertido em um ponteiro para qualquer membro. Embora o Padrão C89 não tenha expressamente dito que converter um T*diretamente para a U*equivale a converter um ponteiro para qualquer tipo de união que contenha ambos Te U, e depois U*converter isso para , nenhum comportamento definido da última sequência de elenco seria afetado pelo tipo de união usado e o Padrão não especificou nenhuma semântica contrária para uma conversão direta de Tpara U. Além disso, nos casos em que uma função recebeu um ponteiro de origem desconhecida, o comportamento de escrever um objeto via T*, converter oT*para a U*e, em seguida, ler o objeto via U*seria equivalente a escrever uma união via membro do tipo Te ler como tipo U, que seria definido em alguns casos por padrão (por exemplo, ao acessar membros da Common Initial Sequence) e definido pela implementação (em vez Indefinido) para o resto. Embora fosse raro que programas explorassem as garantias do CIS com objetos reais do tipo união, era muito mais comum explorar o fato de que ponteiros para objetos de origem desconhecida deviam se comportar como ponteiros para membros do sindicato e ter as garantias comportamentais associadas a eles.

supercat
fonte
você pode dar um exemplo disso: `era possível escrever uma função que pudesse executar algumas operações úteis em vários tipos de estrutura de forma intercambiável`. Como poderia ser usado um membro de várias estruturas com o mesmo nome? Se duas estruturas têm o mesmo alinhamento de dados e, portanto, um membro com o mesmo nome e o mesmo deslocamento do exemplo, então, de qual estrutura eu produziria os dados reais? (valor). A estrutura dois tem o mesmo alinhamento e os mesmos membros, mas com valores diferentes. Você pode elaborá-lo
Pastor
@ Herdsman: Nas versões anteriores do C, um nome de membro struct encapsulava um tipo e um deslocamento. Dois membros de estruturas diferentes podem ter o mesmo nome se, e somente se, seus tipos e compensações corresponderem. Se o membro struct foofor um intcom deslocamento 8, anyPointer->foo = 1234;significava "pegar o endereço em anyPointer, substituí-lo por 8 bytes e executar um armazenamento inteiro do valor 1234 no endereço resultante. O compilador não precisaria saber ou se importar se anyPointeridentificado qualquer tipo de estrutura foolistado entre seus membros
supercat
Com o ponteiro, você pode desreferenciar qualquer endereço, independentemente da 'origem' do ponteiro, isso é verdade, mas qual é o objetivo do compilador conter tabelas de membros de estruturas e seus nomes (como você disse em sua postagem) se eu puder buscar dados com qualquer ponteiro apenas sabendo o endereço de um membro em uma estrutura específica? E se o compilador não souber se o anyPointeridentifica com um membro struct, como o compilador verifica essas condições to have a member with the same name only if the type and offset matchedda sua postagem?
Pastor
@ Herdsman: O compilador manteria a lista dos nomes dos membros da estrutura porque o comportamento preciso de p->foodependeria do tipo e deslocamento de foo. Essencialmente, p->fooera uma abreviação de *(typeOfFoo*)((unsigned char*)p + offsetOfFoo). Quanto à sua última pergunta, quando um compilador encontra uma definição de membro struct, exige que nenhum membro com esse nome exista ou que o membro com esse nome tenha o mesmo tipo e deslocamento; Eu diria que isso teria ocorrido se existisse uma definição de membro de estrutura não correspondente, mas não sei como ela lidou com erros.
supercat
0

Um exemplo simples e muito útil, é ....

Imagine:

você tem um uint32_t array[2]e deseja acessar o terceiro e o quarto bytes da cadeia de bytes. você poderia fazer *((uint16_t*) &array[1]). Mas isso infelizmente quebra as estritas regras de alias!

Mas os compiladores conhecidos permitem que você faça o seguinte:

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

tecnicamente, isso ainda é uma violação das regras. mas todos os padrões conhecidos suportam esse uso.

dhein
fonte