2010-05-24 17 views
8

Come parte di rispondere a un'altra domanda, mi sono imbattuto in un pezzo di codice come questo, che gcc compila senza lamentarsi.Come è legale fare riferimento a un tipo non definito all'interno di una struttura?

typedef struct { 
    struct xyz *z; 
} xyz; 
int main (void) { 
    return 0; 
} 

Questo è il mezzo che ho sempre usato per costruire i tipi che puntano a se stessi (per esempio, le liste collegate), ma ho sempre pensato che dovevi nome struct così si potrebbe usare autoreferenzialità . In altre parole, non è possibile utilizzare xyz *z all'interno della struttura perché il typedef non è ancora completo in quel punto.

Ma questo particolare esempio fa non nome della struttura e si compila ancora. Inizialmente pensavo che nel compilatore ci fosse della magia nera che traduceva automaticamente il codice precedente perché la struttura e i nomi typedef erano gli stessi.

Ma questa piccola bellezza funziona così:

typedef struct { 
    struct NOTHING_LIKE_xyz *z; 
} xyz; 

Che cosa mi manca qui? Questa sembra una chiara violazione poiché non esiste un tipo struct NOTHING_LIKE_xyz definito ovunque.

Quando cambio da un puntatore ad un tipo effettivo, ottengo l'errore previsto:

typedef struct { 
    struct NOTHING_LIKE_xyz z; 
} xyz; 

