c++ 虛繼承,多繼承相關(guān)總結(jié)
看這一篇文章之前強(qiáng)烈建議先看以下我之前發(fā)布的
例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; }
構(gòu)造函數(shù)從最初始的基類開(kāi)始構(gòu)造,各個(gè)類的同名變量沒(méi)有形成覆蓋,都是單獨(dú)的變量。
理解這兩個(gè)重要的C++特性后解決這個(gè)問(wèn)題就比較輕松了。 下面我們?cè)斀膺@幾條輸出語(yǔ)句。
cout << c.GetData() << endl; 本來(lái)是要調(diào)用C類的 GetData(), C中未定義, 故調(diào)用 B 中的, 但是 B 中也未定義, 故調(diào)用 A 中的 GetData(), 因?yàn)?A 中的 doGetData()是虛函數(shù),所以調(diào)用 B 類中的 doGetData(),而 B 類的 doGetData() 返回 B::m_data, 故輸出1。
cout << c.A::GetData() << endl; 因?yàn)?A 中的 doGetData() 是虛函數(shù),又因?yàn)?C 類中未重定義該接口,所以調(diào)用 B 類中的 doGetData(), 而 B 類的 doGetData() 返回 B::m_data, 故輸出 l 。
cout << c.B::GetData() << endl; C調(diào)用哪一個(gè)GetData() 本質(zhì)上都是調(diào)用的A::GetData(), 調(diào)用到 doGetData() 虛函數(shù),再調(diào)用父類B覆蓋后的虛函數(shù),返回B::m_data, 所以前5個(gè)都是1
cout << c.A::doGetData() << endl; 顯示調(diào)用A::doGetData(), 返回 A::m_data, 是0
cout << c.B::doGetData() << endl;, cout << c.C::doGetData() << endl; 都將調(diào)用B::doGetData(), 返回B::m_data, 是1
所以結(jié)果為: 1 1 1 1 1 0 1 1
方便排版,請(qǐng)忽略掉換行。
最后附上內(nèi)存結(jié)構(gòu)圖:
例2: 為什么虛函數(shù)效率低?
因?yàn)樘摵瘮?shù)需要一次間接的尋址,而普通的函數(shù)可以在編譯時(shí)定位到函數(shù)的地址,虛函數(shù)是要根據(jù)虛指針定位到函數(shù)的地址。多增加了一個(gè)過(guò)程,效率肯定低一些,但帶來(lái)了運(yùn)行時(shí)的多態(tài)。
C++支持多重繼承,從而大大增強(qiáng)了面向?qū)ο蟪绦蛟O(shè)計(jì)的能力。多重繼承是一個(gè)類從多個(gè)基類派生而來(lái)的能力,派生類實(shí)際上獲取了所有基類的特性。當(dāng)一個(gè)類是兩個(gè)或多個(gè)基類的派生類時(shí),必須在派生類名和冒號(hào)之后,列出所有基類的類名,基類間用逗號(hào)隔開(kāi)。 派生類的構(gòu)造函數(shù)必須激活所有基類的構(gòu)造函數(shù),并把相應(yīng)的參數(shù)傳遞給它們。派生類可以是另一個(gè)類的基類,這樣,相當(dāng)于形成了一個(gè)繼承鏈。當(dāng)派生類的構(gòu)造函數(shù)被激活時(shí),它的所有基類的構(gòu)造函數(shù)也都會(huì)被激活。
在面向?qū)ο蟮某绦蛟O(shè)計(jì)中,繼承和多重繼承一般指公共繼承。 在無(wú)繼承的類中,protected 和 private 控制符是沒(méi)有差別的,在繼承中,基類的 private 對(duì)所有的外界都屏蔽(包括自己的派生類), 基類的 protected 控制符對(duì)應(yīng)用程序是屏蔽的, 但對(duì)其派生類是可訪問(wèn)的。
虛繼承
什么是虛繼承?它與一般的繼承有什么不同?它有什么用?
虛擬繼承是多重繼承中特有的概念。 虛擬基類是為解決多重繼承而出現(xiàn)的。 請(qǐng)看下圖:
類D繼承自類B和類C, 而類B和類C都繼承自類A.
在類D中會(huì)兩次出現(xiàn)A。為了節(jié)省內(nèi)存空間,可以將B、C對(duì)A 的繼承定義為虛擬繼承,而A就成了虛擬基類。 最后形成如下圖所示的情況:
代碼如下: class A; class B : public virtual A; class C : public virtual A; class D : public B, public C;
注意: 虛函數(shù)繼承和虛繼承是完全不同的兩個(gè)概念.
例3: 請(qǐng)?jiān)u價(jià)多重繼承的優(yōu)點(diǎn)和缺陷。
多重繼承在語(yǔ)言上并沒(méi)有什么很?chē)?yán)重的問(wèn)題,但是標(biāo)準(zhǔn)本身只對(duì)語(yǔ)義做了規(guī)定,而對(duì)編譯器的細(xì)節(jié)沒(méi)有做規(guī)定。所以在使用時(shí)(即使是繼承),最好不要對(duì)內(nèi)存布局等有什么假設(shè)。為了避免由此帶來(lái)的復(fù)雜性,通常推薦使用復(fù)合。
- 多重繼承本身并沒(méi)有問(wèn)題,不過(guò)大多數(shù)系統(tǒng)的類層次往往有一個(gè)公共的基類,而這樣的結(jié)構(gòu)如果使用多重繼承,稍有不慎,將會(huì)出現(xiàn)一個(gè)嚴(yán)重現(xiàn)象————菱形繼承,這樣的繼承方式會(huì)使得類的訪問(wèn)結(jié)構(gòu)非常復(fù)雜。 但并非不可處理,可以用virtual繼承(并非唯一的方法)
- 從哲學(xué)上來(lái)說(shuō),C++多重繼承必須要存在,這個(gè)世界本來(lái)就不是單根的。從實(shí)際用途上來(lái)說(shuō),多重繼承不是必需的。
- 多重繼承在面向?qū)ο罄碚撝胁⒎鞘潜匾摹驗(yàn)樗惶峁┬碌恼Z(yǔ)義,可以通過(guò)單繼承與復(fù)合結(jié)構(gòu)來(lái)取代。 而Java則放棄了多重繼承,使用簡(jiǎn)單的interface取代。 因?yàn)镃++中沒(méi)有interface這個(gè)關(guān)鍵字,所以不存在所謂的“接口”技術(shù)。但是C++可以很輕松地做到這樣的模擬,因?yàn)镃++中的不定義屬性的抽象類就是接口。
- 多重繼承本身并不復(fù)雜,對(duì)象布局也不混亂,語(yǔ)言中都有明確的定義。真正復(fù)雜的是使用了運(yùn)行時(shí)多態(tài)(virtual)的多重繼承(因?yàn)檎Z(yǔ)言對(duì)于多態(tài)的實(shí)現(xiàn)沒(méi)有明確的定義)。
- 要了解C++,就要明白有很多概念是C++ 試圖考慮但是最終放棄的設(shè)計(jì)。你會(huì)發(fā)現(xiàn)很多Java、C#中的東西都是C++考慮后放棄的。
不是說(shuō)這些東西不好,而是在C++中它將破壞C++作為一個(gè)整體的和諧性,或者C++ 并不需要這樣的東西。
舉個(gè)例子來(lái)說(shuō)明,C#中有一個(gè)關(guān)鍵字base用來(lái)表示該類的父類,C++卻沒(méi)有對(duì)應(yīng)的關(guān)鍵字。為什么沒(méi)有?其實(shí)C++中曾經(jīng)有人提議用一個(gè)類似的關(guān)鍵字 inherited, 來(lái)表示被繼承的類,即父類。 這樣一個(gè)好的建議為什么沒(méi)有被采納呢?因?yàn)檫@樣的關(guān)鍵字既不必須又不充分。 不必須是因?yàn)?C++有一個(gè) typedef* inherited,不充分是因?yàn)橛卸鄠€(gè)基類,你不可能知道 inherited 指的是哪個(gè)基類。
例4: 在多繼承的時(shí)候,如果一個(gè)類繼承同時(shí)繼承自 class A 和 class B, 而 class A 和 B 中都有一個(gè)函數(shù)叫 foo(), 如何明確地在子類中指出調(diào)用是哪個(gè)父類的 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, 如果出現(xiàn)了相同的函數(shù)foo(), 那么C.A::foo(), C.B::foo() 就分別代表從 A 類中繼承的 foo 函數(shù)和從 B 類中繼承的 foo 函數(shù)。
例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; }
當(dāng)進(jìn)行pC=pB比較時(shí),實(shí)際上是比較pC指向的對(duì)象和隱式轉(zhuǎn)換pB后pB 指向的對(duì)象 (pC指向的對(duì)象)的部分,這個(gè)是同一部分,是相等的。
但是,pB實(shí)際上指向的地址是對(duì)象C中的父類B部分,從地址上跟pC不一樣,所以直接比較地址數(shù)值的時(shí)候是不相等的。
內(nèi)存結(jié)構(gòu)圖如下:
例6: 如果鳥(niǎo)是可以飛的,那么駝鳥(niǎo)是鳥(niǎo)么?駝鳥(niǎo)如何繼承鳥(niǎo)類?
鳥(niǎo)是可以飛的。 也就是說(shuō),當(dāng)鳥(niǎo)飛行時(shí),它的高度是大于0的。 駝鳥(niǎo)是鳥(niǎo)類(生物學(xué)上)的一種, 但它的飛行高度為0(駝鳥(niǎo)不能飛)。
不要把可替代性和子集相混淆。 即使駝鳥(niǎo)集是鳥(niǎo)集的一個(gè)子集(每個(gè)駝鳥(niǎo)集都在鳥(niǎo)集內(nèi)),但并不意味著鴕鳥(niǎo)的行為能夠代替鳥(niǎo)的行為。 可替代性與行為有關(guān),與子集沒(méi)有關(guān)系。 當(dāng)評(píng)價(jià)一個(gè)潛在的繼承關(guān)系時(shí),重要的因素是可替代的行為,而不是子集。
如果一定要讓駝鳥(niǎo)來(lái)繼承鳥(niǎo)類, 可以采取組合的辦法, 把鳥(niǎo)類中的可以被駝鳥(niǎo)繼承的函數(shù)挑選出來(lái),這樣駝鳥(niǎo)就不是"a kind of"鳥(niǎo)了,而是"has some kind of"鳥(niǎo)的屬性而已。
class bird { public: void eat(); void sleep(); void fly(); }; class ostrich { public: void eat(); void sleep(); };
例7: C++中如何阻止一個(gè)類被實(shí)例化?
使用抽象類,或者構(gòu)造函數(shù)被聲明成private。
最后補(bǔ)充兩個(gè)知識(shí)點(diǎn):
函數(shù)的隱藏和覆蓋
- 函數(shù)的隱藏: 沒(méi)有定義多態(tài)的情況下,即沒(méi)有加virtual的前提下,如果定義了父類和子類,父類和子類出現(xiàn)了同名的函數(shù),就稱子類的函數(shù)把同名的父類的函數(shù)給隱藏了。
- 函數(shù)的覆蓋:是針對(duì)多態(tài)來(lái)說(shuō)的。 如果定義了父類和子類,父類中定義了公共的虛函數(shù),如果此時(shí)子類中沒(méi)有定義同名的虛函數(shù),那么在子類的虛函數(shù)表中將會(huì)寫(xiě)上父類的該虛函數(shù)的函數(shù)入口地址,如果在子類中定義了同名虛函數(shù)的話,那么在子類的虛函數(shù)表中將會(huì)把原來(lái)的父類的虛函數(shù)地址覆蓋掉,覆蓋成子類的虛函數(shù)的函數(shù)地址。
總結(jié): 本文的重點(diǎn)還是承接之前“虛指針,虛表剖析”的內(nèi)容,對(duì)于多重繼承,沒(méi)有探究其內(nèi)存結(jié)構(gòu),并且也不是很好弄清楚,其功能大多數(shù)可以被組合(composition)的方式實(shí)現(xiàn),C++標(biāo)準(zhǔn)沒(méi)有給出編譯器具體的多繼承的實(shí)現(xiàn)細(xì)節(jié),不同的編譯器有不同的做法。
以上就是c++虛繼承,多繼承相關(guān)總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于c++ 繼承的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
StretchBlt函數(shù)和BitBlt函數(shù)用法案例詳解
這篇文章主要介紹了StretchBlt函數(shù)和BitBlt函數(shù)用法案例詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08基于OpenCV和C++ 實(shí)現(xiàn)圖片旋轉(zhuǎn)
這篇文章主要介紹了基于OpenCV和C++ 實(shí)現(xiàn)圖片旋轉(zhuǎn),幫助大家更好的利用c++處理圖片,感興趣的朋友可以了解下2020-12-12C++鏈表實(shí)現(xiàn)通訊錄管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C++鏈表實(shí)現(xiàn)通訊錄管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-12-12C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)易學(xué)生管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)易學(xué)生管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06C++中Semaphore內(nèi)核對(duì)象用法實(shí)例
這篇文章主要介紹了C++中Semaphore內(nèi)核對(duì)象用法實(shí)例,有助于深入了解信號(hào)量(Semaphore)的基本用法,需要的朋友可以參考下2014-10-10