2016-03-17 192 views
1

我正在學習C++繼承,因此我通過動態創建基類來嘗試此代碼,並對其派生類進行了向下轉換(顯然,它對於向下轉換無效)以便使這個動態創建的Base對象由Derived類指針指向。但是當我通過這個Derived指針調用方法who()時,它會調用Derived類方法而不是Base類方法。C++繼承:派生類指向基類調用Derived類方法

根據我的假設,沒有爲Derived類創建對象,那麼如何調用非創建的Derived類對象的方法而不是實際創建的Base類對象?

我爲這種現象搜索了一下,但是找不到一個清晰脆弱的解釋來調用非創建的Derived類對象的方法。如果它是根據標準,然後解釋我是如何工作的。 我知道,如果我製作方法virtual,但這裏使用的方法不是virtual,故事會有所不同。

class parent{ 
    public: 
     void who(){ 
      cout << "I am parent"; 
     } 
}; 

class child : public parent{ 
    public: 
     void who(){ 
      cout << "I am child"; 
     } 
}; 

int main(){ 
    child *c = (child *)new parent(); 
    c->who(); 
    return 0; 
} 

輸出是I am child但我預計I am parent

編輯:: 我沒有在上面的代碼中解脫出來了內存並提出了無效沮喪的,因爲我只是想看看會發生什麼。所以只解釋調用方法的這種行爲。

+2

行爲未定義。任何事情都可能發生。即使你讓這個函數變成虛擬的,它也是一樣的未定義的故事。 – molbdnilo

+0

@molbdnilo:我不認爲它在這個特殊情況下是未定義的。看到我非常好的答案。 –

+2

@ P45Imminent - 行爲未定義。事實上,你可以對觀察到的行爲進行理性解釋並不會改變這一點。 「未定義的行爲」意味着語言定義不會告訴您發生了什麼;而已。 –

回答

0

由於@Mark Ransom在評論中曾建議,我看了後When does invoking a member function on a null instance result in undefined behavior?。然後我想出了我自己的解決方案。

調用非創建對象方法的這種行爲與類parent類或child類或向下或繼承無關。我靜靜地告訴通過向下調用child::who(),編譯器調用child::who()而不關心對象類型。

但是如何調用who()方法,因爲沒有爲child類創建對象?

其中我試圖調用沒有任何成員變量,所以沒有必要提領this指針的方法。它只是調用who(),如果child類指針也指向NULL,則同樣的行爲也是如此。

child *c = NULL; 
c->who(); 

它打印I am child即使cNULL。因爲不需要取消指針this。讓我們有一種情況,who()方法包含如下的成員變量x

class child : public parent{ 
    private: 
     int x; 
    public: 
     void who(){ 
      x = 10; 
      cout << "I am child " << x; 
     } 
}; 

child *c = NULL; 
c->who() 

現在,它會導致Segmentation fault因爲爲了使用x編譯器必須取消引用this指針x作爲(*this).x。由於this指向NULL解引用將導致Segmentation fault。這顯然是一個未定義的行爲。

+0

其實它們都是未定義的行爲,只是在一個「未定義」的案例中竟然是無害的。我無法強調你應該總是避免未定義的行爲,即使它看起來有效,因爲它可能會在沒有明顯原因的情況下在將來停止工作。 –

1

多態性不能像那樣工作。有趣的是,你的C風格從parent*轉換爲child*可以工作,因爲這些類沒有v-表或其他功能who。所以who的地址必須與class本身的地址相同。

parent *p = (parent*)new child();

會更有意義,但即使是這樣,如果你在parent類,你沒有做標記功能whovirtualp->who()只會調用子類的功能。

+0

即使它是無意義的,但是如何調用子方法,因爲沒有爲子類創建對象 –

+0

@jblixr你可能想了解v-表以及如何在內存中佈局對象以理解爲什麼會發生這樣的結果。 – CompuChip

+0

@jvlixr因爲sizeof(父級)必須非零(標準堅持),但sizeof(child)+ sizeof(parent)= sizeof(parent)不重要。 –

1

首先,這是無效的downcast。 new parent()的實際類型確實是parent而不是child。只有當真實(也稱爲動態)類型爲child,但指向對象此刻爲parent時,才允許向下轉換。

反過來會更有意義。如果你創建一個child並將它分配給一個parent指針,這可以。但即使如此:除非whovirtual靜態類型而不是動態類型決定調用哪個函數。

例子:

class parent{ 
    public: 
     void who(){ 
      cout << "I am parent"; 
     } 

    ~virtual parent() {}; // important to have that in class hierarchies where you might use a child that is assigned to a parent pointer! 
}; 

class child : public parent{ 
    public: 
     void who(){ 
      cout << "I am child"; 
     } 
}; 

int main(){ 
    parent *c = (parent *)new child(); 
    c->who(); // output would still be parent because who() is not virtual 
    delete c; // important! never forget delete! 
    return 0; 
} 

