2010-05-24 3 views
8

Dans le cadre de la réponse à une autre question, je suis tombé sur un morceau de code comme celui-ci, que gcc compile sans se plaindre.Comment est-il légal de référencer un type indéfini dans une structure?

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

Ce sont les moyens que je l'ai toujours utilisé pour construire des types qui pointent vers eux-mêmes (par exemple, des listes liées) mais j'ai toujours pensé que vous deviez nom struct pour que vous puissiez utiliser l'auto-référence . En d'autres termes, vous ne pouvez pas utiliser xyz *z dans la structure car le typedef n'est pas encore terminé à ce stade.

Mais cet exemple particulier pas nomme la structure et il compile encore. Je pensais à l'origine qu'il y avait de la magie noire dans le compilateur qui traduisait automatiquement le code ci-dessus parce que la structure et les noms de typedef étaient les mêmes.

Mais cette petite beauté fonctionne aussi bien:

typedef struct { 
    struct NOTHING_LIKE_xyz *z; 
} xyz; 

Qu'est-ce que je manque ici? Cela semble une violation claire puisqu'il n'y a aucun type struct NOTHING_LIKE_xyz défini n'importe où.

Quand je changerai d'un pointeur vers un type réel, je reçois l'erreur attendue:

typedef struct { 
    struct NOTHING_LIKE_xyz z; 
} xyz; 

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

Aussi, lorsque je retire le struct, je reçois une erreur (parse error before "NOTHING ...).

Est-ce autorisé dans ISO C?


Mise à jour: Une struct NOSUCHTYPE *variable; compile également il est donc pas seulement à l'intérieur structures où il semble être valide. Je ne trouve rien dans la norme c99 qui permette cette clémence pour les pointeurs de structure.

+1

Les deuxième et troisième blocs de code sont identiques. – badp

+0

Désolé, réparé cela. Cut'n'paste erreur de l'utilisateur. – paxdiablo

+0

+1, Cette question a laissé plusieurs personnes se gratter la tête (moi inclus). –

Répondre

6

Les parties de la norme C99 vous êtes après sont 6.7.2.3, paragraphe 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.

... et 6.2.5 paragraphe 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

C'est ce que je voulais voir, étant un avocat de langue anale-rétentive :-) Bien que ce soit para8 dans ma copie (mais je l'ai mis à jour à TC3, ce qui peut expliquer cela). – paxdiablo

7

Comme l'avertissement dit dans le second cas, struct NOTHING_LIKE_xyz est un type incomplet , comme void ou des tableaux de taille inconnue. Un type incomplet ne peut apparaître que comme un type pointé, avec une exception pour les tableaux de taille inconnue qui sont autorisés en tant que dernier membre d'une structure, rendant la structure elle-même un type incomplet dans ce cas. Le code qui suit ne peut pas déréférencer un pointeur vers un type incomplet (pour une bonne raison).

Les types incomplets peuvent offrir une sorte d'encapsulation de types de données dans C ... Le paragraphe correspondant dans http://www.ibm.com/developerworks/library/pa-ctypes1/ semble être une bonne explication.

+0

+1 pour l'explication et la référence aux documents d'appui. – paxdiablo

2

Les 1er et 2ème cas sont bien définis, car la taille et l'alignement d'un pointeur sont connus. Le compilateur C n'a besoin que des informations de taille et d'alignement pour définir une structure.

Le 3ème cas n'est pas valide car la taille de cette structure réelle est inconnue.

Mais méfiez-vous que pour le 1er cas être logique, vous devez donner un nom à la struct:

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

sinon la struct extérieure et la *z seront considérés deux struct différents.


Le 2ème cas a un cas d'utilisation populaire connu sous le nom "opaque pointer" (pimpl). Par exemple, vous pouvez définir une structure d'emballage comme

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

dans l'en-tête, puis dans l'un des .c,

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

l'avantage est hors de cette .c, vous ne pouvez pas jouer avec les internes de l'objet. C'est une sorte d'encapsulation.

+0

+1 pour les infos "c'est en fait différent". – paxdiablo

+0

D'accord, votre explication m'a aidé à comprendre l'avantage de l'encapsulation. Cet article wikipedia a vraiment conduit le concept à la maison! –

1

Vous devez le nommer. Dans ce:

typedef struct { 
    struct xyz *z; 
} xyz; 

ne sera pas en mesure de pointer vers lui-même comme z fait référence à un autre type complet, pas au struct sans nom que vous venez de définir. Essayez ceci:

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

