虛方法的呼叫是怎麼實現的 單繼承VS多繼承

2021-09-08 09:40:09 字數 4280 閱讀 3500

我們知道通過乙個指向之類的父類指標可以呼叫子類的虛方法,因為子類的方法會覆蓋父類同樣的方法,通過這個指標可以找到物件例項的位址,通過例項的位址可以找到指向對應方法表的指標,而通過這個方法的名字就可以確定這個方法在方法表中的位置,直接呼叫就行,在多繼承的時候,乙個類可能有多個方法表,也就有多個指向這些方法表的指標,乙個類有多個父類,怎麼通過其中乙個父類的指標呼叫之類的虛方法?

其實前面幾句話並沒有真正說清楚,在單繼承中,父類是怎麼呼叫子類的虛方法的,還有多繼承又是怎麼實現這點的,想知道這些,請認真往下看。

我們先看單繼承是怎麼實現的。先上兩個簡單的類:

#include using

namespace

std;

class

a

virtual ~a(){}

virtual

void

geta()

void seta(int

_a)

inta;

};class b:public

a

virtual ~b(){}

virtual

void

geta()

virtual

void

getb()

private

:

intb;

};typedef

int (*fun)(void

);void

testa()

cout

<

b b ;

a* b1=&b;

cout

<

類b的虛方法(第0個是b的析構函式)通過類b的例項:

"<

int** pvtab1 = (int**)&b;

for (int i=1; (fun)pvtab1[0][i]!=null; i++)

cout

<

cout

<

類b的虛方法(第0個是b的析構函式)通過類a的指標:

"<

int** pvtab2 = (int**)&*b1;

for (int i=1; (fun)pvtab2[0][i]!=null; i++)

cout

<

cout

<

"<

cout

執行結果如下:

通過執行結果我們知道:通過父類指向子類的指標呼叫的是子類的虛方法。在單一繼承中,雖然父類有父類的虛方法表,子類有子類的虛方法表,但是子類並沒有指向父類虛方法的指標,在子類的例項中,子類和父類是公用乙個虛方法表,當然只有乙個指向方法表的指標,為什麼可以公用乙個虛方法表呢,虛方法表的第乙個方法是析構函式,子類的方法會覆蓋父類的同樣的方法,子類新增的虛方法放在虛方法表的後面,也就是說子類的虛方法表完全覆蓋父類的虛方法表,即子類的每個虛方法與父類對應的虛方法,在各種的方法表中的索引是一樣的。

但是在多繼承中就不是這樣了,第乙個被繼承的類使用起來跟單繼承是完全一樣的,但是後面被繼承的類就不是這樣了,且仔細往下看。

還是先上3個簡單的類

#include using

namespace

std;

class

a

virtual ~a(){}

virtual

void

geta()

inta;

};class

b

virtual ~b(){}

virtual

void

sb()

virtual

void

getb()

private

:

intb;

};class c:public a,public

b

virtual ~c(){}

virtual

void getb()//

覆蓋類b的同名方法

virtual

void

getc()

virtual

void

justc()

private

:

intc;

};typedef

int (*fun)(void

);void

testc()

cout

cout

<

類c的虛方法(第0個是c的析構函式)(通過b型別的指標):

"<

pvtab1 = (int**)&*b;

for (int i=1; (fun)pvtab1[0][i]!=null; i++)

}

執行結果如下:

從結果說話:

sizeof(c)=20,我們並不意外,在單繼承的時候,父類和子類是公用乙個指向虛方法表的指標,在多繼承中,同樣第乙個父類和子類公用這個指標,而從第二個父類開始就有自己單獨的指標,其實就是父類的例項在子類的記憶體中保持完整的結構,也就是說在多重繼承中,之類的例項就是每乙個父類的例項拼接而成的,當然可能因為繼承的複雜性,會加一些輔助的指標。

指標a與指標c指向同乙個位址,即c的首位址,而b所指的位址與a所指的位址相差8位元組剛好就是類a例項的大小,也就是說在c的記憶體布局中,先存放了a的例項,在存放b的例項,sizeof(b)=8(欄位int b和指向b虛方法表的指標),在家上c自己的字段int c剛好是20位元組。

