2010-05-24 11 views
8

Como parte de responder a otra pregunta, me encontré con un código como este, que gcc compila sin quejarse.¿Cómo es legal hacer referencia a un tipo indefinido dentro de una estructura?

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

Este es el medio que siempre he utilizado para construir tipos que apuntan a sí mismos (por ejemplo, listas enlazadas), pero siempre he pensado que tenía que nombre de la estructura lo que podría utilizar autorreferencia . En otras palabras, no puede usar xyz *z dentro de la estructura porque el typedef aún no está completo en ese punto.

Pero esta muestra particular hace no nombre la estructura y aún compila. Al principio pensé que había algo de magia negra en el compilador que traducía automáticamente el código anterior porque la estructura y los nombres typedef eran los mismos.

Pero esta pequeña belleza funciona así:

typedef struct { 
    struct NOTHING_LIKE_xyz *z; 
} xyz; 

Qué me estoy perdiendo aquí? Esto parece una clara violación ya que no hay ningún tipo de struct NOTHING_LIKE_xyz definido en ninguna parte.

Cuando cambio desde un puntero a un tipo real, me sale el error esperado:

typedef struct { 
    struct NOTHING_LIKE_xyz z; 
} xyz; 

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

Además, cuando quito la struct, me sale un error (parse error before "NOTHING ...).

¿Está permitido en ISO C?


Actualización: Un struct NOSUCHTYPE *variable; también compila por lo que es no sólo dentro estructuras en los que parece ser válida. No puedo encontrar nada en el estándar c99 que permita esta indulgencia con los indicadores de estructura.

+1

El segundo y el tercer bloque de códigos son idénticos. – badp

+0

Lo siento, solucionado eso. Cut'n'paste error de usuario. – paxdiablo

+0

+1, esa pregunta dejó a varias personas rascándose la cabeza (yo incluido). –

Respuesta

6

Las partes de la norma C99 que está después son 6.7.2.3, párrafo 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.

... y 6.2.5 párrafo 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

Eso es lo que quería ver, ser un abogado de lengua anal-retentivo :-) Aunque es para8 en mi copia (pero tengo la actualizada a TC3 para que pueda explicar eso). – paxdiablo

7

Como dice el aviso en el segundo caso, es un struct NOTHING_LIKE_xyzincompleta tipo, como void o matrices de tamaño desconocido. Un tipo incompleto solo puede aparecer como un tipo señalado, con una excepción para las matrices de tamaño desconocido que están permitidas como el último miembro de una estructura, lo que hace que la estructura misma sea un tipo incompleto en este caso. El código que sigue no puede desreferenciar ningún puntero a un tipo incompleto (por una buena razón).

Los tipos incompletos pueden ofrecer algún tipo de encapsulado de tipo de datos en C ... El párrafo correspondiente en http://www.ibm.com/developerworks/library/pa-ctypes1/ parece una buena explicación.

+0

+1 para la explicación y referencia a documentos de soporte. – paxdiablo

2

Los casos 1 y 2 están bien definidos, porque se conoce el tamaño y la alineación de un puntero. El compilador de C solo necesita el tamaño y la información de alineación para definir una estructura.

El tercer caso no es válido porque se desconoce el tamaño de esa estructura real.

Pero ten en cuenta que para el primero caso de ser lógico, es necesario dar un nombre a la estructura:

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

de lo contrario la estructura exterior y la *z Se considerará que dos estructuras diferentes.


El segundo caso tiene un caso de uso popular conocido como "opaque pointer" (pimpl). Por ejemplo, se podría definir una estructura de envoltura como

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

en la cabecera, y luego en uno de los .c,

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

la ventaja es de ese .c, no se puede jugar con los elementos internos del objeto. Es un tipo de encapsulación.

+0

+1 para la información "en realidad es estructuras diferentes". – paxdiablo

+0

De acuerdo, tu explicación me ayudó a entender la ventaja de la encapsulación. ¡Ese artículo de wikipedia realmente condujo el concepto a casa! –

1

Tienes que ponerle el nombre. En este:

typedef struct { 
    struct xyz *z; 
} xyz; 

