2010-09-22 13 views
24

Proszę wziąć pod uwagę następujący kod.Przesyłanie jednego wskaźnika struktury do drugiego - 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; 
} 

W funkcji cons zmienna ptr jest typu cons_object*. Ale w wartości zwracanej jest konwertowany na typ object*.

  1. Zastanawiam się, jak to jest możliwe, ponieważ cons_object i object różne elemencie.
  2. Czy są jakieś problemy z robieniem takich rzeczy?

Wszelkie przemyślenia!

Odpowiedz

30

To jest w porządku i jest dość powszechną techniką implementacji "orientacji obiektowej" w C. Ponieważ układ pamięci struct s jest dobrze zdefiniowany w C, o ile oba obiekty mają ten sam układ, to można bezpiecznie rzucają między siebie wskaźniki. Oznacza to, że przesunięcie elementu type jest takie samo w strukturze object, co w strukturze cons_object.

W tym przypadku członek type opowiada API czy object jest cons_object lub foo_object lub jakiś inny rodzaj obiektu, więc może być zobaczyć coś takiego:

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 
} 

Częściej ja” ve wydają implementacje gdzie klasa „rodzic” jest zdefiniowany jako pierwszy członek klasy „dziecko”, tak:

typedef struct { 
    enum type type; 
} object; 

typedef struct { 
    object parent; 

    object *car; 
    object *cdr; 
} cons_object; 

działa to w dużej mierze ten sam sposób, chyba że masz silną ga urantee, że układ pamięci "klas" dziecka będzie taki sam jak rodziców. Oznacza to, że jeśli dodasz członka do "bazy" object, zostanie ona automatycznie pobrana przez dzieci i nie będziesz musiał ręcznie sprawdzać, czy wszystkie struktury są zsynchronizowane.

+1

interesujące. ta wiedza znacznie uprości mój kod. dzięki. –

+3

Należy wspomnieć, że jest to uzasadnione C z dobrze określonym zachowaniem, a nie "hack" lub inwokacja "niezdefiniowanego zachowania". –

+0

re. object-in-cons_object - możesz również użyć makr, aby w tym przypadku rzutowanie było nieco bezpieczniejsze, np. #define OBJECT (x) i ((x) -> rodzic). To nie ma kosztu runtime (jest to adres pamięci równy x), ale oznacza, że ​​nie rzucasz przypadkowo czegoś dziwnego. –

14

Aby dodać do odpowiedzi Dean, tutaj jest coś o ogólnych konwersjach wskaźnika. Zapomniałem, co to jest termin, ale wskaźnik do rzutowania wskaźnika nie wykonuje żadnej konwersji (w ten sam sposób, w jaki int jest float). Jest to po prostu reinterpretacja bitów, na które wskazują (wszystko dla korzyści kompilatora). "Nieniszcząca konwersja" Myślę, że tak było. Dane się nie zmieniają, tylko jak kompilator interpretuje to, na co się wskazuje.

np
Jeśli ptr jest wskaźnikiem do object, kompilator wie, że istnieje pole szczególna przesunięcie o nazwie type typu enum type. Z drugiej strony, jeśli ptr jest rzutowany na wskaźnik na inny typ, cons_object, ponownie będzie wiedział, jak uzyskać dostęp do pól każdego z nich z ich własnymi przesunięciami w podobny sposób.

Aby zilustrować wyobrazić układ pamięci dla cons_object:

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

Pole type ma przesunięcie 0, car wynosi 4, cdr wynosi 8. Aby uzyskać dostęp do pola samochodu, wszystko kompilator musi zrobić, to dodać 4 do wskaźnika do struktury.

Jeśli wskaźnik został wrzucony do wskaźnika do object:

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

Wszystko kompilator musi wiedzieć, że istnieje pole o nazwie type z offsetem 0. Cokolwiek jest w pamięci znajduje się w pamięci.

Wskaźniki nawet nie muszą być ze sobą powiązane. Możesz mieć wskaźnik do int i przesłać go do wskaźnika do cons_object. Jeśli masz dostęp do pola car, to tak jak zwykły dostęp do pamięci. Ma pewne przesunięcie względem początku struktury. W tym przypadku to, co jest w tej lokalizacji pamięci, jest nieznane, ale to nieważne. Aby uzyskać dostęp do pola, potrzebne jest tylko przesunięcie i informacja ta znajduje się w definicji typu.

wskaźnik do int punktów do bloku pamięci:

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

odlewano do cons_object palików:

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

niesamowite !. Dzięki za szczegółowy post! –

6

Korzystanie z oddzielnych konstrukcjom narusza ścisłe reguły aliasingu i jest niezdefiniowane zachowanie: http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

Użycie wbudowanej struktury, jak w ostatnim przykładzie Deana, jest w porządku.

+0

AFAIK, ta odpowiedź jest poprawna i (obecnie) zaakceptowana odpowiedź jest błędna. Zobacz także https://stackoverflow.com/questions/47710585/does-inheritance-via-unwinding-violate-strict-aliasing-rule –

Powiązane problemy