讓我有點意外的是:方法b::sb,c::getb並沒有出現在類c的方法表中,而且c::getb是c覆寫b中的getb方法,怎麼沒有出現在c的方法表中呢?在《深入探索c++物件模型》一書中講到,這兩個方法同時應該出現在c的方法表中,同樣也會覆蓋b的虛方法表。可能是不通的編譯器有不同的實現,我用的是vs2010,那本書上講的是編譯器cfront

ok,我們不用管不同的編譯器實現上的區別,這點小區別無傷大雅,虛方法的呼叫機制還是一樣的。

先來分析幾個小例子,看看虛方法的實現機制。

c* c=new c();

a* a=c;

a->geta();

c->geta();

c->getc();

a->geta()   ->   (a->vptr1[1])(a);   // geta在方法表中的索引是1

c->geta()  ->  (c->vptr1[1])(c);   // geta在方法表中的索引是1

c->getc()   ->   (a->vptr1[2])(c);   // getc在方法表中的索引是2

vptr1表示指向類c第乙個方法表的指標,這個指標實際的名字會複雜一些,暫且將指向類c的第乙個方法表的指標命名為vptr2,下面會用到這個指標。

再來分析幾行**:

b* b=c;

c->getb();

b->getb();

b* b=c+sizeof(a);

c所指的位址加上a的大小,剛好是b所指的位址。

c->getb();同樣需要轉換,因為方法getb根本不在c所指的那個方法表中,可能轉換成這個樣子(實際轉換成啥樣子我真不知道):

this=c+sizeof(a);

(this->vptr2[2])(c);

如果像編譯器cfront所說的那樣,方法getb在vptr1所指的方法表中,那麼就不用產生調整this指標了,如果在vptr1所指的方法表中,就讓方法表變大了,且跟別的方法表是重複的。

b->getb();就不需要做過多的轉換了,因為b正好指向vptr2,可能轉換成下面這個樣子:

b->getb()   ->   (b->vptr2[2])(b);   // getb在方法表中的索引是2

總之指標所指的方法表如果沒有要呼叫的方法,就要做調整,虛方法需要通過方法表呼叫,相對於非虛方法,效能就慢那麼一點點,這也是別人常說的c++效能不如c的其中一點。

虛多繼承就更麻煩了,不熟悉可能就會被坑。《深入探索c++物件模型》這本書是這樣建議的:不要在乙個virtual base class中宣告nonstatic data members,如果這樣做,你會距複雜的深淵越來越近,終不可拔。

virtual base class還是當做介面來用吧。

虛方法的呼叫是怎麼實現的 單繼承VS多繼承

我們知道通過乙個指向之類的父類指標可以呼叫子類的虛方法,因為子類的方法會覆蓋父類同樣的方法,通過這個指標可以找到物件例項的位址,通過例項的位址可以找到指向對應方法表的指標,而通過這個方法的名字就可以確定這個方法在方法表中的位置,直接呼叫就行,在多繼承的時候,乙個類可能有多個方法表,也就有多個指向這些...

虛繼承之單繼承的記憶體布局

c 2.0以後全面支援虛函式與虛繼承,這兩個特性的引入為c 增強了不少功能,也引入了不少煩惱。虛函式與虛繼承有哪些特性,今天就不記錄了,如果能搞了解一下編譯器是如何實現虛函式和虛繼承,它們在類的記憶體空間中又是如何布局的,卻可以對c 的了解深入不少。這段時間花了一些時間了解這些玩意,搞得偶都,不過總...

虛繼承之單繼承的記憶體布局

c 2.0以後全面支援虛函式與虛繼承,這兩個特性的引入為c 增強了不少功能,也引入了不少煩惱。虛函式與虛繼承有哪些特性,今天就不記錄了,如果能搞了解一下編譯器是如何實現虛函式和虛繼承,它們在類的記憶體空間中又是如何布局的,卻可以對c 的了解深入不少。這段時間花了一些時間了解這些玩意,搞得偶都 先看一...