2010-03-11 73 views
7

當我們創建一個類的對象時,它的內存映射是什麼樣的。我更關心對象如何調用非虛擬成員函數。編譯器是否創建了像所有對象之間共享的vtable這樣的表?C++類對象內存映射

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

A * a = new A; 

什麼是內存映射?

+4

如果您想要如何建模C++對象(我說可以,因爲有多種方法來實現C++內部),我推薦Stanley Lippman的'Inside the C++ Object Model'。 – 2010-03-11 06:59:40

+0

如果你更正了你的代碼,爲什麼不用編譯器輸出運行你的編譯器,並看看它產生了什麼? – 2010-07-04 12:23:29

回答

12

你能想象這樣的代碼:

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

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

被改造成類似:

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

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

調用f是非常簡單的,因爲它是非虛擬的。 (有時對於虛擬調用,如果對象的動態類型是已知的,則虛擬調度可以避免,因爲它在這裏。)


較長的例子,要麼給你的功能如何虛功或可怕的想法迷惑你:

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; 
} 

變爲類似:

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; 
} 

每個對象只一個vtable指針,並且您可以將多個虛擬方法添加到該類中,而不會影響對象大小。 (vtable增長,但是每個類存儲一次,並且不佔用大量開銷。)請注意,在此示例中,我簡化了許多細節,但does work:析構函數沒有解決(它應該另外虛擬於此)泄漏內存,並且值將會略有不同(它們由編譯器爲當前函數的名稱生成)等等。

+0

第二個例子是我寫過幾個星期的舊書,現在我發現我忘了添加* this *指針,即使它們沒有被使用。如果你看不到如何添加它們,只需告訴我,我可以編輯;否則我會保持它與編碼板鏈接中編譯的代碼相同。 – 2010-03-11 07:10:29

3

認識到C++語言沒有指定或強制對象的所有內存佈局。也就是說,大多數編譯器都做得差不多。

在您的示例中,類型A的對象只需要足夠的內存來存放int。由於它沒有虛函數,所以不需要vtable。如果f0成員已被聲明爲虛擬的,則類型A的對象通常以指向類A vtable的指針(由所有類型爲A的對象共享)開始,後跟int成員。

反過來,vtable有一個指向每個虛函數的指針,定義,繼承或覆蓋。調用一個對象的虛擬函數包括跟蹤從對象到vtable的指針,然後在vtable中使用一個固定偏移量(在編譯時爲每個虛函數確定)來查找要調用的函數的地址。

+0

我知道vtable是如何工作的。我對編譯器如何處理非虛函數感興趣。他們是否還有單獨的桌子? – Bruce 2010-03-11 06:24:57

+1

@Peter:函數對類的大小沒有影響,也不影響佈局。函數就像你寫的任何其他函數一樣,它們駐留在等待被調用的地方。關於成員函數唯一的事情是他們有一個你沒有看到的隱含的'this'指針。 – GManNickG 2010-03-11 06:33:35

+0

所以當我編寫a.f0()時,編譯器如何獲取f0()的地址? – Bruce 2010-03-11 06:34:57

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

A *a = new A(); 

在內部實現(表示)是這樣的:(功能名實際上錯位)

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; 
} 

它可以通過執行在對象文件中的命令「nm」是被驗證(有錯位的命名導致)

+0

您從問題中複製了'A a = new A();'錯誤。 – 2010-03-11 06:59:17

+0

@Roger:謝謝,我沒有注意到 – Phong 2010-03-11 07:30:01

1

功能不存儲基於他們在什麼課。

通常編譯器將只把任何成員函數類似,只是任何其他功能爲「this」指針添加一個參數。當您根據調用的對象的地址調用該函數時,該函數將自動傳遞給該函數。

所有函數,靜態,成員甚至虛擬成員都以相同的方式存儲在內存中,它們都只是函數。

當編譯器構建代碼時,它會將代碼放到內存中,然後鏈接器會遍歷代碼並用「在此硬編碼地址中調用函數」替換「用此名稱調用函數」命令「