99热99这里只有精品6国产,亚洲中文字幕在线天天更新,在线观看亚洲精品国产福利片 ,久久久久综合网

歡迎加入QQ討論群258996829
麥子學(xué)院 頭像
蘋果6袋
6
麥子學(xué)院

C++ 虛函數(shù)和虛繼承如何使用?

發(fā)布時(shí)間:2016-10-26 22:39  回復(fù):0  查看:2791   最后回復(fù):2016-10-26 22:39  

本文針對(duì)C++里的虛函數(shù),虛繼承表現(xiàn)和原理進(jìn)行一些簡(jiǎn)單分析,有希望對(duì)大家學(xué)習(xí)C++有所幫助。下面都是以VC2008編譯器對(duì)這兩種機(jī)制內(nèi)部實(shí)現(xiàn)為例。

  虛函數(shù)

  以下是百度百科對(duì)于虛函數(shù)的解釋:

  定義:在某基類中聲明為 virtual 并在一個(gè)或多個(gè)派生類中被重新定 義的成員函數(shù)[1]

  語法:virtual 函數(shù)返回類型 函數(shù)名(參數(shù)表) 函數(shù)體 }

  用途實(shí)現(xiàn)多態(tài)性,通過指向派生類的基類指針,訪問派生類中同名覆蓋成員函數(shù)

  函數(shù)聲明和定義和普通的類成員函數(shù)一樣,只是在返回值之前加入了關(guān)鍵字“virtual”聲明為虛函數(shù)。而虛函數(shù)是實(shí)現(xiàn)多態(tài)的重要手段,意思是只有對(duì)虛函數(shù)的調(diào)用才能動(dòng)態(tài)決定調(diào)用哪一個(gè)函數(shù),這是相對(duì)于普通成員函數(shù)而言的,普通的成員函數(shù)在編譯階段就能確定調(diào)用哪一個(gè)函數(shù)。舉個(gè)栗子:

  #include

  class A {public:

  void fn() { printf("fn in A\\n"); }

  virtual void v_fn() { printf("virtual fn in A\\n"); }

  };

  class B : public A {public:

  void fn() { printf("fn in B\\n"); }

  virtual void v_fn() { printf("virtual fn in B\\n"); }

  };

  int main() {

  A *a = new B();

  a->fn();

  a->v_fn();

  return 0;

  }

  基類A有兩個(gè)成員函數(shù)fnv_fn,派生類B繼承自基類A,同樣實(shí)現(xiàn)了兩個(gè)函數(shù),然后在main函數(shù)中用A的指針指向B的實(shí)例(向上轉(zhuǎn)型,也是實(shí)現(xiàn)多態(tài)的必要手段),然后分別調(diào)用fnv_fn函數(shù)。結(jié)果是“fn in A””virtual fn in B”。這是因?yàn)?/span>fn是普通成員函數(shù),它是通過類A的指針調(diào)用的,所以在編譯的時(shí)候就確定了調(diào)用Afn函數(shù)。而v_fn是虛函數(shù),編譯時(shí)不能確定,而是在運(yùn)行時(shí)再通過一些機(jī)制來調(diào)用指針?biāo)赶虻膶?shí)例(B的實(shí)例)中的v_fn函數(shù)。假如派生類B中沒有實(shí)現(xiàn)(完全一樣,不是重載)v_fn這個(gè)函數(shù),那么依然會(huì)調(diào)用基類類A中的v_fn;如果它實(shí)現(xiàn)了,就可以說派生類B覆蓋了基類A中的v_fn這個(gè)虛函數(shù)。這就是虛函數(shù)的表現(xiàn)和使用,只有通過虛函數(shù),才能實(shí)現(xiàn)面向?qū)ο笳Z言中的多態(tài)性。

  以上只是虛函數(shù)的表現(xiàn)和用途,下面來探討它的實(shí)現(xiàn)機(jī)制。在此之前,先來看一個(gè)問題,還是以上的代碼,基類A的大小為多少,也就是“printf(“%d\\n”, sizeof(A));”的輸出會(huì)是多少呢?A中一個(gè)成員變量都沒有,有人可能會(huì)說是0。額,0是絕對(duì)錯(cuò)誤的,因?yàn)樵?/span>C++中,即時(shí)是空類,它的大小也為1,這是另外的話題,不在本文討論。當(dāng)然1也是不對(duì)的,實(shí)際結(jié)果是432位系統(tǒng)),4剛好是一個(gè)int,一個(gè)指針(32位)的大小,派生類B的大小同樣為4。這四個(gè)字節(jié)和實(shí)現(xiàn)多態(tài),虛函數(shù)的機(jī)制有著很重要的關(guān)系。

  其實(shí)用VC2008調(diào)試上面代碼的時(shí)候,就會(huì)發(fā)現(xiàn)指針a所指向的實(shí)力中有一個(gè)成員常量(const),它的名字叫做vftable,全稱大概叫做virtual function table(虛函數(shù)表)。它實(shí)際指向了一個(gè)數(shù)組,數(shù)組里面保存的是一系列函數(shù)指針,而上面的程序中,這個(gè)表只有一項(xiàng),它就是派生類B中的v_fn函數(shù)入口地址。假如我們用一個(gè)A的指針指向一個(gè)A的實(shí)例呢?它同樣有一個(gè)vftable,而它指向的表中也只有一項(xiàng),這項(xiàng)保存的基類的v_fn函數(shù)入口地址。這用代碼表示,就類似于下面這樣:

  void* vftable_of_A[] = {

  A::v_fn,

  ...

  };

  class A {

  const void* vftable = vftable_of_A;

  virtual void v_fn() {}

  };

  void* vftable_of_B[] = {

  B::v_fn,

  ...

  };

  class B {

  const void *vftable = vftable_of_B;

  vritual void v_fn() {}

  };

  上面vftable的類型之所以用void*表示,實(shí)際上一個(gè)類中所有虛函數(shù)的地址都被放到這個(gè)表中,不同虛函數(shù)對(duì)應(yīng)的函數(shù)指針類型不盡相同,所以這個(gè)表用C++的類型不好表述,但是在機(jī)器級(jí)里都是入口地址,即一個(gè)32位的數(shù)字(32位系統(tǒng)),等到調(diào)用時(shí),因?yàn)榫幾g器預(yù)先知道了函數(shù)的參數(shù)類型,返回值等,可以自動(dòng)做好處理。

  這樣我們就能更好的理解虛函數(shù)和多態(tài)了。第一個(gè)代碼中,a指針雖然是A*類型的,但是它卻調(diào)用了B中的v_fn,因?yàn)椴还苁?/span>A類,還是A的基類,都會(huì)有一個(gè)變量vftable,它指向的虛函數(shù)表中保存了正確的v_fn入口。所以a->v_fn()實(shí)際做的工作就是從a指向的實(shí)例中取出vftable的值,然后找到虛函數(shù)表,再從表中去的v_fn的入口,進(jìn)行調(diào)用。不管a是指向A的實(shí)例,還是指向B的實(shí)例,a->fn()所做的步驟都是上面說的一樣,只是A的實(shí)例和B的實(shí)例有著不同的虛函數(shù)表,虛函數(shù)表里也保存著可能不同的虛函數(shù)入口,所以最終將進(jìn)入不同的函數(shù)調(diào)用中。通過表來達(dá)到不用判斷類型,亦可實(shí)現(xiàn)多態(tài)的作用。還有一點(diǎn)指的提醒的是,因?yàn)樘摵瘮?shù)表是一個(gè)常量表,在編譯時(shí),編譯器會(huì)自動(dòng)生成,并且不會(huì)改變,所以如果有多個(gè)B類的實(shí)例,每個(gè)實(shí)例中都會(huì)有一個(gè)vftable指針,但是它們指向的是同一個(gè)虛函數(shù)表。

  上面一段中說到了,AB的實(shí)例有著不同的虛函數(shù)表,但是虛函數(shù)表中只是可能保存著不同的v_fn,那是因?yàn)?/span>C++允許派生類不覆蓋基類中的虛函數(shù),意思就是假如派生類B中沒有實(shí)現(xiàn)v_fn這個(gè)函數(shù)(不是重載),那么B的實(shí)例的虛函數(shù)表會(huì)保存著基類Av_fn的入口地址。也就是說B類不實(shí)現(xiàn)v_fn函數(shù),但是它同樣提供了這個(gè)接口,實(shí)際上是調(diào)用基類A中的v_fn。假如某個(gè)類只是一個(gè)抽象類,抽象出一些列接口,但是又不能實(shí)現(xiàn)這些接口,而要有派生類來實(shí)現(xiàn),那么就可以把這些接口聲明為純虛函數(shù),包含有純虛函數(shù)的類稱為抽象類。純虛函數(shù)是一類特殊的虛函數(shù),它的聲明方式如下:

  class A {

  public:

  virtual 返回值 函數(shù)名(參數(shù)表)= 0

  };

  在虛函數(shù)聲明方式后加一個(gè)“=0”,并且不提供實(shí)現(xiàn)。抽象類不允許實(shí)例化(這樣做編譯器會(huì)報(bào)錯(cuò),因?yàn)橛谐蓡T函數(shù)沒有實(shí)現(xiàn),編譯器不知道怎么調(diào)用)。純虛函數(shù)的實(shí)現(xiàn)機(jī)制和虛函數(shù)類似,只是要求派生類類必須自己實(shí)現(xiàn)一個(gè)(也可以不實(shí)現(xiàn),但是派生類也會(huì)是個(gè)抽象類,不能實(shí)例化)。

  順帶提一下,java中的每一個(gè)成員函數(shù)都可以以理解為C++中的virtual函數(shù),不用顯式聲明都可以實(shí)現(xiàn)重載,多態(tài)。而java的接口類似于C++中的抽象類,需要實(shí)現(xiàn)里面的接口。

  虛繼承

  C++支持多重繼承,這和現(xiàn)實(shí)生活很類似,任何一個(gè)物體都不可能單一的屬于某一個(gè)類型。就像馬,第一想到的就是它派生自動(dòng)物這個(gè)基類,但是它在某系地方可不可以說也派生自交通工具這一個(gè)基類呢?所以C++的多重繼承很有用,但是又引入了一個(gè)問題(專業(yè)術(shù)語叫做菱形繼承?)。動(dòng)物和交通工具都是從最根本的基類——“事物繼承而來,事物包含了兩個(gè)最基本的屬性,體積和質(zhì)量。那么動(dòng)物和交通工具都保存了基類成員變量——體積和質(zhì)量的副本。而馬有繼承了這兩個(gè)類,那么馬就有兩份體積和質(zhì)量,這是不合理的,編譯器無法確定使用哪一個(gè),所以就會(huì)報(bào)錯(cuò)。JAVA中不存在這樣的問題,因?yàn)?/span>JAVA不允許多重繼承,它只可能實(shí)現(xiàn)多個(gè)接口,而接口里面只包含一些函數(shù)聲明不包含成員變量,所以也不存在這樣的問題。

  這個(gè)問題用具體代碼表述如下所示:

  class A {public:

  int a;

  };

  class B : public A {

  };

  class C : public A {

  };

  class D : public B, public C {

  };

  int main() {

  D d;

  d.a = 1;

  return 0;

  }

  這個(gè)代碼會(huì)報(bào)錯(cuò),因?yàn)?/span>d中保存了兩份A的副本,即有兩個(gè)成員變量a,一般不會(huì)報(bào)錯(cuò),但是一旦對(duì)D中的a使用,就會(huì)報(bào)一個(gè)對(duì)a的訪問不明確。虛繼承就可以解決這個(gè)問題。在探討虛函數(shù)之前,先來一個(gè)sizeof的問題。

  #include

  class A {public:

  int a;

  };

  class B : virtual public A {

  };

  int main() {

  printf("%d\\n", sizeof(B));

  return 0;

  }

  B的大小是?首先回答0的是絕對(duì)錯(cuò)的,理由我之前都說了。1也是錯(cuò)的,不解釋。4也是錯(cuò)的,如果B不是虛繼承自A的,那么4就是對(duì)的。正確答案是8,B虛繼承A了之后,比預(yù)想中的多了4個(gè)字節(jié),這是怎么回事呢?這個(gè)通過調(diào)試是看不出來的,因?yàn)榭床坏筋愃朴?/span>vftable的成員變量(實(shí)際上編譯器生成了一個(gè)類似的東西,但是調(diào)試時(shí)看不到,但是在觀察反匯編的時(shí)候,可以見到vbtable的字樣,應(yīng)該是virtual base table的意思)。

  虛繼承的提出就是為了解決多重繼承時(shí),可能會(huì)保存兩份副本的問題,也就是說用了虛繼承就只保留了一份副本,但是這個(gè)副本是被多重繼承的基類所共享的,該怎么實(shí)現(xiàn)這個(gè)機(jī)制呢?編譯器會(huì)在派生類B的實(shí)例中保存一個(gè)A的實(shí)例,然后在B中加入一個(gè)變量,這個(gè)變量是A的實(shí)例在實(shí)際B實(shí)例中的偏移量,實(shí)際上B中并不直接保存offset的值,而是保存的一個(gè)指針,這個(gè)指針指向一個(gè)表vbtable,vbtable表中保存著所有虛繼承的基類在實(shí)例中的offset值,多個(gè)B的實(shí)例共享這個(gè)表,每個(gè)實(shí)例有個(gè)單獨(dú)的指針指向這個(gè)表,這樣就很好理解為什么多了4個(gè)字節(jié)了。用代碼表示就像下面這樣。

  class A {public:

  ...

  };

  int vbtable_of_B[] = {

  offset(B::_a),

  ...

  };

  class B virtual public A{private:

  const int* vbtable = vbtable_of_B;

  A _a;

  };

  每一個(gè)A的虛派生類,都會(huì)有自己的vbtable表,這個(gè)派生類的所有實(shí)例共享這個(gè)表,然后每個(gè)實(shí)例各自保存了一個(gè)指向vbtable表的指針。假如還有一個(gè)類C虛繼承了A,那么編譯器就會(huì)為它自動(dòng)生成一個(gè)vbtable_of_C的表,然后C的實(shí)例都會(huì)有一個(gè)指向這個(gè)vbtable表的指針。

  假如有多級(jí)的虛繼承會(huì)發(fā)生什么情況,就像下面這段代碼一樣:

  #include

  class A {public:

  int a;

  };

  class B : virtual public A {public:

  int b;

  };

  class C : virtual public B {

  };

  int main() {

  printf("%d\\n", sizeof(C));

  return 0;

  }

  程序運(yùn)行的結(jié)果是16,按照之前的理論,大概會(huì)這么想。基類A里有1個(gè)變量,4個(gè)字節(jié)。B類虛繼承了A,所以它有一個(gè)A的副本和一個(gè)vbtable,還有自己的一個(gè)變量,那就是12字節(jié)。然后C類又虛繼承了B類,那么它有一個(gè)B的副本,一個(gè)vbtable,16字節(jié)。但實(shí)際上通過調(diào)試和反匯編發(fā)現(xiàn),C中保存分別保存了AB的副本(不包括B類的vbtable),8字節(jié)。然后有一個(gè)vbtable指針,4字節(jié),表里面包含了A副本和B副本的偏移量。最后還有一個(gè)無用的4字節(jié)(?),一共16字節(jié)。不僅是這樣,每經(jīng)過一層的虛繼承,便會(huì)多出4字節(jié)。這個(gè)多出來的四字節(jié)在反匯編中沒發(fā)現(xiàn)實(shí)際用途,所以這個(gè)有待探討,不管是編譯器不夠智能,還是有待其它作用,虛繼承和多重繼承都應(yīng)該謹(jǐn)慎使用。

  還是以上面的例子,假如C類是直接繼承B類,而不是使用虛繼承,那么C類的大小為12字節(jié)。它里面是直接保存了AB的副本(不包含Bvbtable),然后還有一個(gè)自己的vbtable指針,所以一共12字節(jié),沒有了上一段所說的最后的4個(gè)字節(jié)。

  但是如果想下面一種繼承,會(huì)是什么情況?

  #include

  class A {public:

  int a;

  };

  class B : virtual public A {

  };

  class C : virtual public A {

  };

  class D : public B, public C{

  };

  int main() {

  printf("%d\\n", sizeof(D));

  return 0;

  }

  DB,C類派生出來,而BC又同時(shí)虛繼承了A。輸出的結(jié)構(gòu)是12,實(shí)際調(diào)試反匯編的時(shí)候發(fā)現(xiàn),D中繼承了BCvbtable,這就是8字節(jié),而同時(shí)還保存了一個(gè)A的副本,4字節(jié),總共12字節(jié)。它和上面的多重虛繼承例子里的12字節(jié)是不一樣的。之前一個(gè)例子中只有一個(gè)vbtable,一個(gè)A的實(shí)例,末尾還有一個(gè)未知的4字節(jié)。而這個(gè)例子中是有兩個(gè)僅挨著的vbtable(都有效)和一個(gè)A的實(shí)例。

 

文章來源:碼農(nóng)網(wǎng)

您還未登錄,請(qǐng)先登錄

熱門帖子

最新帖子

?