2009-02-18 75 views
10

虛擬類的每個對象都有一個指向vtable的指針嗎?虛擬類的每個對象都有一個指向vtable的指針嗎?

還是隻有具有虛函數的基類的對象才擁有它?

vtable存儲在哪裏?代碼段或數據段的過程?

+0

重複? http://stackoverflow.com/questions/99297/at-as-deep-of-a-level-as-possible-how-are-virtual-functions- implementation – Anonymous 2009-02-18 15:53:18

+0

有沒有這樣的事情,作爲一個「虛擬課堂」 C++。 – curiousguy 2016-04-22 03:14:58

回答

1

所有虛擬類通常都有一個vtable,但它不是C++標準所要求的,存儲方法依賴於編譯器。

4

Vtable是每個類實例,即如果我有一個具有虛擬方法的類的10個對象,那麼在所有10個對象中只有一個共享一個vtable。

本例中的所有10個對象都指向相同的vtable。

+0

Vptr怎麼樣,每個對象會有10個vptr關聯還是像單個vtable那樣只有一個vptr? – Rndp13 2015-06-02 02:23:24

0

多態類型的每個對象都會有一個指向Vtable的指針。

存儲的VTable依賴於編譯器。

15

所有具有虛擬方法的類都將有一個由該類的所有對象共享的vtable。

每個對象實例都有一個指向該vtable的指針(這是vtable的發現方式),通常稱爲vptr。編譯器隱式生成代碼來初始化構造函數中的vptr。

請注意,這些都不是C++語言所要求的 - 如果需要,實現可以用其他方式處理虛擬調度。但是,這是我熟悉的每個編譯器所使用的實現。 Stan Lippman的書「C++對象模型內部」描述了它如何很好地工作。

+2

+1你能解釋爲什麼虛擬指針是每個對象,而不是每個類?謝謝。 – Viet 2013-03-11 07:11:57

+1

@Viet您可以將vPtr視爲對象的運行時定義的引導。只有在vPtr被設置後,對象才能知道它的實際類型是什麼。在這個概念中,爲每個類創建一個vPtr(靜態)是沒有意義的。 想想另一種方式,如果一個對象不需要vPtr,那麼它在編譯時必須已經知道它的運行時定義,這與它是一個動態解析的對象相矛盾。 – 2014-03-12 06:57:36

0

不一定

幾乎每一個具有虛擬功能將有一個v表指針對象。對於每個具有對象派生的虛擬函數的類,不需要有v表指針。

儘管分析代碼的新編譯器可能能夠在某些情況下消除v表。例如,在一個簡單的情況下:如果只有一個抽象基類的具體實現,編譯器知道它可以將虛擬調用更改爲常規函數調用,因爲每當調用該虛函數時,它將始終解決完全相同的功能。另外,如果只有幾個不同的具體函數,編譯器可以有效地改變調用站點,以便它使用'if'來選擇正確的具體函數來調用。

因此,在這種情況下,不需要v表,並且對象可能最終沒有。

+0

嗯。我剛剛試圖找到一個可以消除v-表指針的編譯器。看起來不像目前有任何。但是,編譯器和鏈接器之間的信息共享變得越來越高,以至於它們正在融合在一起。隨着持續發展,這可能會發生。 – 2009-02-18 18:03:36

4

在家裏試試這個:

#include <iostream> 
struct non_virtual {}; 
struct has_virtual { virtual void nop() {} }; 
struct has_virtual_d : public has_virtual { virtual void nop() {} }; 

int main(int argc, char* argv[]) 
{ 
    std::cout << sizeof non_virtual << "\n" 
      << sizeof has_virtual << "\n" 
      << sizeof has_virtual_d << "\n"; 
} 
2

V表是一個實現細節沒有什麼語言定義,指出它的存在。事實上,我已閱讀了有關實現虛擬功能的其他方法。

但是:所有常見的編譯器(即我所知道的)使用VTabels。
然後是的。任何具有虛擬方法或從具有虛擬方法的類(直接或間接)派生​​的類將具有帶有指向VTable的指針的對象。

你問的所有其他問題將取決於編譯器/硬件,這些問題沒有真正的答案。

11

就像別人說的那樣,C++標準沒有強制使用虛擬方法表,但允許使用一個。我已經做了使用gcc和這個代碼,並儘可能簡單的場景我的一個測試:

class Base { 
public: 
    virtual void bark() { } 
    int dont_do_ebo; 
}; 

class Derived1 : public Base { 
public: 
    virtual void bark() { } 
    int dont_do_ebo; 
}; 

class Derived2 : public Base { 
public: 
    virtual void smile() { } 
    int dont_do_ebo; 
}; 

