2011-12-07 125 views
1

考慮下面的以下示例代碼:派生類對象是否包含基類對象?

#include <iostream> 

using namespace std; 

class base 
{ 
    public: 
     base() 
     { 
     cout << "ctor in base class\n"; 
     } 
}; 

class derived1 : public base 
{ 
    public: 
     derived1() 
     { 
     cout <<"ctor in derived class\n"; 
     } 
}; 

int main() 
{ 
    derived1 d1obj; 
    return 0; 
} 

問題

  1. 當創建d1obj,該構造函數中推導的順序調用:基類構造函數被稱爲第一,然後將派生類構造函數。這樣做是因爲以下原因:In-order to construct the derived class object the base class object needs to be constructed first

  2. d1obj是否包含基類對象?

我加入一個問題

3)當創建d1obj,控制第一達到基類的構造,然後將其轉移到派生類的構造?或者它是相反的:它首先到達派生類的構造函數,發現它有基類,因此控制權轉到基類的構造函數中?

+4

任何基本的C++常見問題解答或書籍都將回答這些問題 –

+1

問問題內容問題而不是決策問題可能更有用。 –

+0

你應該習慣在派生類中顯式調用父類的ctor。假設基地沒有提供默認的Ctor,那麼這個代碼就不會編譯。但是如果您在派生類的初始化程序列表中調用非默認ctor,它就會失敗。 – danca

回答

6

1)是的,首先構造鹼基,然後構造非靜態數據成員,然後調用派生類的構造函數。原因是這個類的構造函數中的代碼可以看到並使用完全構造的基礎。

2)是的。您可以完全按照字面意思進行操作:在分配給派生類對象的內存中,有一個稱爲「基類子對象」的區域。派生類對象「包含」基類子對象的方式與包含任何非靜態數據成員的成員子對象的方式完全相同。實際上,問題中給出的例子恰好是一個特例:「空基類優化」。儘管類型base類型的完整對象永遠不會是大小零,但此基類子對象的大小爲零。

雖然這個遏制是一個低級的事情。其他人認爲概念上基礎與成員不同,並且語言的語法和語義對待它們的方式不同,即使子對象本身都只是類的佈局的一部分。

3)這是一個實現細節。基類構造函數體中的代碼在派生類構造函數的主體中的代碼之前執行,實際上派生類構造函數然後在不可見的編譯器生成的try/catch塊中執行,以確保如果它拋出,基類被破壞。但是,編譯器如何根據發出的代碼中的函數入口點實際做到這一點來實現這一點。

當一個類具有虛擬基地時,構造函數通常會導致發出兩個不同的函數體 - 一個用於此類是派生類型最多的類型,另一個用於當此類本身是基類時使用。原因是虛擬基類是由派生最多的類構造的,以確保它們共享時只構造一次。因此,構造函數的第一個版本將調用所有基類構造函數,而第二個版本將只調用非虛擬基類的構造函數。

編譯器總是「知道」類的基礎是什麼,因爲你只能構造一個完整類型的對象,這意味着編譯器可以看到類定義,並且指定了基類。所以當進入構造函數時,沒有任何問題只有「發現它具有基類」 - 編譯器知道它具有基類,並且如果對基類構造函數的調用位於派生類構造函數代碼中這只是爲了方便編譯器。它可能在構建對象的每個位置發出對基類構造函數的調用,並且在派生類構造函數可以被內聯的情況下,這是最終結果。

+0

+1,on 3),而被髮射的確切符號是一個實現細節,編譯器必須從最大派生類型開始構造,因爲這是生成的代碼片段,它具有傳遞給基礎構造函數的正確參數,所以執行必須從那裏開始,跳轉到基類,然後繼續向下。 –

+0

@大衛:是的,放置它的好方法是,在你到達派生類構造函數的主體之前,初始化列表中可能會有相當多的代碼。這就是爲什麼構造函數沒有名字,而且你也不能接受它們的地址 - 讓實現自由地將它所喜歡的任何代碼放到函數的開頭,在構造函數體之前,在知道它不能(依法)直接調用。 –

