回答
所有虛擬類通常都有一個vtable,但它不是C++標準所要求的,存儲方法依賴於編譯器。
Vtable是每個類實例,即如果我有一個具有虛擬方法的類的10個對象,那麼在所有10個對象中只有一個共享一個vtable。
本例中的所有10個對象都指向相同的vtable。
Vptr怎麼樣,每個對象會有10個vptr關聯還是像單個vtable那樣只有一個vptr? – Rndp13 2015-06-02 02:23:24
多態類型的每個對象都會有一個指向Vtable的指針。
存儲的VTable依賴於編譯器。
所有具有虛擬方法的類都將有一個由該類的所有對象共享的vtable。
每個對象實例都有一個指向該vtable的指針(這是vtable的發現方式),通常稱爲vptr。編譯器隱式生成代碼來初始化構造函數中的vptr。
請注意,這些都不是C++語言所要求的 - 如果需要,實現可以用其他方式處理虛擬調度。但是,這是我熟悉的每個編譯器所使用的實現。 Stan Lippman的書「C++對象模型內部」描述了它如何很好地工作。
+1你能解釋爲什麼虛擬指針是每個對象,而不是每個類?謝謝。 – Viet 2013-03-11 07:11:57
@Viet您可以將vPtr視爲對象的運行時定義的引導。只有在vPtr被設置後,對象才能知道它的實際類型是什麼。在這個概念中,爲每個類創建一個vPtr(靜態)是沒有意義的。 想想另一種方式,如果一個對象不需要vPtr,那麼它在編譯時必須已經知道它的運行時定義,這與它是一個動態解析的對象相矛盾。 – 2014-03-12 06:57:36
不一定
幾乎每一個具有虛擬功能將有一個v表指針對象。對於每個具有對象派生的虛擬函數的類,不需要有v表指針。
儘管分析代碼的新編譯器可能能夠在某些情況下消除v表。例如,在一個簡單的情況下:如果只有一個抽象基類的具體實現,編譯器知道它可以將虛擬調用更改爲常規函數調用,因爲每當調用該虛函數時,它將始終解決完全相同的功能。另外,如果只有幾個不同的具體函數,編譯器可以有效地改變調用站點,以便它使用'if'來選擇正確的具體函數來調用。
因此,在這種情況下,不需要v表,並且對象可能最終沒有。
嗯。我剛剛試圖找到一個可以消除v-表指針的編譯器。看起來不像目前有任何。但是,編譯器和鏈接器之間的信息共享變得越來越高,以至於它們正在融合在一起。隨着持續發展,這可能會發生。 – 2009-02-18 18:03:36
在家裏試試這個:
#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";
}
V表是一個實現細節沒有什麼語言定義,指出它的存在。事實上,我已閱讀了有關實現虛擬功能的其他方法。
但是:所有常見的編譯器(即我所知道的)使用VTabels。
然後是的。任何具有虛擬方法或從具有虛擬方法的類(直接或間接)派生的類將具有帶有指向VTable的指針的對象。
你問的所有其他問題將取決於編譯器/硬件,這些問題沒有真正的答案。
就像別人說的那樣,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
函數中的最後一個聲明中。
要回答關於哪些對象(從現在開始的實例)有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指針的數量。
- 1. 從Objdump實用程序檢索vptr(指向虛擬表指針VTABLE的指針)?
- 2. 純虛擬對象是否有指向vtbl的指針?
- 3. 設置一個指向一個對象的指針爲零會影響對象或其他指針嗎?
- 4. 創建一個指向抽象對象的指針向量
- 5. 一個指向二維數組對象類型指針的指針
- 6. 傳遞一個臨時指針指向一個對象的溫度指針
- 7. 指向虛擬類
- 8. 指向一個對象和一個指針在C(如何指向另一個指針)之間的區別
- 9. 一個類內的指針可以指向這個嗎?
- 10. 指向對象類型的指針
- 11. C++指向一個類的指針
- 12. 爲什麼所有虛擬主機都指向第一個虛擬主機?
- 13. 爲第一個指針指向的對象分配第二個指針
- 14. 是否有可能有一個指針指向第一個指針所指向的變量的指針?
- 15. 指向現有對象的指針?
- 16. 找到並返回一個指向一個向量中的對象的指針
- 17. 返回一個指向一個對象數組的指針,並將其指定給一個指針
- 18. 通過指向const的指針調用虛擬函數基類
- 19. 向量的對象指針和虛擬方法
- 20. 指向同一類對象的指針的類
- 21. 指向const對象的指針自動轉換爲指向對象的指針
- 22. 指向一個對象的成員函數的指針
- 23. 指向結構或類的指針與指向第一個字段的指針
- 24. 指向類中對象的指針:push_back和指針衝突
- 25. 指針幫助,指向對象的指針和類
- 26. 傳遞對象指針作爲指向基類的指針
- 27. 如何檢查一個指針是否指向一個虛擬基類而不是C++中的子類?
- 28. 設置指向對象的指針,讓對象指向對方
- 29. Cocos2d:可以有指向父級類指針的指針嗎?
- 30. UML:當一個類具有指向其他類的指針參數指針時
重複? 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
有沒有這樣的事情,作爲一個「虛擬課堂」 C++。 – curiousguy 2016-04-22 03:14:58