void use(Base*); 

int main() { 
    Base * b = new Derived1; 
    use(b); 

    Base * b1 = new Derived2; 
    use(b1); 
} 

新增數據成員,以防止編譯器給基類大小的零(它被稱爲空基類優化)。這是GCC選擇的佈局:(打印使用-fdump類層次結構)

Vtable for Base 
Base::_ZTV4Base: 3u entries 
0  (int (*)(...))0 
4  (int (*)(...))(& _ZTI4Base) 
8  Base::bark 

Class Base 
    size=8 align=4 
    base size=8 base align=4 
Base (0xb7b578e8) 0 
    vptr=((& Base::_ZTV4Base) + 8u) 

Vtable for Derived1 
Derived1::_ZTV8Derived1: 3u entries 
0  (int (*)(...))0 
4  (int (*)(...))(& _ZTI8Derived1) 
8  Derived1::bark 

Class Derived1 
    size=12 align=4 
    base size=12 base align=4 
Derived1 (0xb7ad6400) 0 
    vptr=((& Derived1::_ZTV8Derived1) + 8u) 
    Base (0xb7b57ac8) 0 
     primary-for Derived1 (0xb7ad6400) 

Vtable for Derived2 
Derived2::_ZTV8Derived2: 4u entries 
0  (int (*)(...))0 
4  (int (*)(...))(& _ZTI8Derived2) 
8  Base::bark 
12 Derived2::smile 

Class Derived2 
    size=12 align=4 
    base size=12 base align=4 
Derived2 (0xb7ad64c0) 0 
    vptr=((& Derived2::_ZTV8Derived2) + 8u) 
    Base (0xb7b57c30) 0 
     primary-for Derived2 (0xb7ad64c0) 

正如你看到的每個類都有一個虛函數表。前兩項是特殊的。第二個指向類的RTTI數據。第一個 - 我知道但忘了。它在一些更復雜的情況下有用處。那麼,如佈局所示,如果您有一個類Derived1的對象,那麼vptr(v-table-pointer)將指向類Derived1的v表,當然,它只有一個入口,它的函數bark指向Derived1的版本。 Derived2的vptr指向Derived2的vtable,它有兩個條目。另一個是它添加的新方法,微笑。它重複Base :: bark的入口,當然它會指向Base的版本,因爲它是它的最衍生版本。

我已經在使用-fdump-tree優化完成一些優化(構造函數內聯,...)後拋棄了由GCC生成的樹。輸出使用GCC的中端語言GIMPL它是獨立的前端,縮進到一些類似C塊結構:

;; Function virtual void Base::bark() (_ZN4Base4barkEv) 
virtual void Base::bark() (this) 
{ 
<bb 2>: 
    return; 
} 

;; Function virtual void Derived1::bark() (_ZN8Derived14barkEv) 
virtual void Derived1::bark() (this) 
{ 
<bb 2>: 
    return; 
} 

;; Function virtual void Derived2::smile() (_ZN8Derived25smileEv) 
virtual void Derived2::smile() (this) 
{ 
<bb 2>: 
    return; 
} 

;; Function int main() (main) 
int main()() 
{ 
    void * D.1757; 
    struct Derived2 * D.1734; 
    void * D.1756; 
    struct Derived1 * D.1693; 

<bb 2>: 
    D.1756 = operator new (12); 
    D.1693 = (struct Derived1 *) D.1756; 
    D.1693->D.1671._vptr.Base = &_ZTV8Derived1[2]; 
    use (&D.1693->D.1671); 
    D.1757 = operator new (12); 
    D.1734 = (struct Derived2 *) D.1757; 
    D.1734->D.1682._vptr.Base = &_ZTV8Derived2[2]; 
    use (&D.1734->D.1682); 
    return 0;  
} 

我們可以看到很好的,它只是設置一個指針 - vptr的 - 這將指向創建對象之前我們已經看到的適當的vtable。我還在創建Derived1和調用以使用($ 4是第一個參數寄存器,$ 2是返回值寄存器,$ 0總是爲0寄存器)之後,將c++filt工具中的名稱解壓縮後彙編代碼: )

 # 1st arg: 12byte 
    add  $4, $0, 12 
     # allocate 12byte 
    jal  operator new(unsigned long)  
     # get ptr to first function in the vtable of Derived1 
    add  $3, $0, vtable for Derived1+8 
     # store that pointer at offset 0x0 of the object (vptr) 
    stw  $3, $2, 0 
     # 1st arg is the address of the object 
    add  $4, $0, $2 
    jal  use(Base*) 

如果我們要調用bark會發生什麼:?