no será capaz de apuntar a sí mismo como z se refiere a algún otro tipo completo, no a la estructura sin nombre que acaba de definir. Pruebe esto:

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

Aparecerá un error con respecto a los tipos incompatibles.

+0

Recibo una advertencia de gcc (c en lugar de C++) pero +1 para señalar el hecho de que en realidad son _different_ tipos. – paxdiablo

0

Me preguntaba sobre esto también. Resulta que el struct NOTHING_LIKE_xyz * z avanza declarando struct NOTHING_LIKE_xyz. Como un ejemplo enrevesado,

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

struct foo { 
    int i; 
}; 

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

Aquí f->bar refiere al tipo struct foo, no typedef struct { ... } foo. La primera línea compilará bien, pero la segunda dará un error. Poco uso para una implementación de lista enlazada.

+0

Puede declararse hacia delante, o struct foo no puede definirse en absoluto dentro de la unidad de compilación, en cuyo caso es un tipo incompleto. –

1

Bueno ... Todo Puedo decir es que su suposición anterior era incorrecta. Cada vez que utiliza una construcción struct X (por sí misma o como parte de una declaración más amplia), se interpreta como una declaración de un tipo de estructura con una etiqueta struct X. Podría ser una nueva declaración de un tipo de estructura declarado previamente. O bien, puede ser una primera declaración de un nuevo tipo de estructura. La nueva etiqueta se declara en el alcance en el que aparece. En su ejemplo específico, resulta ser un alcance de archivo (ya que el lenguaje C no tiene "alcance de clase", como sería en C++).

El ejemplo más interesante de este comportamiento es cuando la declaración aparece en la función prototipo:

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

En este caso, la nueva declaración struct X tiene ámbito función prototipo, que termina al final del prototipo .Si se declara un archivo-alcance struct X tarde

struct X; 

y tratar de pasar un puntero de struct X tipo a la función anterior, el compilador le dará un diagnóstico acerca de la no-coincidencia de tipo de puntero

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

esto también significa inmediatamente que en las siguientes declaraciones

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

cada declaración de struct X es una declaración de una tipo diferente, cada local a su propio alcance de prototipo de función.

Pero si usted pre-declarar struct X como en

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

todos struct X referencias en todo el prototipo de la función se referirán a la misma Anteriormente: declarada struct X tipo.

0

Cuando se declara una variable o campo de un tipo de estructura, el compilador tiene que asignar suficientes bytes para mantener esa estructura. Dado que la estructura puede requerir un byte, o puede requerir miles, no hay forma de que el compilador sepa cuánto espacio necesita asignar. Algunos lenguajes utilizan compiladores de múltiples pasos que podrían conocer el tamaño de la estructura en una sola pasada y asignarle el espacio en una pasada posterior; ya que C fue diseñado para permitir la compilación de un solo pase, sin embargo, eso no es posible. Por lo tanto, C prohíbe la declaración de variables o campos de tipos de estructura incompletos.

Por otro lado, cuando se declara una variable o campo de un tipo de puntero a estructura, el compilador tiene que asignar suficientes bytes para mantener un puntero a la estructura. Independientemente de si la estructura ocupa un byte o un millón, el puntero siempre requerirá la misma cantidad de espacio. Efectivamente, el compilador puede pisar el puntero al tipo incompleto como un vacío * hasta que obtenga más información sobre su tipo, y luego lo trate como un puntero al tipo apropiado una vez que descubra más sobre él. El puntero de tipo incompleto no es muy análogo a void *, ya que uno puede hacer cosas con void * que no se puede hacer con tipos incompletos (por ejemplo, si p1 es un puntero a struct s1, y p2 es un puntero a struct s2, no se puede asignar p1 a p2) pero no se puede hacer nada con un puntero a un tipo incompleto que no se podría hacer para anular *. Básicamente, desde la perspectiva del compilador, un puntero a un tipo incompleto es una burbuja de bytes del tamaño de un puntero. Puede copiarse desde o hacia otros bloques de bytes del tamaño de puntero similares, pero eso es todo. el compilador puede generar código para hacer eso sin tener que saber qué otra cosa va a hacer con los blobs de bytes del tamaño de puntero.

Cuestiones relacionadas