要記住的第一點是免責聲明:這些標準實際上都沒有保證。該標準說明了代碼需要的外觀以及它應該如何工作,但實際上並沒有明確說明編譯器需要如何實現這一點。
也就是說,基本上所有的C++編譯器在這方面的工作非常相似。
因此,讓我們從非虛函數開始。它們分爲兩類:靜態和非靜態。
兩者中較簡單的是靜態成員函數。一個靜態成員函數幾乎就像一個全局函數,它是該類的一個friend
,除了它還需要類的名稱作爲函數名稱的前綴。
非靜態成員函數稍微複雜一些。它們仍然是直接調用的正常函數 - 但它們傳遞了一個隱藏的指針,指向它們被調用的對象的實例。在函數內部,您可以使用關鍵字this
來引用該實例數據。所以,當你打電話給a.func(b);
時,生成的代碼與你得到的代碼非常相似func(a, b);
現在讓我們考慮一下虛擬功能。這裏是我們進入vtable和vtable指針的地方。我們有足夠的間接性,可能最好繪製一些圖表來看看它是如何佈置的。這幾乎是最簡單的例子:有兩個虛函數一類的一個實例:
所以,對象包含其數據和一個指向虛函數表。 vtable包含一個指向由該類定義的每個虛函數的指針。但是,它可能並不是顯而易見的,爲什麼我們需要這麼多的間接性。要理解的是,讓我們看看接下來的(非常輕微)更復雜的情況:這個類的兩個實例:
注意類的每個實例如何有自己的數據,但他們都共享相同的vtable和相同的代碼 - 如果我們有更多的實例,他們仍然會在同一個類的所有實例中共享一個vtable。
現在,我們來考慮派生/繼承。舉個例子,讓我們將現有的類重命名爲「Base」,並添加一個派生類。由於我感覺想象力豐富,我將其命名爲「派生」。如上所述,基類定義了兩個虛函數。派生類覆蓋的那些中的一個(而不是其他):
當然,我們可以將二者結合起來,使每個基體的多個實例和/或派生類:
現在我們來深入瞭解一下更詳細的內容。關於派生的有趣之處在於,我們可以將派生類的對象的指針/引用傳遞給爲接收基類的指針/引用而編寫的函數,它仍然有效 - 但如果調用虛函數,你會得到實際類的版本,而不是基類。那麼,這是如何工作的?我們如何將派生類的實例視爲它是基類的一個實例,並且仍然有效?爲此,每個派生對象都有一個「基類子對象」。例如,讓我們考慮這樣的代碼:
struct simple_base {
int a;
};
struct simple_derived : public simple_base {
int b;
};
在這種情況下,當您創建的simple_derived
一個實例,你就會得到包含兩個int
秒的對象:a
和b
。 a
(基類部分)位於內存中對象的開始處,並且b
(派生類部分)緊跟其後。因此,如果將對象的地址傳遞給期望基類實例的函數,則它將使用基類中存在的部分,編譯器將相同的偏移量放置在對象中, d在基類的一個對象中,所以函數可以在不知道它處理派生類的對象的情況下操縱它們。同樣,如果你調用一個虛擬函數,所有它需要知道的是vtable指針的位置。就它而言,類似Base::func1
的東西基本上只意味着它遵循vtable指針,然後在指定的偏移量處使用指向函數的指針(例如,第四個函數指針)。
至少現在,我將忽略多重繼承。它增加了相當複雜的圖片(特別是當涉及到虛擬繼承時),你根本沒有提到它,所以我懷疑你真的很在乎。
至於訪問任何的這種,或者使用比簡單地調用虛函數以外的任何方式:你可以拿出一些特定的編譯器 - 但不要指望它是便攜式的。雖然像調試器這樣的東西經常需要看這樣的東西,但涉及的代碼往往非常脆弱並且特定於編譯器。
[Wikipedia](https://en.wikipedia.org/wiki/Virtual_method_table) – Barmar
這個vtable是依賴於實現的,沒有標準的方法來訪問它。 – Barmar
因此,@Barmar有什麼方法可以訪問vtable或查看使用了多少內存vtable? –