void doit(Base* b) { 
    b->bark(); 
} 

GIMPL代碼:

;; Function void doit(Base*) (_Z4doitP4Base) 
void doit(Base*) (b) 
{ 
<bb 2>: 
    OBJ_TYPE_REF(*b->_vptr.Base;b->0) (b) [tail call]; 
    return; 
} 

OBJ_TYPE_REF是GIMP大號構建體,它是相當印製成(它的記錄中gcc/tree.def在GCC SVN源代碼)

OBJ_TYPE_REF(<first arg>; <second arg> -> <third arg>) 

它意味着:使用對象b上表達*b->_vptr.Base,並存儲前端(C++)特定值0(它是vtable的索引)。最後,它通過b作爲「這個」的論點。我們會調用一個函數出現在vtable的第二個索引處(注意,我們不知道哪個類型的vtable!再次

OBJ_TYPE_REF(*(b->_vptr.Base + 4);b->1) (b) [tail call]; 

當然,這裏的彙編代碼(堆棧幀的東西切斷):),該GIMPL是這樣

# load vptr into register $2 
    # (remember $4 is the address of the object, 
    # doit's first arg) 
ldw  $2, $4, 0 
    # load whatever is stored there into register $2 
ldw  $2, $2, 0 
    # jump to that address. note that "this" is passed by $4 
jalr $2 

記住,正是在第一功能的vptr點。 (在該條目之前,RTTI槽被存儲)。所以,無論什麼時候出現在這個位置被稱爲。它也將該呼叫標記爲尾呼,因爲它發生在我們的doit函數中的最後一個聲明中。

1

要回答關於哪些對象(從現在開始的實例)有vtable和哪裏的問題,考慮什麼時候需要一個vtable指針會有幫助。

對於任何繼承層次結構,您需要爲該層次結構中的特定類定義的每組虛函數創建一個虛表。換句話說,考慮到以下幾點:

class A { virtual void f(); int a; }; 
class B: public A { virtual void f(); virtual void g(); int b; }; 
class C: public B { virtual void f(); virtual void g(); virtual void h(); int c; }; 
class D: public A { virtual void f(); int d; }; 
class E: public B { virtual void f(); int e; }; 

因此,您需5個虛函數表:A,B,C,d,和E都需要自己的虛函數表。

接下來,您需要知道使用給定的指針或引用特定類的vtable。例如,給定一個指向A的指針,你需要充分了解A的佈局,這樣你就可以得到一個vtable,告訴你在哪裏分派A :: f()。給定一個指向B的指針,你需要充分了解B的佈局來調度B :: f()和B :: g()。等等等等。

一個可能的實現可以將一個vtable指針作爲任何類的第一個成員。這將意味着A的實例的佈局將是:

A's vtable; 
int a; 

和B的一個實例是:

A's vtable; 
int a; 
B's vtable; 
int b; 

而且你可以生成從這個佈局正確的虛擬調度代碼。

您還可以通過組合具有相同佈局或者其中一個是另一個子集的vtable的vtable指針來優化佈局。因此,在上面的示例中,您還可以將B佈局爲:

B's vtable; 
int a; 
int b; 

因爲B的vtable是A的超集。 B的vtable具有A :: f和B :: g的條目,而A的vtable具有A :: f的條目。

爲了完整,這是你將如何佈局到目前爲止,我們已經看到了所有虛函數表:

A's vtable: A::f 
B's vtable: A::f, B::g 
C's vtable: A::f, B::g, C::h 
D's vtable: A::f 
E's vtable: A::f, B::g 

與實際條目是:

A's vtable: A::f 
B's vtable: B::f, B::g 
C's vtable: C::f, C::g, C::h 
D's vtable: D::f 
E's vtable: E::f, B::g 

多重繼承,你做同樣的分析:

class A { virtual void f(); int a; }; 
class B { virtual void g(); int b; }; 
class C: public A, public B { virtual void f(); virtual void g(); int c; }; 

,所得的佈局將是:

A: 
A's vtable; 
int a; 

B: 
B's vtable; 
int b; 

C: 
C's A vtable; 
int a; 
C's B vtable; 
int b; 
int c; 

您需要一個指向兼容A的vtable的指針和一個指向與B兼容的vtable的指針,因爲對C的引用可以轉換爲A或B的引用,您需要將虛擬函數分派給C.

由此可以看出,特定類所具有的vtable指針的數量至少是它從中派生的根類(直接或由於超類)的數量。一個根類是一個類,它有一個不能從一個也有一個vtable的類繼承的vtable。

虛擬繼承向混合中拋出另一個間接位,但可以使用相同的度量來確定vtable指針的數量。

相關問題