Una domanda sull'unione in C - archivia come un tipo e leggi come un altro - è definita l'implementazione?

StackOverflow https://stackoverflow.com/questions/1812348

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.

È stato utile?

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 .

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

  2. Con -O2 , ottengo l'output di 3 2 515 come te, il che rende la memoria di unione {3, 2, 0, 0} . Quello che succede è che gcc ottimizza la chiamata a printf 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.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top