2017-08-25 119 views
5

有人可以解釋這個不同類的虛擬表是如何存儲在內存中的?當我們使用指針調用函數時,他們如何使用地址位置調用函數?我們可以使用類指針獲取這些虛擬表內存分配大小嗎?我想查看一個虛擬表爲一個類使用多少內存塊。我怎麼能看到它?虛擬表和_vptr存儲方案

class Base 
{ 
public: 
    FunctionPointer *__vptr; 
    virtual void function1() {}; 
    virtual void function2() {}; 
}; 

class D1: public Base 
{ 
public: 
    virtual void function1() {}; 
}; 

class D2: public Base 
{ 
public: 
    virtual void function2() {}; 
}; 
int main() 
{ 
    D1 d1; 
    Base *dPtr = &d1; 
    dPtr->function1(); 
} 

謝謝!提前

+0

[Wikipedia](https://en.wikipedia.org/wiki/Virtual_method_table) – Barmar

+4

這個vtable是依賴於實現的,沒有標準的方法來訪問它。 – Barmar

+0

因此,@Barmar有什麼方法可以訪問vtable或查看使用了多少內存vtable? –

回答

5

要記住的第一點是免責聲明:這些標準實際上都沒有保證。該標準說明了代碼需要的外觀以及它應該如何工作,但實際上並沒有明確說明編譯器需要如何實現這一點。

也就是說,基本上所有的C++編譯器在這方面的工作非常相似。

因此,讓我們從非虛函數開始。它們分爲兩類:靜態和非靜態。

兩者中較簡單的是靜態成員函數。一個靜態成員函數幾乎就像一個全局函數,它是該類的一個friend,除了它還需要類的名稱作爲函數名稱的前綴。

非靜態成員函數稍微複雜一些。它們仍然是直接調用的正常函數 - 但它們傳遞了一個隱藏的指針,指向它們被調用的對象的實例。在函數內部,您可以使用關鍵字this來引用該實例數據。所以,當你打電話給a.func(b);時,生成的代碼與你得到的代碼非常相似func(a, b);

現在讓我們考慮一下虛擬功能。這裏是我們進入vtable和vtable指針的地方。我們有足夠的間接性,可能最好繪製一些圖表來看看它是如何佈置的。這幾乎是最簡單的例子:有兩個虛函數一類的一個實例:

enter image description here

所以,對象包含其數據和一個指向虛函數表。 vtable包含一個指向由該類定義的每個虛函數的指針。但是,它可能並不是顯而易見的,爲什麼我們需要這麼多的間接性。要理解的是,讓我們看看接下來的(非常輕微)更復雜的情況:這個類的兩個實例:

enter image description here

注意類的每個實例如何有自己的數據,但他們都共享相同的vtable和相同的代碼 - 如果我們有更多的實例,他們仍然會在同一個類的所有實例中共享一個vtable。

現在,我們來考慮派生/繼承。舉個例子,讓我們將現有的類重命名爲「Base」,並添加一個派生類。由於我感覺想象力豐富,我將其命名爲「派生」。如上所述,基類定義了兩個虛函數。派生類覆蓋的那些中的一個(而不是其他):

enter image description here

當然,我們可以將二者結合起來,使每個基體的多個實例和/或派生類:

enter image description here

現在我們來深入瞭解一下更詳細的內容。關於派生的有趣之處在於,我們可以將派生類的對象的指針/引用傳遞給爲接收基類的指針/引用而編寫的函數,它仍然有效 - 但如果調用虛函數,你會得到實際類的版本,而不是基類。那麼,這是如何工作的?我們如何將派生類的實例視爲它是基類的一個實例,並且仍然有效?爲此,每個派生對象都有一個「基類子對象」。例如,讓我們考慮這樣的代碼:

struct simple_base { 
    int a; 
}; 

struct simple_derived : public simple_base { 
    int b; 
}; 

在這種情況下,當您創建的simple_derived一個實例,你就會得到包含兩個int秒的對象:aba(基類部分)位於內存中對象的開始處,並且b(派生類部分)緊跟其後。因此,如果將對象的地址傳遞給期望基類實例的函數,則它將使用基類中存在的部分,編譯器將相同的偏移量放置在對象中, d在基類的一個對象中,所以函數可以在不知道它處理派生類的對象的情況下操縱它們。同樣,如果你調用一個虛擬函數,所有它需要知道的是vtable指針的位置。就它而言,類似Base::func1的東西基本上只意味着它遵循vtable指針,然後在指定的偏移量處使用指向函數的指針(例如,第四個函數指針)。

至少現在,我將忽略多重繼承。它增加了相當複雜的圖片(特別是當涉及到虛擬繼承時),你根本沒有提到它,所以我懷疑你真的很在乎。

至於訪問任何的這種,或者使用比簡單地調用虛函數以外的任何方式:你可以拿出一些特定的編譯器 - 但不要指望它是便攜式的。雖然像調試器這樣的東西經常需要看這樣的東西,但涉及的代碼往往非常脆弱並且特定於編譯器。

+0

我認爲你的圖片與Derived :: vtable是誤導性的,因爲它看起來像你需要查看func2的'Base :: vtable',但是(如你的文本所描述的)'func1'和'func2 '必須內嵌存儲在'Derived :: vtable'中。此外,圖片可能表明'Derived :: vtable'中的'func1' /'func2'仍然可以指向'A :: *'(如果Derived沒有覆蓋它)。 – Stefan

-1

每個類都有一個指向函數列表的指針,它們每個都與派生類的順序相同,然後被覆蓋的特定函數在列表中的該位置發生變化。

當您指向基本指針類型時,指向該對象的對象仍然具有正確的_vptr。

基地的

Base::function1() 
Base::function2() 

D1的

D1::function1() 
Base::function2() 

D2的

Base::function1() 
D2::function2() 

此外衍生DROM D1或D2將只是添加在2電流下面的列表中的新的虛擬功能。

當調用虛函數,我們只需要調用相應的索引,功能1將是指數0

所以您的通話

dPtr->function1(); 

實際上是

dPtr->_vptr[0](); 
+0

你能指出標準中指定這一點的部分嗎? –

4

虛擬表應該在一個類的實例之間共享。更確切地說,它生活在「階級」層面,而不是實例層面。每個實例都具有實際具有指向虛擬表的指針的開銷,如果其層次結構中存在虛函數和類。

表格本身至少是容納每個虛函數指針所需的大小。除此之外,它是一個實現細節,它是如何定義的。查詢here以獲取更多關於此問題的SO問題。

1

答案傑裏·科芬了優異的解釋虛函數指針的工作如何實現運行時多態性在C++中。但是,我認爲它缺乏回答存儲虛擬表的內存的地方。正如其他人指出的那樣,這不是由標準決定的。

然而,由馬丁Kysel是進入非常詳細哪裏虛表存儲一個極好的blog post(s)。總結博客文章:

  1. 一個虛擬表是創建爲每個類(不是實例)與虛擬功能。
  2. 每個虛函數表存儲在此類指向同一V表中存儲的每個實例只讀
  3. 爲vtable中的每個函數的拆卸被存儲在所得到的ELF二進制文件的文本部分中的產生的二進制文件的存儲器
  4. 試圖寫在虛函數表,位於只讀存儲器,導致分段故障(如預期)
3

首先,下面的答案包含幾乎所有的東西你想關於虛擬表就知道: https://stackoverflow.com/a/16097013/8908931

如果你正在尋找的東西多一點具體的(與常規的免責聲明,這可能的平臺,編譯器和CPU架構之間變化):

  1. 當需要時,正在爲一個類創建一個虛擬表。該類將只有一個虛擬表的實例,並且該類的每個對象都有一個指向該虛擬表的內存位置的指針。虛擬表本身可以被認爲是一個簡單的指針數組。
  2. 當您將派生指針分配給基指針時,它還包含指向虛表的指針。這意味着基指針指向派生類的虛表。編譯器會將此調用指向虛擬表中的偏移量,該虛擬表將包含派生類中函數的實際地址。
  3. 不是。通常在對象開始時,有一個指向虛擬表本身的指針。但這不會對你有太大的幫助,因爲它只是一系列指針,並沒有真正指示它的大小。
  4. 製作一個很長的短的答案:對於一個確切大小,你可以找到在可執行該信息(或者從它加載到內存段)。有了足夠的關於虛擬表的工作原理的知識,只要知道代碼,編譯器和目標架構,就可以得到相當準確的估計。

    對於確切的大小,就可以找到此信息在任一可執行文件,或在正被從可執行加載在內存段。可執行文件通常是一個ELF文件,這種文件包含運行程序所需的信息。這些信息的一部分是各種語言結構的符號,如變量,函數和虛擬表。對於每個符號,它都包含它在內存中佔用的大小。所以按鈕行,您將需要虛擬表的符號名稱和ELF中的足夠知識,以便提取您想要的內容。