2012-04-24 29 views
1

(C++,MinGW的4.4.0,Windows操作系統)C++:虛函數如何解決「this」指針作用域問題?

所有這一切都在評論代碼,除了標籤< 1>和< 2>,是我的猜測。請糾正我,如果你認爲我錯了地方:

class A { 
public: 
    virtual void disp(); //not necessary to define as placeholder in vtable entry will be 
         //overwritten when derived class's vtable entry is prepared after 
         //invoking Base ctor (unless we do new A instead of new B in main() below) 
}; 

class B :public A { 
public: 
    B() : x(100) {} 
    void disp() {std::printf("%d",x);} 
    int x; 
}; 

int main() { 
    A* aptr=new B;    //memory model and vtable of B (say vtbl_B) is assigned to aptr 
    aptr->disp();    //<1> no error 
    std::printf("%d",aptr->x); //<2> error -> A knows nothing about x 
} 

< 2>是一個錯誤,是顯而易見的。爲什麼< 1>不是錯誤?我認爲這種調用的發生是:aptr->disp(); --> (*aptr->*(vtbl_B + offset to disp))(aptr)aptr中的參數是指向成員函數的隱式this指針。在disp()裏面我們會有std::printf("%d",x); --> std::printf("%d",aptr->x); SAME AS std::printf("%d",this->x);那麼爲什麼< 1>沒有錯誤,而< 2>呢?

(我知道虛函數表是實現具體的東西,但我仍然認爲這是值得一問的問題)

回答

3

的規則是:

在C++中動態分配僅適用於成員函數的功能不爲成員變量。

對於成員變量的編譯器只looksup用於該特定類或它的基類的符號名。

在情況1中,將被稱爲合適的方法是通過提取vpt取出適當的方法的地址,然後調用此時,相應的部件的功能決定。
因此,在靜態綁定的情況下,動態分派本質上是fetch-fetch-call而不是正常的call

在情況2:編譯器只在this範圍內尋找x顯然,它找不到它並報告錯誤。

+0

確定但是'這個'被切成只知道'A'爲<1>。在成員函數內部,我認爲這些調用會解析爲'this-> whatEver'。令我困惑的是,由於「this」隱含地傳遞,如圖所示與B對象不同,該機制如何工作? – ustulation 2012-04-24 06:58:16

+0

@ustulation:不,這個''是'B'類型,方法'B :: disp()'是被調用的方法,當然它有權訪問它自己的成員'x'。這裏沒有切片。 – 2012-04-24 07:02:39

+1

真正的規則是在C++中,動態分派僅用於虛擬members_。你的規則是由此而來的,並且這個語言不允許將變量定義爲虛擬。 – 2012-04-24 07:24:25

0
virtual void disp(); //not necessary to define as placeholder in vtable entry will be 
        //overwritten when derived class's vtable entry is prepared after 
        //invoking Base ctor (unless we do new A instead of new B in main() below) 

您的評論是不正確的。一個虛函數是odr-used,除非它是純的(反過來不一定成立),這意味着你必須爲它提供一個定義。如果你不想爲它提供一個定義,你必須使它成爲一個純虛函數。

如果你進行這些修改,然後aptr->disp();的作品之一,並調用派生類disp()因爲在派生類中disp()覆蓋基類的功能。當你通過指向base的指針調用它時,基類函數仍然存在。 x不是基類的成員,因此aptr->x不是有效的表達式。

+0

'這意味着你必須爲它提供一個定義。如果你不想爲它提供一個定義,你必須使它成爲一個純粹的虛函數。「請嘗試一下。它的工作原理沒有定義A :: disp() – ustulation 2012-04-24 07:01:19

+0

@ustulation:僅僅因爲它適合你,並不一定意味着你的代碼是正確的。看來工作是具有_undefined behavior_的代碼的可能行爲之一。 – 2012-04-24 07:05:01

+0

@ustulation正如Charles Bailey所說,這是未定義的行爲。它是否有效取決於編譯器,可能取決於基類的構造函數和析構函數的作用,以及它們是否可見。 – 2012-04-24 07:29:56

1

this與內部的aptr不相同。 B::disp執行需要thisB*,就像B的任何其他方法一樣。當您通過A*指針調用虛擬方法時,它將首先轉換爲B*(甚至可能更改其值,因此在調用期間它不一定等於aptr)。

I.e.到底發生了什麼是一樣的東西

typedef void (A::*disp_fn_t)(); 
disp_fn_t methodPtr = aptr->vtable[index_of_disp]; // methodPtr == &B::disp 

B* b = static_cast<B*>(aptr); 
(b->*methodPtr)(); // same as b->disp() 

對於更復雜的例子,看看這個帖子http://blogs.msdn.com/b/oldnewthing/archive/2004/02/06/68695.aspx。在這裏,如果有多個A基地可能會調用相同的B::disp,MSVC會生成不同的入口點,每個入口點將不同的偏移量移動A*指針。當然,這是特定於實現的;其他編譯器可能會選擇在vtable中某處存儲偏移量。

+0

這個答案完全消除了我對這個問題的疑惑(把A *轉換成B *,沒有這個答案,我仍然沒有看到,因爲缺乏我的知識,也許''this-> x'在void B: :disp(B * const this){}'當傳遞A *作爲'this')..但什麼是不明白的是編譯器不知道在編譯時如果'methodPtr ==&B :: disp'或'methodPtr ==&A :: disp'..we可以有'void f(A * aptr){aptr-> disp();}'和'if(something)f(&bObj); else f(&aObj);'對於後者沒有(我有一種感覺,我搞砸了:))...... – ustulation 2012-04-25 08:04:23

+0

A和B的進一步記憶模型是一致的,事情似乎更容易在這裏解釋。如果A是第二個基類, B.指針對齊會破壞,但是B :: disp()仍然可以訪問最左邊的Base類的非私有成員。我認爲'this'靜態地解決了在遇到'new B'時,找到/指向成員變量ed和'this-> aMemberVar'在運行時不會被觸及。 – ustulation 2012-04-25 08:12:00

+0

@ustulation檢查我更新的答案。例如,MSVC通過指向base的每個可能的方式調用'B :: disp',每次適當地轉換this'然後跳轉到'B :: disp'本身,從而生成「蹦牀」。 – hamstergene 2012-04-25 08:37:02

1

你很困惑,而且我覺得你來自更加動態的語言。

在C++中,編譯和運行時間顯然是孤立的。一個程序必須首先被編譯,然後才能運行(並且任何這些步驟都可能失敗)。


所以,去落後:

<2>失敗在編譯,因爲編譯約靜態信息。 aptr的類型爲A*,因此可通過此指針訪問A的所有方法和屬性。既然您宣稱disp()但沒有x,那麼致電disp()編譯,但沒有x

因此,<2>的失敗是關於語義的,這些都是在C++標準中定義的。


前往<1>,它的工作原理,因爲是在disp()一個A聲明。這保證了函數的存在(我會說你實際上躺在這裏,因爲你沒有定義它在A)。

運行時發生的事情由C++標準語義定義,但標準沒有提供實施指導。大多數(如果不是全部的話)C++編譯器將使用每個類的虛擬表+每個實例策略的虛擬指針,並且在這種情況下,描述看起來是正確的。

但是,這是純粹的運行時實現,它運行的事實不會追溯地影響程序編譯的事實。