1
  1. 是的。想象一下,在派生類的構造函數中,你想使用基類的一些成員。因此,他們需要初始化。因此,首先調用基類構造函數是有意義的。

  2. d1obj是一個基類對象。這就是繼承。在某種程度上,您可以說它包含基類的一個對象。在內存中,對象的第一部分將對應一個基礎對象(在您的示例中,您沒有虛擬功能,如果是這樣,則首先指向vftable,derived1,然後是基類成員),之後即屬於derived1的會員。

+3

不,請從字面上理解。派生對象實際上包含一個基礎子對象。你甚至可以通過靜態轉換指向派生對象的指針來獲取指向它的指針。 –

+0

請謹慎操作,因爲_containment_可能會很容易混淆爲_composition_,這是一個相似但不同的範例。 – Chad

+0

@KerrekSB你是對的,但這取決於你對字面上的解釋。我會找到類似B的東西; A類{B_b}; 「遏制」的字面意思。我可能錯了。 –

1

是的,是的。


由於您的編輯,廣告3)派生類的構造函數調用基類的構造函數的義務它的第一個動作; 然後所有的成員對象構造函數,最後它執行構造函數體。

銷燬的工作原理與此相反:首先析構函數體執行,然後成員對象被銷燬(與其銷燬的順序相反),最後調用基礎子對象析構函數(這就是爲什麼你總是需要一個可訪問的析構函數基類,即使它是純虛擬的)。

+0

+1用於糾正我的答案。這說得通。 –

0

1--是的。這是合乎邏輯的。

derived1類型的對象是base類型的特殊對象,這意味着,作爲第一件事,它們是base類型的對象。這是首先構造的,然後「derived1」將其「特性」添加到對象。

2--它不是一個含有的問題,它是繼承。請參閱上面的段落以更好地理解此答案。

0
  1. 好,概念,不是真的。 d1obj包含base的實例將會響應的所有數據成員,並響應所有實例將會但不包含base實例的成員函數:例如,您不能說d1obj.base.func()。例如,如果你已經重載了一個父方聲明的方法,那麼可以調用d1obj.base::func()來實現它,而不是僅僅調用d1obj->func()

儘管它可能看起來有點像吹毛求疵地說,你包含的所有數據和您的父母的方法沒有概念包含你的父母的實例,這是一個重要的區別,因爲你經常可以達到許多的直接繼承的好處通過創建一個類,其中包含「父」類的數據成員,就像這樣:

class derived2 /*no parent listed */ { 
public: 

    derived2() :_b() {} 

private: 
    base _b; 
} 

這種結構允許你利用已經base實現的方法,作爲實施細則你自己的方法,而不必公開任何方法base宣佈公開但你不想授予訪問權限。這方面的一個重要的例子是stack,其可以包含另一個STL容器的一個實例(如vectordequelist),然後使用其back()實施top()push_back()push()pop_back()pop(),所有這些都無需以暴露原來的方法給用戶。

+1

*你不能說d1obj.base.func()例如,如果你已經重載了你的父母聲明的方法*。是的,你可以,但語法略有不同:'d1obj.base :: func()'會調用'base'成員函數'func',即使它在派生類型中被* hidden *或* overridden *重載*表示'func'的*不同*簽名在派生類型中定義,並且會隱藏*父類版本) –

+0

我的觀點是在語法中。在我給出的例子中,你試圖訪問某種包含的對象。在你的例子中,類似於我給出的第二個例子,你要求'd1obj'像那個調用的父類一樣行爲。它看起來有點小差異,但是'derived1 :: func()'可能會修改'base :: func()'所具有的相同成員,只能用不同的方式,所以它確實是一種行爲轉變,而不僅僅是修改一個包含的基類。你從'base'繼承了單獨的數據成員,但是它們並沒有在內部的'base'結構中包裝在一起。 – matthias

+1

我不太清楚你在哪裏得到你的理由,但是*「你從基地單獨繼承了數據成員,但是它們並沒有在基地內部一起包裝」*又是不準確的,派生類型確實包含基類型子對象包裹在一起作爲「基地」。考慮當你上傳或者當一個不是overiden的成員函數被執行時會發生什麼,不管最終的對象是'base'還是'derived'類型,它都必須能夠在存儲器中精確定位基本成員。我同意*概念*派生類型*是-a *而不是*包含*。 –