Vous obtiendrez une erreur sur les types incompatibles.

+0

Je reçois un avertissement de gcc (c plutôt que C++) mais +1 pour souligner le fait qu'ils sont réellement des types différents. – paxdiablo

0

Je m'interrogeais aussi à ce sujet. Il s'avère que le struct NOTHING_LIKE_xyz * z est en cours déclarant struct NOTHING_LIKE_xyz. A titre d'exemple alambiquée,

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

struct foo { 
    int i; 
}; 

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

Ici f->bar fait référence au type struct foo, pas typedef struct { ... } foo. La première ligne compilera bien, mais la seconde donnera une erreur. Pas beaucoup d'utilisation pour une implémentation de liste liée alors.

+0

Il peut s'agir d'une déclaration directe ou struct foo peut ne pas être définie du tout dans l'unité de compilation, auquel cas il s'agit d'un type incomplet. –

1

Eh bien ... Tout Je peux dire que votre hypothèse précédente était incorrecte. Chaque fois que vous utilisez une construction struct X (seule ou en tant que partie d'une déclaration plus grande), elle est interprétée comme une déclaration d'un type struct avec une balise struct X. Cela pourrait être une re-déclaration d'un type struct précédemment déclaré. Ou, ce peut être une toute première déclaration d'un nouveau type struct. La nouvelle balise est déclarée dans la portée dans laquelle elle apparaît. Dans votre exemple spécifique, il s'agit d'une portée de fichier (puisque le langage C n'a pas de "portée de classe", comme ce serait le cas en C++).

L'exemple plus intéressant de ce comportement est lorsque la déclaration apparaît dans un prototype de fonction:

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

Dans ce cas, la nouvelle déclaration struct X a champ fonction prototype, qui se termine à la fin du prototype .Si vous déclarez un fichier entier struct X plus tard

struct X; 

et essayez de passer un pointeur de type struct X à la fonction ci-dessus, le compilateur vous donnera un diagnostic sur le type de pointeur non correspondant

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

Cela signifie aussi immédiatement que dans les déclarations suivantes

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

chaque déclaration struct X est une déclaration d'un différent type, chacun local à sa propre portée de prototype de fonction.

Mais si vous pré-déclarer struct X comme dans

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

toutes struct X références dans tous les prototypes de fonction se référeront à la même previosly a déclaré le type struct X.

0

Lorsqu'une variable ou un champ d'un type de structure est déclaré, le compilateur doit allouer suffisamment d'octets pour contenir cette structure. Étant donné que la structure peut nécessiter un octet, ou qu'elle peut nécessiter des milliers, le compilateur n'a aucun moyen de connaître l'espace qu'il doit allouer. Certaines langues utilisent des compilateurs à passages multiples qui pourraient déterminer la taille de la structure en une passe et lui attribuer l'espace pour un passage ultérieur; puisque C a été conçu pour permettre la compilation en un seul passage, ce n'est pas possible. Ainsi, C interdit la déclaration de variables ou de champs de types de structure incomplets. En revanche, lorsqu'une variable ou un champ d'un type pointeur vers structure est déclaré, le compilateur doit allouer suffisamment d'octets pour contenir un pointeur vers la structure. Indépendamment du fait que la structure prenne un octet ou un million, le pointeur nécessitera toujours la même quantité d'espace. En fait, le compilateur peut passer le pointeur vers le type incomplet comme un void * jusqu'à ce qu'il obtienne plus d'informations sur son type, puis le traiter comme un pointeur vers le type approprié une fois qu'il en trouve plus à son sujet. Le pointeur de type incomplet n'est pas tout à fait analogue à void *, car on peut faire des choses avec void * qu'on ne peut pas faire avec des types incomplets (par exemple si p1 est un pointeur vers struct s1, et p2 est un pointeur vers struct s2, on ne peut assigner p1 à p2) mais on ne peut rien faire avec un pointeur vers un type incomplet que l'on ne pouvait pas faire pour annuler *. Fondamentalement, du point de vue du compilateur, un pointeur vers un type incomplet est un blob d'octets de la taille d'un pointeur. Il peut être copié vers ou à partir d'autres blocs d'octets de la taille d'un pointeur, mais c'est tout. le compilateur peut générer du code pour le faire sans avoir à savoir quoi d'autre va faire avec les blobs de taille de pointeur.

Questions connexes