小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

多重繼承

 sun317 2013-01-14

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ù)存儲

#include <stdio.h>
  
class IBase
{
public:
    IBase() : n(0) {}
    virtual ~IBase() {}
    void show() { printf("%dn", n); }
    int inc() { return ++n; }
    int dec() { return --n; }
  
protected:
    int n;
};
  
class IA : public IBase
{
public:
    virtual ~IA() {}
};
  
class IB : public IBase
{
public:
    virtual ~IB() {}
};
  
class CImpl : public IA, public IB
{
public:
    virtual ~CImpl() {}
};
  
int main(int argc, char* argv[])
{
    CImpl o;
    IA *pA = &o;
    IB *pB = &o;
  
    pA->inc();
    pA->show();
  
    pB->dec();
    pB->show();
  
    return 0;
}
編譯,OK,成功了!好,運行試一試。
run-result-1:

為什么是1和-1呢?明明n只在繼承的一個類IBase里面有,一次加1,一次減一,結(jié)果不是應(yīng)該是1和0么?是不是很奇怪?

這便是使用多重繼承的時候經(jīng)常產(chǎn)生的第一個問題:多副本的數(shù)據(jù)存儲。
當(dāng)然這個問題很好解決,只需要使用虛繼承即可解決。只需要在IA和IB的定義中,在public IBase前加入virtual關(guān)鍵字即可。

class IA : virtual public IBase
class IB : virtual public IBase
現(xiàn)在再讓我們來看一看運行結(jié)果:
run-result-2:

結(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)系。

class A
{
public:
    void foo();
};
  
class B : public A
{
public:
    void foo();
};
  
class C : public B
{
public:
    void foo();
};
編譯一下,試試。
error C2385: ambiguous access of ‘foo’
could be the ‘foo’ in base ‘IBase’
or could be the ‘foo’ in base ‘IBase’
error C3861: ‘foo’: identifier not found

出錯了!杯具。。為什么?錯誤還這么奇怪,神馬叫做可以是IBase中的foo又可以是IBase中的foo呢?

這就是使用多重繼承的時候經(jīng)常產(chǎn)生的第二個問題:二義性。
在使用多重繼承時,如果有兩個被繼承的類擁有共同的基類,那么就很容易出現(xiàn)這種情況。那什么是二義性呢?
我們先來看一個更簡單的繼承關(guān)系:

class A
{
public:
    void foo();
};
  
class B : public A
{
public:
    void foo();
};
  
class C : public B
{
public:
    void foo();
};
我們可以把繼承關(guān)系中,兩個類之間沿著基類方向的相隔的繼承級數(shù)看成一個距離,那么C到A的距離是2,B到A的距離就是1。當(dāng)然距離不能為負。
當(dāng)我們對ABC中某個對象調(diào)用foo函數(shù)的時候,編譯器會優(yōu)先選擇離當(dāng)前指針類型的距離最短的一個函數(shù)實現(xiàn)去調(diào)用,也就是說,foo函數(shù)的查找路徑是C->B->A,找到一個最近的去調(diào)用。
而對于我們當(dāng)前這個繼承關(guān)系來說,IA和IB還是各包含一份IBase的實例,雖然在內(nèi)存里這里僅僅是包含一份數(shù)據(jù),但是在編譯的過程中,IA和IB中還包含了一份從IBase中繼承下來的函數(shù)列表。所以有兩個包含有foo函數(shù)類與CImpl類的距離是一樣的,所以在對CImpl調(diào)用foo函數(shù),就產(chǎn)生了所謂的二義性,除非我們指定使用IA::foo或者IB::foo,否則編譯器將無法決定使用哪一個基類的foo函數(shù)。
o.IA::foo();    // 指定調(diào)用CImpl從IA部分繼承過來的foo函數(shù),這樣就可以編譯通過了。
當(dāng)然如果我們這樣寫代碼也是不行的:
IBase *pBase = &o;    // 指針轉(zhuǎn)義時的二義性,不知道是使用IA中的IBase部分,還是IB中的IBase部分
o.inc();                    // 數(shù)據(jù)訪問時的二義性,不知道是訪問IA中IBase部分的n,還是IB中IBase部分的n
多重繼承中的虛函數(shù)

既然直接使用多重繼承會有如此多的問題,那么我們能不能通過虛函數(shù)來解決這個問題呢?

這里小小的提一下,剛剛二義性里面說到兩個類的距離,對于編譯器來說,一般是找離當(dāng)前的類距離最近的函數(shù)實現(xiàn)來調(diào)用(或者數(shù)據(jù)來訪問),而虛函數(shù)則是讓編譯器做相反的事情:找一個離當(dāng)前類反向距離最遠的函數(shù)實現(xiàn)來調(diào)用。

好,我們先把上面的程序做一點點小改變,把foo()函數(shù)變成一個虛函數(shù),看看有什么變化。

class IBase
{
public:
    virtual ~IBase() {}
    virtual void foo() {}    // 變成虛函數(shù)了
};
編譯,結(jié)果還是失敗。
error C2385: ambiguous access of ‘foo’
could be the ‘foo’ in base ‘IBase’
or could be the ‘foo’ in base ‘IBase’
error C3861: ‘foo’: identifier not found
產(chǎn)生問題的原因依然是二義性。即便換成virtual函數(shù),也不能改變二義性這個問題。為什么呢?
因為我們是用的.運算符來訪問的,而不是用指針,所以這里虛函數(shù)和普通函數(shù)沒有任何區(qū)別。=.=。。。
好,我們再來小小的修改一下,把他變成指針,讓他通過虛表去訪問,看看行不行。
CImpl *p = &o;
p->foo();
編譯,結(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)一次了。
class CImpl : public IA, public IB
{
public:
    virtual ~CImpl() {}
    virtual void foo() { }
};
  
int main(int argc, char* argv[])
{
    CImpl o;
    o.foo();
  
    CImpl *p = &o;
    p->foo();
  
    return 0;
}

編譯一下,通過了!對于o.foo()來說,這當(dāng)然是意料之中,離CImpl距離最近的foo函數(shù)實現(xiàn),就是CImpl自己嘛,當(dāng)然沒有問題。
對于后面這個p->foo()的調(diào)用,編譯器現(xiàn)在也已經(jīng)可以決定對于CImpl這個類來說,離他最遠的foo函數(shù)調(diào)用是誰了——也是他自己。
所以這里就不會產(chǎ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(); }
};
 

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多