2010-03-11 18 views
7

Kiedy tworzymy obiekt klasy, jak wygląda mapa pamięci. Bardziej interesuje mnie, w jaki sposób obiekt wywołuje nie-wirtualne funkcje członkowskie. Czy kompilator tworzy tablicę vtable, która jest współdzielona między wszystkimi obiektami?Mapa pamięci obiektów klasy C++

class A 
{ 
public: 
    void f0() {} 
    int int_in_b1; 
}; 

A * a = new A; 

Jaka będzie mapa pamięci?

+4

Polecam "Wewnątrz obiektu obiektowego C++" Stanleya Lippmana, jeśli chcesz, aby można było modelować obiekty C++ (mówię, że może, ponieważ istnieje wiele sposobów implementacji elementów C++). –

+0

Jeśli poprawisz swój kod, dlaczego nie uruchomić kompilatora z danymi wyjściowymi asemblera i zobaczyć, co generuje? –

Odpowiedz

12

można sobie wyobrazić ten kod:

struct A { 
    void f() {} 
    int int_in_b1; 
}; 

int main() { 
    A a; 
    a.f(); 
    return 0; 
} 

przekształca się w coś w rodzaju:

struct A { 
    int int_in_b1; 
}; 
void A__f(A* const this) {} 

int main() { 
    A a; 
    A__f(&a); 
    return 0; 
} 

Wywołanie f jest prosty, ponieważ nie jest wirtualny. (Czasami w przypadku połączeń wirtualnych można uniknąć wirtualnej wysyłki, jeśli znany jest typ dynamiczny obiektu, tak jak jest tutaj.)


A dłuższą przykład, że będzie albo daje wyobrażenie o tym, jak wirtualne funkcje działają lub strasznie mylić:

struct B { 
    virtual void foo() { puts(__func__); } 
}; 
struct D : B { 
    virtual void foo() { puts(__func__); } 
}; 

int main() { 
    B* a[] = { new B(), new D() }; 
    a[0]->foo(); 
    a[1]->foo(); 
    return 0; 
} 

staje się czymś tak:

void B_foo(void) { puts(__func__); } 
void D_foo(void) { puts(__func__); } 

struct B_VT { 
    void (*foo)(void); 
} 
B_vtable = { B_foo }, 
D_vtable = { D_foo }; 

typedef struct B { 
    struct B_VT* vt; 
} B; 
B* new_B(void) { 
    B* p = malloc(sizeof(B)); 
    p->vt = &B_vtable; 
    return p; 
} 

typedef struct D { 
    struct B_VT* vt; 
} D; 
D* new_D(void) { 
    D* p = malloc(sizeof(D)); 
    p->vt = &D_vtable; 
    return p; 
} 

int main() { 
    B* a[] = {new_B(), new_D()}; 
    a[0]->vt->foo(); 
    a[1]->vt->foo(); 
    return 0; 
} 

Każdy obiekt ma tylko jeden wskaźnik vtable, a do klasy można dodać wiele metod wirtualnych bez wpływu na rozmiar obiektu. (Vtable rośnie, ale jest przechowywany tylko raz w klasie i nie ma znacznego wzrostu narzutów). Zauważ, że uprościłem wiele szczegółów w tym przykładzie, ale to does work: destruktory nie są adresowane (co powinno dodatkowo być tutaj wirtualne), to wycieka pamięć, a wartości są nieznacznie różne (między innymi są generowane przez kompilator dla nazwy bieżącej funkcji).

+0

Drugi przykład to kilka tygodni, które napisałem, a teraz widzę, że zapomniałem dodać * tego * wskaźników, mimo że nie są używane. Jeśli nie wiesz, jak je dodać, daj mi znać i mogę edytować; w przeciwnym razie zachowam to samo, co skompilowany kod w łączu kodowania. –

3

Rozpoznaj, że język C++ nie określa ani nie nakazuje wszystkiego w odniesieniu do układu pamięci obiektów. Powiedział, że większość kompilatorów robi to prawie tak samo.

W twoim przykładzie obiekty typu A wymagają tylko pamięci wystarczającej do przechowywania int. Ponieważ nie ma żadnych funkcji wirtualnych, nie potrzebuje vtable. Jeśli element f0 zostałby zadeklarowany jako wirtualny, obiekty typu A zwykle zaczynałyby się od wskaźnika do tabeli v klasy (dzielonej przez wszystkie obiekty typu A), po której następuje element int.

Z kolei vtable ma wskaźnik do każdej wirtualnej funkcji, zdefiniowany, odziedziczony lub przesłonięty. Wywołanie funkcji wirtualnej dla obiektu polega na podążaniu za wskaźnikiem do tabeli vtable z obiektu, a następnie za pomocą ustalonego przesunięcia w tabeli vtable (określanej podczas kompilacji dla każdej funkcji wirtualnej) w celu znalezienia adresu wywoływanej funkcji.

+0

Wiem, jak działa vtable. Jestem zainteresowany tym, jak kompilator radzi sobie z funkcją inną niż wirtualna. Czy jest dla nich osobny stolik? – Bruce

+1

@Peter: funkcje nie mają wpływu na rozmiar klasy i nie mają wpływu na układ. Funkcje są jak każda inna funkcja, którą piszesz, znajdują się w pamięci gdzieś czekając na wywołanie. Jedyną rzeczą związaną z funkcjami członków jest niejawny "ten" wskaźnik, którego nie widać. – GManNickG

+0

Kiedy więc piszę a.f0() w jaki sposób kompilator uzyskuje adres f0()? – Bruce

0
class A 
{ 
public: 
    void f0() {} 
    void f1(int x) {int_in_b1 = x; } 
    int int_in_b1; 
}; 

A *a = new A(); 

jest wewnętrznie realizowany (przedstawiciele) tak: (nazwa funkcji są rzeczywiście zniekształcone)

struct A 
{ 
    int int_in_b1; 
}; 

void Class_A__constructor(struct a*) {} // default constructor 
void Class_A__f0(struct a*) {} 
void Class_A__f1(struct a*, int x) {a->int_in_b1 = x;} 

// new is translated like this: (inline) 
void* new() { 
    void* addr = malloc(sizeof(struc a)); 
    Class_A__constructor(addr); 
    return addr; 
} 

Można sprawdzić, wykonując polecenia „nm” na pliku obiektu (wynik z zniekształcone nazwie)

+0

Skopiowano błąd "A a = nowy A();" z pytania. –

+0

@Roger: Dziękuję, nie zauważyłem – Phong

1

funkcje nie są przechowywane na podstawie jakiej klasy są w.

zwykle kompilator będzie tylko leczyć żadnej funkcji członka podobnie jak każdej innej funkcji poza dodaje argument dla "tego" wskaźnika. która jest automatycznie przekazywana do funkcji, gdy wywołałeś ją w oparciu o adres obiektu, do którego jest wywoływana.

wszystkie funkcje, statyczne, element członkowski, a nawet wirtualny element członkowski są przechowywane w pamięci w taki sam sposób, wszystkie są po prostu funkcjami.

kiedy kompilator buduje kod, który bardzo mocno koduje gdzie trafia do pamięci, linker przechodzi przez twój kod i zastępuje polecenie "wywołaj funkcję o tej nazwie" z "zadzwoń do funkcji pod ten adres zakodowany na sztywno "

Powiązane problemy