2010-09-22 13 views
24

Si prega di considerare il seguente codice.Lanciare un puntatore di struct su un altro - 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; 
} 

Nella funzione cons, variabile ptr è di tipo cons_object*. Ma nel valore restituito viene convertito in tipo di object*.

  1. Mi chiedo come questo è possibile perché cons_object e object sono diverse strutture.
  2. Ci sono problemi nel fare cose come questa?

Qualsiasi pensiero!

risposta

30

Questo va bene ed è una tecnica abbastanza comune per l'implementazione di "orientamento all'oggetto" in C. Perché il layout di memoria di struct s è ben definito in C, purché i due oggetti condividano lo stesso layout, quindi è possibile lanciare in modo sicuro i puntatori tra di loro. Cioè, l'offset del membro type è lo stesso nella struttura object come nella struttura cons_object.

In questo caso, il membro type dice l'API se l'object è un cons_object o foo_object o qualche altro tipo di oggetto, per cui si potrebbe essere vedere qualcosa di simile:

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 
} 

Più comunemente, io' ve sembrare implementazioni in cui la classe "padre" è definito come il primo membro della classe "bambino", in questo modo:

typedef struct { 
    enum type type; 
} object; 

typedef struct { 
    object parent; 

    object *car; 
    object *cdr; 
} cons_object; 

Questo funziona in gran parte allo stesso modo, tranne che hai un forte ga garantire che il layout di memoria delle "classi" del bambino sia lo stesso dei genitori. Cioè, se aggiungi un membro alla 'base' object, verrà automaticamente rilevato dai bambini e non dovrai assicurarti manualmente che tutte le strutture siano sincronizzate.

+1

interessante. questa conoscenza sta semplificando molto il mio codice. Grazie. –

+3

Si dovrebbe menzionare che questo è C legittimo con un comportamento ben definito, e non un "hack" o invocazione di "comportamento non definito". –

+0

ri. object-in-cons_object: puoi anche utilizzare macro per rendere un po 'più sicuro il typecasting in questo caso, ad es. #define OBJECT (x) & ((x) -> parent). Questo non ha costi di runtime (è un indirizzo di memoria uguale a x), ma significa che non casualmente casti qualcosa di strano. –

14

Per aggiungere alla risposta di Dean, ecco qualcosa sulle conversioni puntatore in generale. Ho dimenticato qual è il termine, ma un puntatore al cast del puntatore non esegue alcuna conversione (nello stesso modo in cui int è float). È semplicemente una reinterpretazione dei bit a cui puntano (tutto per il beneficio del compilatore). "Conversione non distruttiva" penso che sia stato. I dati non cambiano, solo come il compilatore interpreta ciò che viene indicato.

esempio,
Se ptr è un puntatore a un object, il compilatore sa che esiste un campo con un offset particolare denominato type di tipo enum type. D'altra parte se ptr viene lanciato su un puntatore a un tipo diverso, cons_object, di nuovo saprà come accedere ai campi dello cons_object ciascuno con i propri offset in modo simile.

Per illustrare immaginare il layout di memoria per un cons_object:

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

Il campo type ha compensato 0, car è 4, cdr è 8. Per accedere al campo auto, tutto il compilatore ha bisogno di fare è aggiungere 4 al puntatore alla struttura.

Se il puntatore è stato lanciato ad un puntatore ad un object:

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

Tutto il compilatore ha bisogno di sapere è che c'è un campo denominato type con all'offset 0. Tutto ciò che è in memoria è in memoria.

I puntatori non devono nemmeno essere correlati. È possibile avere un puntatore a int e convertirlo in un puntatore a cons_object. Se si dovesse accedere al campo car, è proprio come qualsiasi normale accesso alla memoria. Ha un certo offset dall'inizio della struttura. In questo caso, ciò che è in quella posizione di memoria è sconosciuto ma non è importante. Per accedere a un campo, è necessario solo l'offset e tali informazioni sono reperibili nella definizione del tipo.

un puntatore ad un int punti per un blocco di memoria:

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

Casted a un puntatore cons_object:

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

fantastico !. Grazie per il post dettagliato! –

6

Utilizzando le strutture separati viola la regola rigorosa aliasing ed è un comportamento indefinito: http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

L'utilizzo di una struttura incorporata come nell'ultimo esempio di Dean va bene.

+0

AFAIK, questa risposta è corretta e la risposta (attualmente) accettata è errata. Vedi anche https://stackoverflow.com/questions/47710585/does-inheritance-via-unwinding-violate-strict-aliasing-rule –

Problemi correlati