qqq.c:2: error: field `z' has incomplete type 

Inoltre, quando rimuovo il struct, ottengo un errore (parse error before "NOTHING ...).

È consentito in ISO C?


Update: Un struct NOSUCHTYPE *variable; compila anche quindi non è solo all'interno strutture dove sembra essere valido. Non riesco a trovare nulla nello standard c99 che permetta questa clemenza per i puntatori di struttura.

+1

Il secondo e il terzo blocco di codice sono identici. – badp

+0

Spiacente, risolto quello. Errore utente cut'n'paste. – paxdiablo

+0

+1, questa domanda ha lasciato parecchie persone grattarsi la testa (me inclusa). –

risposta

6

Le parti dello standard C99 siete dopo sono 6.7.2.3, paragrafo 7:

If a type specifier of the form struct-or-union identifier occurs other than as part of one of the above forms, and no other declaration of the identifier as a tag is visible, then it declares an incomplete structure or union type, and declares the identifier as the tag of that type.

... e 6.2.5 punto 22:

A structure or union type of unknown content (as described in 6.7.2.3) is an incomplete type. It is completed, for all declarations of that type, by declaring the same structure or union tag with its defining content later in the same scope.

+0

Questo è quello che volevo vedere, essendo un avvocato linguistico a ritenzione anale :-) Anche se nella mia copia è para8 (ma ho quello aggiornato a TC3 in modo che possa spiegarlo). – paxdiablo

7

Come l'avviso dice nel secondo caso, struct NOTHING_LIKE_xyz è un tipo incompleto, come void o matrici di dimensioni sconosciute. Un tipo incompleto può apparire solo come un tipo puntato a, con un'eccezione per matrici di dimensioni sconosciute che sono consentite come ultimo membro di una struct, rendendo in questo caso la struct stessa un tipo incompleto. Il codice che segue non può dereferenziare alcun puntatore a un tipo incompleto (per una buona ragione).

I tipi incompleti possono offrire alcuni tipi di incapsulamento di tipi di dati in C ... Il paragrafo corrispondente in http://www.ibm.com/developerworks/library/pa-ctypes1/ sembra una buona spiegazione.

+0

+1 per la spiegazione e il riferimento ai documenti di supporto. – paxdiablo

2

Il 1 ° e il 2 ° caso sono ben definiti, poiché la dimensione e l'allineamento di un puntatore sono noti. Il compilatore C ha bisogno solo delle informazioni sulla dimensione e sull'allineamento per definire una struttura.

Il terzo caso non è valido perché la dimensione di quella struttura effettiva è sconosciuta.

ma attenzione che per il 1 ° caso di essere logico, è necessario dare un nome alla struct:

//    vvv 
typedef struct xyz { 
    struct xyz *z; 
} xyz; 

altrimenti l'esterno struct e il *z saranno prese in considerazione due le strutture differenti.


Il secondo caso ha un caso di uso comune noto come "opaque pointer" (pimpl). Ad esempio, è possibile definire una struct involucro come

typedef struct { 
    struct X_impl* impl; 
} X; 
// usually just: typedef struct X_impl* X; 
int baz(X x); 

nell'intestazione, e poi in una delle .c,

#include "header.h" 
struct X_impl { 
    int foo; 
    int bar[123]; 
    ... 
}; 
int baz(X x) { 
    return x.impl->foo; 
} 

il vantaggio è fuori di tale .c, non è possibile confusione con l'interno dell'oggetto. È una specie di incapsulamento.

+0

+1 per le informazioni "è in realtà una diversa struttura". – paxdiablo

+0

D'accordo, la tua spiegazione mi ha aiutato a capire il vantaggio dell'incapsulamento. Questo articolo di wikipedia ha davvero guidato il concetto a casa! –

1

Devi chiamarlo. In questo:

typedef struct { 
    struct xyz *z; 
} xyz; 

non sarà in grado di indicare se stesso come z riferisce a qualche altro tipo completo, non alla struct senza nome appena definito. Prova questo:

int main() 
{ 
    xyz me1; 
    xyz me2; 
    me1.z = &me2; // this will not compile 
} 

Otterrai un errore sui tipi incompatibili.

+0

Viene visualizzato un avviso con gcc (c anziché C++) ma +1 per indicare il fatto che in realtà sono tipi _different_. – paxdiablo

0

Mi stavo chiedendo anche questo. Risulta che il struct NOTHING_LIKE_xyz * z è in avanti dichiarando struct NOTHING_LIKE_xyz. Come esempio contorto,

typedef struct { 
    struct foo * bar; 
    int j; 
} foo; 

struct foo { 
    int i; 
}; 

void foobar(foo * f) 
{ 
    f->bar->i; 
    f->bar->j; 
} 

Qui f->bar si riferisce al tipo struct foo, non typedef struct { ... } foo. La prima riga verrà compilata correttamente, ma il secondo darà un errore. Non molto utile quindi per un'implementazione di liste collegate.

+0

Potrebbe essere in avanti dichiarante o struct foo non può essere definito affatto all'interno dell'unità di compilazione, nel qual caso è un tipo incompleto. –

1

Beh ... Tutto Posso dire che la tua precedente ipotesi non era corretta. Ogni volta che si utilizza un costrutto struct X (da solo o come parte di una dichiarazione più grande), viene interpretato come una dichiarazione di un tipo di struttura con un tag struct X. Potrebbe essere una nuova dichiarazione di un tipo di struttura precedentemente dichiarato. Oppure, può essere una prima dichiarazione di un nuovo tipo di struttura . Il nuovo tag è dichiarato nell'ambito in cui appare. Nel tuo esempio specifico capita di essere un ambito di file (dal momento che il linguaggio C non ha "scope di classe", come sarebbe in C++).

Il più interessante esempio di questo comportamento è quando la dichiarazione appare in funzione prototipo:

void foo(struct X *p); // assuming `struct X` has not been declared before 

In questo caso la nuova dichiarazione struct X trovi funzione-prototipo portata, che termina alla fine del prototipo .Se si dichiara un file-scope struct X tardi

struct X; 

e tenta di passare un puntatore di struct X tipo alla funzione di cui sopra, il compilatore vi darà una diagnosi sulla non-matching tipo di puntatore

struct X *p = 0; 
foo(p); // different pointer types for argument and parameter 

questo significa anche subito che nelle seguenti dichiarazioni

void foo(struct X *p); 
void bar(struct X *p); 
void baz(struct X *p); 

ciascuna dichiarazione struct X è una dichiarazione di un diverso tipo, ogni locale per il proprio ambito di prototipo di funzione.

Ma se si pre-dichiarare struct X come in

struct X; 
void foo(struct X *p); 
void bar(struct X *p); 
void baz(struct X *p); 

tutti struct X riferimenti in tutto prototipo di funzione faranno riferimento al stesso previosly dichiarato struct X tipo.

0

Quando viene dichiarata una variabile o un campo di un tipo di struttura, il compilatore deve allocare abbastanza byte per contenere quella struttura. Dato che la struttura può richiedere un byte, o potrebbe richiedere migliaia, non c'è modo per il compilatore di sapere quanto spazio è necessario allocare. Alcune lingue usano compilatori multi-pass che potrebbero scoprire la dimensione della struttura su un passaggio e allocare lo spazio per esso su un passaggio successivo; poiché C è stato progettato per consentire la compilazione a passaggio singolo, tuttavia, ciò non è possibile. Pertanto, C vieta la dichiarazione di variabili o campi di tipi di struttura incompleti.

D'altra parte, quando viene dichiarata una variabile o un campo di un tipo puntatore-struttura, il compilatore deve allocare abbastanza byte per contenere un puntatore alla struttura. Indipendentemente dal fatto che la struttura richieda un byte o un milione, il puntatore richiederà sempre la stessa quantità di spazio. In modo efficace, il compilatore può passare il puntatore al tipo incompleto come un vuoto * finché non ottiene più informazioni sul suo tipo e quindi trattarlo come un puntatore al tipo appropriato una volta che ne ha scoperto di più. Il puntatore di tipo incompleto non è del tutto analogo al vuoto *, in quanto si possono fare cose con vuoto * che non si può fare con tipi incompleti (ad esempio se p1 è un puntatore a struct s1 e p2 è un puntatore a struct s2, non si può assegnare p1 a p2) ma non si può fare nulla con un puntatore a un tipo incompleto che non si può fare per annullare *. Fondamentalmente, dal punto di vista del compilatore, un puntatore a un tipo incompleto è un blob di byte di dimensioni puntatore. Può essere copiato da o verso altri blob di byte simili a puntatori, ma il gioco è fatto. il compilatore può generare codice per farlo senza dover sapere che cosa altro farà con i blob di byte del puntatore.

Problemi correlati