Una domanda sull'unione in C - archivia come un tipo e leggi come un altro - è definita l'implementazione?
-
06-07-2019 - |
Domanda
Stavo leggendo dell'unione in C da K & amp; R, per quanto ho capito, una singola variabile in unione può contenere uno dei diversi tipi e se qualcosa viene memorizzato come un tipo ed estratto come un altro il risultato è puramente implementazione definita.
Ora controlla questo frammento di codice:
#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
Qui sto assegnando valori nel u.ch
ma recuperando sia da u.ch
che da u.i
. È definita l'implementazione? O sto facendo qualcosa di veramente stupido?
So che può sembrare molto principiante per la maggior parte delle altre persone, ma non riesco a capire il motivo dietro quell'output.
Grazie.
Soluzione
Questo è un comportamento indefinito. u.i
e u.ch
si trovano nello stesso indirizzo di memoria. Pertanto, il risultato della scrittura in uno e della lettura dall'altro dipende dal livello di ottimizzazione del compilatore, della piattaforma, dell'architettura e talvolta persino del compilatore. Pertanto, l'output di u.i
potrebbe non essere sempre 515
.
Esempio
Ad esempio gcc
sulla mia macchina produce due risposte diverse per -O0
e -O2
.
-
Poiché la mia macchina ha un'architettura little-endian a 32 bit, con
-O0
finisco con due byte meno significativi inizializzati su 2 e 3, due byte più significativi non sono inizializzati. Quindi la memoria del sindacato è simile alla seguente:{3, 2, garbage, garbage}
Quindi ottengo un output simile a
3 2 -1216937469
. -
Con
-O2
, ottengo l'output di3 2 515
come te, il che rende la memoria di unione{3, 2, 0, 0}
. Quello che succede è chegcc
ottimizza la chiamata aprintf
con valori effettivi, quindi l'output dell'assemblaggio sembra un equivalente di:#include <stdio.h> int main() { printf("%d %d %d\n", 3, 2, 515); return 0; }
Il valore 515 può essere ottenuto come spiegato in altre risposte ad altre domande a questa domanda. In sostanza, significa che quando
gcc
ha ottimizzato la chiamata, ha scelto gli zero come valore casuale di una possibile unione non inizializzata.
Scrivere a un membro del sindacato e leggere da un altro di solito non ha molto senso, ma a volte può essere utile per i programmi compilati con aliasing rigoroso .
Altri suggerimenti
La risposta a questa domanda dipende dal contesto storico, poiché le specifiche della lingua sono cambiate nel tempo. E questa faccenda sembra essere quella interessata dai cambiamenti.
Hai detto che stavi leggendo K & amp; R. L'ultima edizione di quel libro (a partire da ora), descrive la prima versione standardizzata del linguaggio C - C89 / 90. In quella versione del linguaggio C scrivere un membro del sindacato e leggere un altro membro è comportamento indefinito . Non implementazione definita (che è una cosa diversa), ma comportamento indefinito . La parte pertinente dello standard linguistico in questo caso è 6.5 / 7.
Ora, in un momento successivo dell'evoluzione di C (versione C99 della specifica del linguaggio con Technical Corrigendum 3 applicato), divenne improvvisamente legale usare il sindacato per la punzonatura del tipo, cioè scrivere un membro del sindacato e poi leggerne un altro.
Nota che tentare di farlo può comunque portare a comportamenti indefiniti. Se il valore letto risulta non valido (la cosiddetta "rappresentazione trap") per il tipo con cui lo leggi, il comportamento è ancora indefinito. Altrimenti, il valore letto è definito dall'implementazione.
Il tuo esempio specifico è relativamente sicuro per la punzonatura di tipo da int
a char [2]
array. È sempre legale in linguaggio C reinterpretare il contenuto di qualsiasi oggetto come un array di caratteri (di nuovo, 6.5 / 7).
Tuttavia, non è vero il contrario. Scrivere i dati nel membro dell'array char [2]
della tua unione e poi leggerli come int
può potenzialmente creare una rappresentazione trap e portare a comportamento indefinito . Il potenziale pericolo esiste anche se il tuo array di caratteri ha una lunghezza sufficiente per coprire l'intero int
.
Ma nel tuo caso specifico, se int
sembra essere più grande di char [2]
, il int
coprirà un'area non inizializzata oltre la fine dell'array, che porta di nuovo a un comportamento indefinito.
Il motivo dietro l'output è che sulla tua macchina gli interi sono memorizzati in little-endian formato: i byte meno significativi vengono memorizzati per primi. Da qui la sequenza di byte [3,2,0,0] rappresenta l'intero 3 + 2 * 256 = 515.
Questo risultato dipende dall'implementazione specifica e dalla piattaforma.
L'output di tale codice dipenderà dalla piattaforma e dall'implementazione del compilatore C. Il tuo output mi fa pensare che stai eseguendo questo codice su un sistema litte-endian (probabilmente x86). Se dovessi mettere 515 in i e guardarlo in un debugger, vedresti che il byte di ordine più basso sarebbe un 3 e il byte successivo in memoria sarebbe un 2, che si associa esattamente a ciò che hai inserito in ch.
Se lo facessi su un sistema big-endian, avresti (probabilmente) ottenuto 770 (assumendo ints a 16 bit) o ??50462720 (assumendo ints a 32 bit).
Dipende dall'implementazione e i risultati possono variare su una piattaforma / compilatore diversi, ma sembra che questo sia ciò che sta accadendo:
515 in binario è
1000000011
Riempimento degli zeri per renderlo due byte (assumendo 16 bit int):
0000001000000011
I due byte sono:
00000010 and 00000011
Che è 2
e 3
Spero che qualcuno spieghi perché sono invertiti - la mia ipotesi è che i caratteri non siano invertiti ma l'int è un piccolo endian.
La quantità di memoria allocata a un'unione è uguale alla memoria richiesta per memorizzare il membro più grande. In questo caso, hai un int e un array di caratteri di lunghezza 2. Supponendo che int sia 16 bit e char sia di 8 bit, entrambi richiedono lo stesso spazio e quindi l'unione è allocata a due byte.
Quando si assegnano tre (00000011) e due (00000010) all'array char, lo stato dell'unione è 0000001100000010
. Quando leggi l'int da questa unione, converte tutto in intero. Supponendo che little-endian dove LSB è memorizzato all'indirizzo più basso, l'int read dall'unione sarebbe 0000001000000011
che è il binario per 515.
NOTA: questo vale anche se l'int era a 32 bit - Controlla La risposta di Amnon
Se si utilizza un sistema a 32 bit, un int è di 4 byte ma si inizializzano solo 2 byte. L'accesso a dati non inizializzati è un comportamento indefinito.
Supponendo che tu sia su un sistema con ints a 16 bit, allora quello che stai facendo è ancora l'implementazione definita. Se il tuo sistema è little endian, allora u.ch [0] corrisponderà al byte meno significativo di ui e u.ch 1 sarà il byte più significativo. Su un grande sistema endian, è il contrario. Inoltre, lo standard C non impone all'implementazione di utilizzare complemento a due per rappresentare numeri interi firmati valori, sebbene il complemento a due sia il più comune. Ovviamente, anche la dimensione di un numero intero è definita dall'implementazione.
Suggerimento: è più semplice vedere cosa succede se si utilizzano valori esadecimali. Su un piccolo sistema endian, il risultato in esadecimale sarebbe 0x0203.