Uma pergunta sobre a união em C - loja como um tipo e ler como um outro - é definido pela implementação?
-
06-07-2019 - |
Pergunta
Eu estava lendo sobre união em C de K & R, tanto quanto eu entendi, a única variável em união pode conter qualquer um dos vários tipos e se algo é armazenado como um tipo e extraído como outro o resultado é puramente implementação definida .
Agora, por favor, verifique este trecho de código:
#include<stdio.h>
int main(void)
{
union a
{
int i;
char ch[2];
};
union a u;
u.ch[0] = 3;
u.ch[1] = 2;
printf("%d %d %d\n", u.ch[0], u.ch[1], u.i);
return 0;
}
Output:
3 2 515
Aqui eu estou atribuindo valores no u.ch
mas a recuperação de ambos u.ch
e u.i
. Trata-se de implementação definido? Ou estou fazendo algo realmente bobo?
Eu sei que pode parecer muito iniciante a maioria de outras pessoas, mas eu sou incapaz de descobrir a razão por trás disso saída.
Graças.
Solução
Este é um comportamento indefinido. u.i
e u.ch
estão localizados no mesmo endereço de memória. Assim, o resultado de escrever em um e lê a partir do outro depende do compilador, plataforma, arquitetura, e por vezes mesmo nível de otimização do compilador. Portanto, a saída para u.i
pode não ser sempre 515
.
Exemplo
Por exemplo gcc
na minha máquina produz duas respostas diferentes para -O0
e -O2
.
-
Porque a minha máquina tem uma arquitectura pouco-endian de 32 bits, com
-O0
acabo com dois bytes menos significativos inicializadas para 2 e 3, dois bytes mais significativos são inicializado. Então olhares memória do sindicato como este:{3, 2, garbage, garbage}
Por isso fico com a saída semelhante ao
3 2 -1216937469
. -
Com
-O2
, recebo a saída do3 2 515
como você faz, o que torna{3, 2, 0, 0}
memória união. O que acontece é quegcc
otimiza a chamada paraprintf
com valores reais, então a aparência de saída de montagem como um equivalente de:#include <stdio.h> int main() { printf("%d %d %d\n", 3, 2, 515); return 0; }
O valor 515 pode ser obtido como outro explicou em outras respostas para essa pergunta. Em essência, isso significa que quando
gcc
otimizado a chamada que escolheu zeros como o valor aleatório de um aspirante a união não inicializado.
escrita a um membro de união e leitura de outro normalmente não faz muito sentido, mas, por vezes, pode ser útil para programas compilados com estrita aliasing .
Outras dicas
A resposta a esta pergunta depende do contexto histórico, desde a especificação da linguagem mudou com o tempo. E essa questão passa a ser o único afetado pelas mudanças.
Você disse que você estava lendo K & R. A última edição do livro (a partir de agora), descreve a primeira versão padronizada da linguagem C - C89 / 90. Nessa versão da linguagem C escrevendo um membro da união e ler outro membro é comportamento indefinido . Não implementação definido (que é uma coisa diferente), mas indefinido comportamento. A parte relevante do padrão de linguagem, neste caso, é de 6,5 / 7.
Agora, em algum momento mais tarde na evolução da C (versão C99 da especificação da linguagem com Rectificação Técnico 3 aplicada), de repente, tornou-se legal para usar a união para o tipo de trocadilhos, ou seja, para escrever um membro da união e, em seguida, ler outra.
Note que a tentativa de fazer isso ainda pode levar a um comportamento indefinido. Se o valor que você lê passa a ser inválido (chamada "representação armadilha") para o tipo que você lê-lo completamente, então o comportamento ainda é indefinido. Caso contrário, o valor que você lê é definido pela implementação.
O específica exemplo é relativamente seguro para o tipo de trocadilhos de int
a variedade char[2]
. É sempre legal em linguagem C a reinterpret o conteúdo de qualquer objecto como uma matriz de char (novamente, 6.5 / 7).
No entanto, o inverso não é verdadeiro. Escrever dados no membro da matriz char[2]
de sua união e, em seguida, lê-lo como um int
pode potencialmente criar uma representação armadilha e levar a comportamento indefinido . O perigo potencial existe mesmo se a sua matriz de char tem comprimento suficiente para cobrir toda a int
.
Mas no seu caso específico, se int
passa a ser maior do que char[2]
, o int
você ler vai cobrir a área não inicializado para além do fim da matriz, que por sua vez leva a um comportamento indefinido.
A razão por trás da saída é que em seus inteiros da máquina são armazenados em little-endian formato: os bytes menos significativos são armazenados em primeiro lugar. Por isso, a sequência de bytes [3,2,0,0] representa o número inteiro 3 + 2 * 256 = 515.
Este resultado depende da implementação específica ea plataforma.
A saída de tal código será dependente de sua plataforma e implementação do compilador C. Sua saída faz-me pensar que você está executando este código em um sistema litte-endian (provavelmente x86). Se você fosse para colocar 515 em i e olhar para ele em um depurador, você veria que o byte de ordem mais baixa seria um 3 e o próximo byte na memória seria a 2, que mapeia exatamente ao que você colocou no ch.
Se você fez isso em um sistema big-endian, você teria que (provavelmente) obtido 770 (assumindo ints de 16 bits) ou 50462720 (assumindo ints de 32 bits).
É dependente de implementação e os resultados podem variar em uma plataforma diferente / compilador mas parece que isso é o que está acontecendo:
515 no binário é
1000000011
zeros preenchimento para tornar mais dois bytes (assumindo int 16 bits):
0000001000000011
Os dois bytes são:
00000010 and 00000011
Qual é 2
e 3
Hope alguém explica por que eles são invertidos -. Meu palpite é que caracteres não são revertidas mas o int é pouco endian
A quantidade de memória alocada para uma união é igual à memória necessária para armazenar o maior membro. Neste caso, você tem um int e uma matriz de char de comprimento 2. Assumindo int é de 16 bits e char é de 8 bits, ambos exigem mesmo espaço e, portanto, a união é alocado dois bytes.
Quando você atribuir três (00000011) e dois (00000010) para a matriz char, o estado de união é 0000001100000010
. Quando você lê a int desta união, ele converte a coisa toda em e inteiro. Assumindo representação little-endian onde LSB é armazenado no endereço mais baixo, a leitura int a partir da união seria 0000001000000011
que é o binário para 515.
NOTA: Isto é verdadeiro mesmo se o int foi de 32 bit - Verifique A resposta de Amnon
Se você estiver em um sistema de 32 bits, em seguida, um int é de 4 bytes, mas você só inicializar apenas 2 bytes. Acessar dados uninitialised é um comportamento indefinido.
Assumindo que você está em um sistema com ints de 16 bits, então o que você está fazendo ainda é implementação definida. Se o seu sistema é pouco endian, então u.ch [0] irá corresponder com o byte menos significativo de ui e u.ch 1 será o byte mais significativo. Em um sistema endian grande, é o contrário. Além disso, o padrão C não forçar a implementação de usar complemento de dois para representar inteiro assinado valores, apesar de complemento de dois é o mais comum. Obviamente, o tamanho de um inteiro também é definido pela implementação.
Dica: é mais fácil ver o que está acontecendo se você usar valores hexadecimais. Em um pequeno sistema endian, o resultado em hexadecimal seria 0x0203.