如果您使用,而另一方面,

virtual void who(); 

然後,輸出將是 「我的孩子」 像你期望的那樣。

+0

但是如何調用子方法?因爲沒有任何對象爲孩子創建任何地方 –

+0

它的工作原理是因爲這兩個類都沒有一個vtable,並且只有一個函數;該函數的地址必須與該類的地址相同。 –

+0

由於您的代碼被盜用,因此無法保證任何內容。但是靜態類型決定在這種情況下,最常見的錯誤代碼。有時甚至有可能寫'child * c = 0x12345678; c-> who();'你會得到輸出,因爲給出了靜態類型(child)。當然,這種代碼絕對不能使用。 – IceFire

2

您的代碼以這種方式行爲的原因可能是因爲編譯器不檢查對象的實際類型(除非您的函數是virtual)否則不需要;它只是叫child::who,因爲你告訴它。也就是說,你的代碼肯定是可疑的。

你是靜態的downcasting指向派生類指針的基類指針,它不是type-safe。 C++不會阻止你這樣做;這取決於你確保你的指針真的指向派生類型的一個對象。如果指針不引用派生類型的對象,則解引用指針可能是undefined behaviour。你很幸運,你的代碼甚至可以打印任何東西。

需要,以確保您的基類功能whovirtual,否則函數調用將不行爲polymorphically。請記住,一旦您將virtual添加到您的代碼中,您肯定會調用未定義的行爲,因爲您非法向下轉換爲無效類型。如果對象不是指定的類型,則可以使用dynamic_cast進行安全降級,如果對象不是指定的類型,則將返回nullptr

在基類中,通常還應該有一個virtual析構函數,以便您的對象可以是多態的delete d。說到這一點,您還需要確保在某個時刻您的動態分配對象delete。通過手動呼叫newdelete,即使您知道需要撥打delete,也很容易泄漏這樣的內存。在C++ 11中將std::unique_ptrstd::shared_ptr添加到標準庫中以解決這些問題。除了最底層的代碼外,請使用這些代替newdelete

總之,這裏是我建議你的代碼應該是:

#include <iostream> 
#include <memory> 

class parent { 
    public: 
     virtual ~parent() {} 

     virtual void who() { 
      std::cout << "I am parent"; 
     } 
}; 

class child : public parent { 
    public: 
     void who() override { 
      std::cout << "I am child"; 
     } 
}; 

int main() { 
    auto p = std::make_unique<parent>(); 
    auto c = dynamic_cast<child*>(p.get()); 

    if (c) // will obviously test false in this case 
    { 
     c->who(); 
    } 
} 
+0

我會按照你的建議,但一個子對象甚至沒有創建,那麼它如何調用子方法? –

+0

@jblixr因爲,正如我所提到的(我編輯過;抱歉,如果你沒有看到),編譯器不需要檢查對象的實際類型。正如你所提到的那樣,添加'virtual'使得它的行爲符合你的期望,但是添加'virtual'肯定會使你的代碼變得非法,因爲正如我之前提到的,你不能通過對派生類型的引用來使用基類型的對象!但是,您可以做相反的處理(通過基地獲取訪問權限)。 –

+0

@jblixr爲了清晰起見,再次編輯。 –

1

你看到的是未定義的行爲在工作。

你的函數是非虛擬的,它們只是指向編譯器指針指向類型的成員函數。

child *c = (child*)new parent; 

這是一個C樣式轉換是強大的武器編譯成的信念,指針Ç絕對點的東西,是一個孩子。

因此,當您撥打c->who()時,您特別呼叫child::who,因爲指針是指向兒童的指針。

沒有可怕的事情發生的原因,你看到「我是孩子」是因爲你不試圖取消引用該指針或使用任何特定於子對象的字段,您的指向實例並不實際有。所以你逃避了。

+0

你是絕對正確的,並學會了自己http://stackoverflow.com/a/36076748/5756174 –

0

很簡單,在該調用c->who()點,靜態型μc(即編譯器知道對於C的類型)是child*並且該方法who是非虛擬的。因此,編譯器發出撥打child::who地址的指令。

編譯器沒有(一般情況下它怎麼可能?)跟蹤在運行時指向c的對象的真實類型。

如果您在child::who曾在child不在parent任何成員,訪問這些成員將產生一個徹頭徹尾的越界訪問,這可能會導致一個SIGSEGV,或其他不愉快。

最後,您觀察到的行爲是否是由標準的保證,我傾向於@ P45Imminent同意:無論parentchild滿足要求POD和child沒有非靜態數據成員。因此,要求每個標準難以區分parentchild對象的運行時間佈局(至少就parentchild而言,可能childparent最後可能會有不同的填充量)。所以從其中一個評論中引用的9.3.1/2的行不適用於恕我直言。如果這種佈局假設不被標準支持,我很樂意聽到更多的人瞭解。

相關問題