C++多重繼承一直是一個讓人搞不太清楚的一個問題,但是有時候為了實現(xiàn)多個接口,多重繼承是基本不可避免,當(dāng)然在Windows下我們有強大的COM來幫我們搞定這個事情,不過如果你想自己實現(xiàn)一套類似于COM的東西出來的時候,麻煩事情就來了。
在COM里面,有兩個很基礎(chǔ)的,而且我們都會用到的特性:
1. 純虛接口:一般使用一個只有純虛函數(shù)的類來作為接口
2. 引用計數(shù):在C++中一般都使用引用計數(shù)來管理對象的生命周期
這兩個功能在一般設(shè)計C++接口的時候也經(jīng)常用到。其實說到底,上面這兩個特性牽扯到的是多重繼承的二個表現(xiàn):
1. 多重繼承中的數(shù)據(jù)存儲
2. 多重繼承中的虛函數(shù)
在COM中,純虛接口是使用的interface來定義的,引用計數(shù)是通過IUnknown接口來實現(xiàn)的,所有的接口都是從IUnknown這種接口中派生出來的。當(dāng)我們需要某一個類實現(xiàn)多個接口的時候,就經(jīng)常會遇見這樣的多重繼承:
multi-inheritance-com:

哦?!是不是很眼熟,ios,istream,ostream,iostream。。各種C++書籍最喜歡用的一個示例。好吧,現(xiàn)在我們先自己實現(xiàn)一個吧,看看到底要怎么使用多重繼承。
多重繼承中對象的的數(shù)據(jù)存儲
這便是使用多重繼承的時候經(jīng)常產(chǎn)生的第一個問題:多副本的數(shù)據(jù)存儲。
當(dāng)然這個問題很好解決,只需要使用虛繼承即可解決。只需要在IA和IB的定義中,在public IBase前加入virtual關(guān)鍵字即可。
結(jié)果已經(jīng)正確了,為什么會發(fā)生這種情況呢?虛繼承到底干了些什么呢?
我們先來看看在沒有使用虛繼承的情況下,CImpl的在內(nèi)存中是怎么樣的:
cimpl-memory-normal:

對于普通的public繼承(非虛繼承),C++會將基類和派生類的內(nèi)容放在一塊,合起來組成一個完整的派生類,在內(nèi)存上看,它們是連在一起的。按照這樣的規(guī)則,在這里,IA和IB中就會各包含一個IBase,而IA,IB和CImpl中的部分內(nèi)容又共同組成了CImpl。在將CImpl對象o的指針轉(zhuǎn)型成IA的指針pA過程中,指針將被移動到IA所在的部分區(qū)域,同樣在轉(zhuǎn)型成IB的過程中,指針將被移動到IB所在的部分區(qū)域(也就是說,轉(zhuǎn)型之后,指針的值都不一樣)。在之后的操作中,pA操作的便是IA這個部分中的IBase,而pB操作的便是IB這個部分中的IBase,最后IA部分中的IBase變成了1,而IB部分中的IBase變成了-1。所以輸出的結(jié)果也就變成了1和-1了。
之后我們修改成了虛繼承,看看到底發(fā)生了什么?
cimpl-memory-virtual:

原來的IA和IB中的IBase部分變成了一個指向基類的指針,而基類也變成了一個單獨的部分。這樣一旦對基類做任何的修改,都會通過這個指針重定向到這個獨立的基類上去,于是,就不存在多副本的數(shù)據(jù)存儲了,這個詭異的問題也就解決了。但是當(dāng)然從這個圖上我們也可以看到,使用虛繼承后,訪問數(shù)據(jù)多了一次跳轉(zhuǎn),這多出的一次跳轉(zhuǎn)將導(dǎo)致效率的下降一倍甚至更多,所以如果一個類使用的非常頻繁,很明顯應(yīng)該盡量避免使用虛繼承。
二義性
當(dāng)然數(shù)據(jù)的存儲只是使用多重繼承中遇到的一個問題,現(xiàn)在我們來看另外一個問題,函數(shù)的二義性。
首先我們先把數(shù)據(jù)的存儲拋開,單純的來看一個只有函數(shù)的繼承關(guān)系。
出錯了!杯具。。為什么?錯誤還這么奇怪,神馬叫做可以是IBase中的foo又可以是IBase中的foo呢?
這就是使用多重繼承的時候經(jīng)常產(chǎn)生的第二個問題:二義性。
在使用多重繼承時,如果有兩個被繼承的類擁有共同的基類,那么就很容易出現(xiàn)這種情況。那什么是二義性呢?
我們先來看一個更簡單的繼承關(guān)系:
既然直接使用多重繼承會有如此多的問題,那么我們能不能通過虛函數(shù)來解決這個問題呢?
這里小小的提一下,剛剛二義性里面說到兩個類的距離,對于編譯器來說,一般是找離當(dāng)前的類距離最近的函數(shù)實現(xiàn)來調(diào)用(或者數(shù)據(jù)來訪問),而虛函數(shù)則是讓編譯器做相反的事情:找一個離當(dāng)前類反向距離最遠的函數(shù)實現(xiàn)來調(diào)用。
好,我們先把上面的程序做一點點小改變,把foo()函數(shù)變成一個虛函數(shù),看看有什么變化。
產(chǎn)生問題的原因依然是二義性。即便換成virtual函數(shù),也不能改變二義性這個問題。為什么呢?
因為我們是用的.運算符來訪問的,而不是用指針,所以這里虛函數(shù)和普通函數(shù)沒有任何區(qū)別。=.=。。。
好,我們再來小小的修改一下,把他變成指針,讓他通過虛表去訪問,看看行不行。
編譯,結(jié)果。。。還是一樣失敗。。。
好吧,我們可以把調(diào)用foo()的幾句話都去掉,來看看CImpl中生成的虛表到底是個什么樣子。
debug-result-vptr-1:
在這個實例中,IA和CImpl部分公用一個虛表,而IB則使用另外的一個虛表(兩個虛表這個特性主要是在指針類型轉(zhuǎn)換的時候有用,這里就不說了)。
在這IA的虛表中存在一個指向IBase::foo()的指針,在IB的虛表中也存在一個指向IBase::foo()的指針,所以在CImpl中,可以找到兩個IBase::foo()函數(shù)的指針,這樣,編譯器就無法確定到底應(yīng)該使用哪一個IBase::foo()函數(shù)作為他自己的foo()函數(shù)了。二義性也就產(chǎn)生了。
既然如此,那解決起來就沒有什么別的辦法了,只能把foo函數(shù)的最終在CImpl中實現(xiàn)一次了。
在多重繼承中編譯器對this指針的修正
這里再讓我們來看看這次編譯出來的虛表,看看還有什么發(fā)現(xiàn)。
debug-result-vptr-2:

0x004112a3 [thunk]:CImpl::foo`adjustor{4}’ (void) *
這個看上去很怪的函數(shù)是什么呢?我們反匯編一下他看看。
virtual-function-wrapper:

這里可以看到有一句匯編指令:sub ecx, 4。這條指令的左右其實是在修正this指針。
因為從IB的虛表來的請求,this指針都是指向CImpl中IB的部分,而當(dāng)調(diào)用CImpl中的foo函數(shù)時,如果還使用IB的this指針,那么程序就會出錯,所以這里需要先將this指針修正為CImpl所在的地址,才能調(diào)用CImpl的foo函數(shù)。
在程序運行的時候,this指針一般被存儲在ecx寄存器中,或者當(dāng)前函數(shù)的第一個參數(shù)傳遞進去,不過不同的語言或者不同的編譯器編譯出來的代碼可能會不一樣。
我們這里的析構(gòu)函數(shù)都是虛函數(shù),所以我們還可以在截圖中看到,編譯器會對析構(gòu)函數(shù)做同樣的處理。
如何同時解決數(shù)據(jù)訪問和二義性問題呢
貌似到現(xiàn)在都只提到最簡單的一種多重繼承的情況,但是實際上我們已經(jīng)遇到了很多的問題了,既然多重繼承中會有這么多問題,那我們有沒有什么比較通用的方法能把他們一起解決了呢?
方法肯定是有的:
1. 使用虛繼承
這算是一種確實可行的方法,只是說會帶來額外的時間和空間的開銷,訪問任何一個數(shù)據(jù),都需要通過虛繼承表進行跳轉(zhuǎn),不過一般來說夠用了。
2. 虛函數(shù)當(dāng)接口,繼承多個接口,統(tǒng)一實現(xiàn)
這個思想就類似于COM了,只是說COM用的是純虛函數(shù),對于那些會產(chǎn)生二義性的類,我們在最后都實現(xiàn)一邊,這樣就不會有問題了。這樣帶來的時間開銷也僅僅是調(diào)用時查詢一次虛表。但是麻煩的地方就是,有時候繼承一下,你可能就要實現(xiàn)一下了,比如引用計數(shù)神馬的,當(dāng)然你也可以通過模版來簡化你的代碼。
class IBase
{
public:
virtual ~IBase() {}
virtual void show() = 0;
};
class IA : public IBase
{
public:
virtual ~IA() {}
virtual int inc() = 0;
};
class IB : public IBase
{
public:
virtual ~IB() {}
virtual int dec() = 0;
};
class CImpl : public IA, public IB
{
public:
CImpl() : n(0) {}
virtual ~CImpl() {}
int inc() { return ++n; }
int dec() { return --n; }
void show() { printf("%dn", n); }
private:
int n;
};
|
3. 通過純虛函數(shù)實現(xiàn)模版方法,將函數(shù)轉(zhuǎn)移
這種實現(xiàn)比較復(fù)雜,wtl中用的比較多,一般是用在引用計數(shù)上,好處很明顯,就是可以繼承,不用每個類都實現(xiàn)一個引用計數(shù),而只用將新的基類的引用計數(shù)轉(zhuǎn)移至原本存在的類上就可以了。
class IBase
{
public:
virtual ~IBase() {}
void foo() {}
};
class IA : public IBase
{
public:
virtual ~IA() {}
};
class IShifter
{
public:
virtual ~IShifter() {}
void foo() { do_foo(); }
protected:
virtual void do_foo() = 0;
};
class IB : public IShifter
{
public:
virtual ~IB() {}
};
class CImpl : public IA, public IB
{
public:
virtual ~CImpl() {}
void foo() { IA::foo(); }
protected:
virtual void do_foo() { IA::foo(); }
};
|