c++ 虛繼承,多繼承相關總結

看這一篇文章之前強烈建議先看以下我之前發佈的

虛指針,虛函數剖析

例1: 以下代碼輸出什麼?

#include <iostream>
using namespace std;


class A 
{
protected:
 int m_data;
public:
 A(int data = 0) {m_data=data;}
 int GetData() { return doGetData(); }
 virtual int doGetData() { return m_data; }
};

class B : public A
{
protected:
 int m_data;
public:
 B(int data = 1) { m_data = data; }
 int doGetData() { return m_data; }
};

class C: public B
{
protected:
 int m_data;
public:
 C(int data=2) { m_data = data; }
};

int main(int argc, char const *argv[])
{
 C c(10);

 cout << c.GetData() << endl;
 cout << c.A::GetData() << endl;
 cout << c.B::GetData() << endl;
 cout << c.C::GetData() << endl;
 cout << c.doGetData() << endl;
 cout << c.A::doGetData() << endl;
 cout << c.B::doGetData() << endl;
 cout << c.C::doGetData() << endl;
 return 0;
}

構造函數從最初始的基類開始構造,各個類的同名變量沒有形成覆蓋,都是單獨的變量。

理解這兩個重要的C++特性後解決這個問題就比較輕松瞭。 下面我們詳解這幾條輸出語句。

cout << c.GetData() << endl; 本來是要調用C類的 GetData(), C中未定義, 故調用 B 中的, 但是 B 中也未定義, 故調用 A 中的 GetData(), 因為 A 中的 doGetData()是虛函數,所以調用 B 類中的 doGetData(),而 B 類的 doGetData() 返回 B::m_data, 故輸出1。

cout << c.A::GetData() << endl; 因為 A 中的 doGetData() 是虛函數,又因為 C 類中未重定義該接口,所以調用 B 類中的 doGetData(), 而 B 類的 doGetData() 返回 B::m_data, 故輸出 l 。

cout << c.B::GetData() << endl; C調用哪一個GetData() 本質上都是調用的A::GetData(), 調用到 doGetData() 虛函數,再調用父類B覆蓋後的虛函數,返回B::m_data, 所以前5個都是1

cout << c.A::doGetData() << endl; 顯示調用A::doGetData(), 返回 A::m_data, 是0

cout << c.B::doGetData() << endl;, cout << c.C::doGetData() << endl; 都將調用B::doGetData(), 返回B::m_data, 是1

所以結果為: 1 1 1 1 1 0 1 1

方便排版,請忽略掉換行。

最後附上內存結構圖:

例2: 為什麼虛函數效率低?

因為虛函數需要一次間接的尋址,而普通的函數可以在編譯時定位到函數的地址,虛函數是要根據虛指針定位到函數的地址。多增加瞭一個過程,效率肯定低一些,但帶來瞭運行時的多態。

C++支持多重繼承,從而大大增強瞭面向對象程序設計的能力。多重繼承是一個類從多個基類派生而來的能力,派生類實際上獲取瞭所有基類的特性。當一個類是兩個或多個基類的派生類時,必須在派生類名和冒號之後,列出所有基類的類名,基類間用逗號隔開。 派生類的構造函數必須激活所有基類的構造函數,並把相應的參數傳遞給它們。派生類可以是另一個類的基類,這樣,相當於形成瞭一個繼承鏈。當派生類的構造函數被激活時,它的所有基類的構造函數也都會被激活。

在面向對象的程序設計中,繼承和多重繼承一般指公共繼承。 在無繼承的類中,protected 和 private 控制符是沒有差別的,在繼承中,基類的 private 對所有的外界都屏蔽(包括自己的派生類), 基類的 protected 控制符對應用程序是屏蔽的, 但對其派生類是可訪問的。

虛繼承

什麼是虛繼承?它與一般的繼承有什麼不同?它有什麼用?

虛擬繼承是多重繼承中特有的概念。 虛擬基類是為解決多重繼承而出現的。 請看下圖:

類D繼承自類B和類C, 而類B和類C都繼承自類A.

在類D中會兩次出現A。為瞭節省內存空間,可以將B、C對A 的繼承定義為虛擬繼承,而A就成瞭虛擬基類。 最後形成如下圖所示的情況:

代碼如下:
class A; 
class B : public virtual A;
class C : public virtual A;
class D : public B, public C;

註意: 虛函數繼承和虛繼承是完全不同的兩個概念.

例3: 請評價多重繼承的優點和缺陷。

多重繼承在語言上並沒有什麼很嚴重的問題,但是標準本身隻對語義做瞭規定,而對編譯器的細節沒有做規定。所以在使用時(即使是繼承),最好不要對內存佈局等有什麼假設。為瞭避免由此帶來的復雜性,通常推薦使用復合。

  1. 多重繼承本身並沒有問題,不過大多數系統的類層次往往有一個公共的基類,而這樣的結構如果使用多重繼承,稍有不慎,將會出現一個嚴重現象————菱形繼承,這樣的繼承方式會使得類的訪問結構非常復雜。 但並非不可處理,可以用virtual繼承(並非唯一的方法)
  2. 從哲學上來說,C++多重繼承必須要存在,這個世界本來就不是單根的。從實際用途上來說,多重繼承不是必需的。
  3. 多重繼承在面向對象理論中並非是必要的————因為它不提供新的語義,可以通過單繼承與復合結構來取代。 而Java則放棄瞭多重繼承,使用簡單的interface取代。 因為C++中沒有interface這個關鍵字,所以不存在所謂的“接口”技術。但是C++可以很輕松地做到這樣的模擬,因為C++中的不定義屬性的抽象類就是接口。
  4. 多重繼承本身並不復雜,對象佈局也不混亂,語言中都有明確的定義。真正復雜的是使用瞭運行時多態(virtual)的多重繼承(因為語言對於多態的實現沒有明確的定義)。
  5. 要瞭解C++,就要明白有很多概念是C++ 試圖考慮但是最終放棄的設計。你會發現很多Java、C#中的東西都是C++考慮後放棄的。

