2010-09-22 21 views
24

Tenga en cuenta el siguiente código.Lanzar un puntero struct a otro - C

enum type {CONS, ATOM, FUNC, LAMBDA}; 

typedef struct{ 
    enum type type; 
} object; 

typedef struct { 
    enum type type; 
    object *car; 
    object *cdr; 
} cons_object; 

object *cons (object *first, object *second) { 
    cons_object *ptr = (cons_object *) malloc (sizeof (cons_object)); 
    ptr->type = CONS; 
    ptr->car = first; 
    ptr->cdr = second; 
    return (object *) ptr; 
} 

En la función cons, variables ptr es de tipo cons_object*. Pero en el valor de retorno se convierte al tipo de object*.

  1. Me pregunto cómo esto es posible porque cons_object y object son diferentes estructuras.
  2. ¿Hay algún problema al hacer cosas como esta?

¡Pensamientos!

Respuesta

30

Esto está muy bien y es una técnica bastante común para la implementación de "orientación a objetos" en C. Debido a que el diseño de memoria de struct s está bien definido en C, siempre y cuando los dos comparten el mismo diseño de objetos, entonces puede arrojar con seguridad punteros entre ellos. Es decir, el desplazamiento del miembro type es el mismo en la estructura object que en la estructura cons_object.

En este caso, el miembro type cuenta la API si el object es una cons_object o foo_object o algún otro tipo de objeto, por lo que es posible que se ve algo como esto:

void traverse(object *obj) 
{ 
    if (obj->type == CONS) { 
     cons_object *cons = (cons_object *)obj; 
     traverse(cons->car); 
     traverse(cons->cdr); 
    } else if (obj->type == FOO) { 
     foo_object *foo = (foo_object *)obj; 
     traverse_foo(foo); 
    } else ... etc 
} 

Más comúnmente, I' he parecer implementaciones donde la clase "padre" se define como el primer miembro de la clase "niño", así:

typedef struct { 
    enum type type; 
} object; 

typedef struct { 
    object parent; 

    object *car; 
    object *cdr; 
} cons_object; 

Esto funciona en gran medida de la misma manera, salvo que tenga un fuerte ga asegúrese de que el diseño de la memoria de las "clases" de los niños será el mismo que el de los padres. Es decir, si agrega un miembro a la 'base' object, los niños lo recogerán automáticamente y no tendrá que asegurarse manualmente de que todas las estructuras estén sincronizadas.

+1

interesante. este conocimiento va a simplificar mucho mi código. Gracias. –

+3

Debe mencionar que esto es C legítimo con un comportamiento bien definido, y no un "hack" o invocación de "comportamiento indefinido". –

+0

re. object-in-cons_object: también puede usar macros para hacer que el encasillado sea un poco más seguro en este caso, p. #define OBJECT (x) y ((x) -> parent). Esto no tiene costo de tiempo de ejecución (es una dirección de memoria igual a x) pero significa que no accidentalmente lanzas algo extraño. –

14

Para agregar a la respuesta de Dean, aquí hay algo acerca de las conversiones de punteros en general. Olvidé cuál es el término para esto, pero un puntero al molde de puntero no realiza ninguna conversión (del mismo modo que int float). Es simplemente una reinterpretación de los bits que señalan (todo para el beneficio del compilador). "Conversión no destructiva" Creo que fue. Los datos no cambian, solo cómo el compilador interpreta a qué se apunta.

por ejemplo,
Si ptr es un puntero a un object, el compilador sabe que hay un campo con un desplazamiento en particular llamado type de tipo enum type. Por otro lado, si ptr se convierte en un puntero a un tipo diferente, cons_object, nuevamente sabrá cómo acceder a los campos del cons_object cada uno con sus propios desplazamientos de forma similar.

Para ilustrar imaginar el diseño de memoria para un cons_object:

    +---+---+---+---+ 
cons_object *ptr -> | t | y | p | e | enum type 
        +---+---+---+---+ 
        | c | a | r | | object * 
        +---+---+---+---+ 
        | c | d | r | | object * 
        +---+---+---+---+ 

El campo type ha compensado 0, car es 4, es cdr 8. Para acceder al campo de coche, todo el compilador necesita hacer es añadir 4 al puntero a la estructura.

Si el puntero fue arrojado a un puntero a un object:

    +---+---+---+---+ 
((object *)ptr) -> | t | y | p | e | enum type 
        +---+---+---+---+ 
        | c | a | r | | 
        +---+---+---+---+ 
        | c | d | r | | 
        +---+---+---+---+ 

Todo el compilador necesita saber es que hay un campo llamado type con desplazamiento 0. Lo que está en la memoria está en la memoria.

Los punteros ni siquiera tienen que estar relacionados en absoluto. Puede tener un puntero a int y convertirlo en un puntero a cons_object. Si tuviera acceso al campo car, es como cualquier acceso normal a la memoria. Tiene un cierto desplazamiento desde el comienzo de la estructura. En este caso, lo que está en esa ubicación de memoria es desconocido, pero eso no es importante. Para acceder a un campo, solo se necesita el desplazamiento y esa información se encuentra en la definición del tipo.

un puntero a un int puntos a un bloque de memoria:

     +---+---+---+---+ 
int    *ptr -> | i | n | t | | int 
         +---+---+---+---+ 

Casted a un cons_object puntero:

     +---+---+---+---+ 
((cons_object *)ptr) -> | i | n | t | | enum type 
         +---+---+---+---+ 
         | X | X | X | X | object * 
         +---+---+---+---+ 
         | X | X | X | X | object * 
         +---+---+---+---+ 
+0

¡increíble !. Gracias por la publicación detallada! –

6

El uso de estructuras separadas viola la regla de alias estricto y es un comportamiento indefinido: http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

Usar una estructura incrustada como en el último ejemplo de Dean está bien.

+0

AFAIK, esta respuesta es correcta y la respuesta (actualmente) aceptada es incorrecta. Ver también https://stackoverflow.com/questions/47710585/does-inheritance-via-unwinding-violate-strict-aliasing-rule –

Cuestiones relacionadas