不是說這些東西不好,而是在C++中它將破壞C++作為一個整體的和諧性,或者C++ 並不需要這樣的東西。

舉個例子來說明,C#中有一個關鍵字base用來表示該類的父類,C++卻沒有對應的關鍵字。為什麼沒有?其實C++中曾經有人提議用一個類似的關鍵字 inherited, 來表示被繼承的類,即父類。 這樣一個好的建議為什麼沒有被采納呢?因為這樣的關鍵字既不必須又不充分。 不必須是因為 C++有一個 typedef* inherited,不充分是因為有多個基類,你不可能知道 inherited 指的是哪個基類。

例4: 在多繼承的時候,如果一個類繼承同時繼承自 class A 和 class B, 而 class A 和 B 中都有一個函數叫 foo(), 如何明確地在子類中指出調用是哪個父類的 foo()?

class A
{
public:
 void foo() { cout << "A foo" << endl; }
};

class B
{
public:
 void foo() { cout << "B foo" << endl; }
};

class C : public A, public B
{

};

int main(int argc, char const* argv[])
{
 C c;
 c.A::foo();
 return 0;
}

C 繼承自 A 和 B, 如果出現瞭相同的函數foo(), 那麼C.A::foo(), C.B::foo() 就分別代表從 A 類中繼承的 foo 函數和從 B 類中繼承的 foo 函數。

例5: 以下代碼輸出什麼?

class A
{
 int m_nA;
};

class B
{
 int m_nB;
};

class C : public A, public B
{
 int m_nC;
};

int main(int argc, char const* argv[])
{
 C* pC = new C;
 B* pB = dynamic_cast<B*>(pC);
 A* pA = dynamic_cast<A*>(pC);
 cout << (pC == pB) << endl;
 cout << (pC == pA) << endl;
 cout << ((int)pC == (int)pB) << endl;
 cout << ((int)pC == (int)pA) << endl;
 return 0;
}

當進行pC=pB比較時,實際上是比較pC指向的對象和隱式轉換pB後pB 指向的對象 (pC指向的對象)的部分,這個是同一部分,是相等的。

但是,pB實際上指向的地址是對象C中的父類B部分,從地址上跟pC不一樣,所以直接比較地址數值的時候是不相等的。

內存結構圖如下:

例6: 如果鳥是可以飛的,那麼駝鳥是鳥麼?駝鳥如何繼承鳥類?

鳥是可以飛的。 也就是說,當鳥飛行時,它的高度是大於0的。 駝鳥是鳥類(生物學上)的一種, 但它的飛行高度為0(駝鳥不能飛)。

不要把可替代性和子集相混淆。 即使駝鳥集是鳥集的一個子集(每個駝鳥集都在鳥集內),但並不意味著鴕鳥的行為能夠代替鳥的行為。 可替代性與行為有關,與子集沒有關系。 當評價一個潛在的繼承關系時,重要的因素是可替代的行為,而不是子集。

如果一定要讓駝鳥來繼承鳥類, 可以采取組合的辦法, 把鳥類中的可以被駝鳥繼承的函數挑選出來,這樣駝鳥就不是”a kind of”鳥瞭,而是”has some kind of”鳥的屬性而已。

class bird
{
public:
 void eat();
 void sleep();
 void fly();
};

class ostrich
{
public:
 void eat();
 void sleep();
};

例7: C++中如何阻止一個類被實例化?

使用抽象類,或者構造函數被聲明成private。

最後補充兩個知識點:

函數的隱藏和覆蓋

  • 函數的隱藏: 沒有定義多態的情況下,即沒有加virtual的前提下,如果定義瞭父類和子類,父類和子類出現瞭同名的函數,就稱子類的函數把同名的父類的函數給隱藏瞭。
  • 函數的覆蓋:是針對多態來說的。 如果定義瞭父類和子類,父類中定義瞭公共的虛函數,如果此時子類中沒有定義同名的虛函數,那麼在子類的虛函數表中將會寫上父類的該虛函數的函數入口地址,如果在子類中定義瞭同名虛函數的話,那麼在子類的虛函數表中將會把原來的父類的虛函數地址覆蓋掉,覆蓋成子類的虛函數的函數地址。

總結: 本文的重點還是承接之前“虛指針,虛表剖析”的內容,對於多重繼承,沒有探究其內存結構,並且也不是很好弄清楚,其功能大多數可以被組合(composition)的方式實現,C++標準沒有給出編譯器具體的多繼承的實現細節,不同的編譯器有不同的做法。

以上就是c++虛繼承,多繼承相關總結的詳細內容,更多關於c++ 繼承的資料請關註WalkonNet其它相關文章!

推薦閱讀: