国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學(xué)院 > 開發(fā)設(shè)計 > 正文

從C++到.NET 揭開多態(tài)的面紗

2019-11-17 05:13:01
字體:
供稿:網(wǎng)友

  多態(tài)是面向?qū)ο罄碚撝械闹匾拍钪唬瑥亩渤蔀楝F(xiàn)代程序設(shè)計語言的一個主要特性,從應(yīng)用角度來說,多態(tài)是構(gòu)建高靈活性低耦合度的現(xiàn)代應(yīng)用程序架構(gòu)所不可忽缺的能力。從概念的角度來說,多態(tài)使得程序員可以不必關(guān)心某個對象的具體類型,就可以使用這個對象的“某一部分”功能。這個“某一部分”功能可以用基類來呈現(xiàn),也可以用接口來呈現(xiàn)。后者顯得更為重要——接口是使程序具有可擴(kuò)展性的重要特性,而接口的實(shí)現(xiàn)依靠于語言對多態(tài)的實(shí)現(xiàn),或者干脆就象征著語言對多態(tài)的實(shí)現(xiàn)。

  本文并不大算贅述多態(tài)的應(yīng)用,因為其應(yīng)用實(shí)在俯拾皆是,其概念理論也早已完善。這里,我們打算從實(shí)現(xiàn)的角度來看一看一門語言在其多態(tài)特性的背后做了些什么——知其所以然,使用時方能游刃有余。

  或許你在學(xué)習(xí)一門語言的時候,曾經(jīng)對多態(tài)的特性很迷惑,雖然教科書上所講的非常簡單,也非常明了——正如它的原本理念一樣,但是你也想知道語言(編譯器)在背后都干了些什么,為什么一個派生類對象就可以被當(dāng)作其基類對象來使用?用指向派生類對象的基類指針調(diào)用虛函數(shù)時憑什么能夠精確的到達(dá)正確的函數(shù)?類的內(nèi)部是如何布局的?

  我們這樣考慮:假設(shè)語言不支持多態(tài),而我們又必須實(shí)現(xiàn)多態(tài),我們可以怎么做?

  多態(tài)的雛形:

class B
{
 public:
  int flag; //為表示簡潔,0代表基類,1代表派生類
  void f(){cout<<”in B::f()”;} //非虛函數(shù)
};

class D:public B
{
 public:
  void f(){cout<<”in D::f()”;} //非虛函數(shù)
};

void call_virtual(B* pb)
{
 if(pb->flag==0) //假如是基類,則直接調(diào)用f
  pb->f(); //調(diào)用的是基類的f
 else //假如是派生類,則強(qiáng)制轉(zhuǎn)化為派生類指針再調(diào)用f
  (D*)pb->f(); //調(diào)用的是派生類的f
}
  這樣,可以正好符合“根據(jù)具體的對象類型調(diào)用相應(yīng)的函數(shù)”的理念。但是這個原始方案有一些缺點(diǎn):;例如,分發(fā)“虛函數(shù)”的代碼要自己書寫,不夠優(yōu)雅,不具有可擴(kuò)展性(當(dāng)繼續(xù)體系擴(kuò)大時,這堆代碼將變得臃腫無比),不具有封閉性(假如加入了一個新的派生類,則“虛函數(shù)”調(diào)用的代碼必須作改動,然而假如恰巧這個調(diào)用是無法改動的(例如,庫函數(shù)),則意味著,一個用戶加入的派生類將無法兼容于那個庫函數(shù))等等。結(jié)果就是——這個方案不具有通用性。

  但是,這個方案能夠說明一些本質(zhì)性的問題:flag數(shù)據(jù)成員用于標(biāo)識對象所屬的具體類型,從而調(diào)用者可以根據(jù)它來確定到底調(diào)用哪個函數(shù)。但是,可不可以不必“知道”對象的具體類型就能夠調(diào)用正確的函數(shù)呢?可以,改進(jìn)的方案如下:

class B
{
 public:
  void (*f)(); //函數(shù)指針,派生類對象可以通過給它重新賦值來改變對象的行為
};

class D:public B
{};

void call_virtual(B* pb)
{
 (*(pb->f))(); //間接調(diào)用f所指的函數(shù)
}

void B_Mem()
{
 cout<<”I am B”;
}

void D_Mem()
{
 cout<<”I am D”;
}

int main()
{
 B b;
 b.f=&B_Mem; //B_Mem代表B的“虛函數(shù)”
 D d;
 d.f=&D_Mem; //以D_Mem來覆蓋(override)B的虛函數(shù)
 call_virtual(&b); //輸出“I am B”
 call_virtual(&d); //輸出“I am D”
}
  在這個改進(jìn)的例子中,派生類對象可以通過修改函數(shù)指針f的指向,從而獲得特定的行為,這里重要的是,call_virtual函數(shù)不再需要通過丑陋的if-else語句來判定對象的具體類型,而只是簡單的通過一個指針來調(diào)用“虛函數(shù)”——這時候,假如派生類需要改變具體的行為,則可以將相應(yīng)的函數(shù)指針指向它自己的函數(shù)即可,這招“偷梁換柱”通過增加一個間接層的辦法“神不知鬼不覺”地將“虛函數(shù)”替換(Override)掉了。
  然而,這招仍然還有缺點(diǎn)——要用戶手動實(shí)現(xiàn),可擴(kuò)展性差,透明性差等等。然而,它的思想已經(jīng)接近現(xiàn)代編譯器對多態(tài)機(jī)制的實(shí)現(xiàn)手法了。

  通過將上面的例子中的函數(shù)指針擴(kuò)展為一個隱含的指針數(shù)組——虛函數(shù)表(vtbl)——C++擁有了我們現(xiàn)在所看到的多態(tài)能力。在虛函數(shù)表中,每一個虛函數(shù)指針占有一個表項,假如派生類覆蓋(override)了相應(yīng)的虛函數(shù),則對應(yīng)表項就改成指向派生類的那個虛函數(shù)的——這些工作由編譯器完成——從而,如上例所示,用戶不必知曉對象的確切類型,就能夠觸發(fā)其特定的行為(也就是說,調(diào)用“取決于對象具體類型”的成員函數(shù)),虛函數(shù)表對用戶是完全透明的,用戶只需要使用一個virtual要害字就能夠輕松擁有強(qiáng)大的多態(tài)能力。

  假如一個C++類中有虛函數(shù),則該類將會擁有一個虛函數(shù)表(vtbl),并且,該類的對象中(一般在頭部)有一個隱含的指向虛函數(shù)表的指針(vptr)。

  現(xiàn)在假設(shè)有如下代碼:

void f(B* pb)
{
 pb->f1();
}
  則編譯器為該函數(shù)生成的代碼如下(以偽代碼表示,以示明了):

void f(B* pb)
{
 DWord* __vptr=((DWORD*)pb)[0]; //獲得虛函數(shù)表指針
 void (B::*midd_pf)()=__vptr[offsetof_virtual_pf1];
 //從表中獲得相應(yīng)虛函數(shù)指針
 (pb->*midd_pf)(); //調(diào)用虛函數(shù)
}
  這樣一來,假如pb指向的是D對象,則獲得的是指向D::f1的函數(shù)指針(參考上面的第二幅圖),假如pb確實(shí)指向B對象,根據(jù)B對象內(nèi)的vptr所指的虛函數(shù)表,獲得的是指向B::f1的函數(shù)指針。

  現(xiàn)在,關(guān)于C++的多態(tài)機(jī)制基本已經(jīng)明了。剩下的就是多重繼續(xù)下的虛函數(shù)表格局,大同小異,就不多說了。只不過,其中還是有一些微妙的細(xì)節(jié)的,可以參見《Inside C++ Object Model》(Lippman著)(中文名《深入C++對象模型》——侯捷譯)。

  關(guān)于C++虛函數(shù)調(diào)用機(jī)制還有一個細(xì)節(jié)——在構(gòu)造函數(shù)中調(diào)用虛函數(shù)要千萬小心,因為“在構(gòu)造函數(shù)中”意味著“對象還沒有構(gòu)造完畢”,這時候虛函數(shù)調(diào)用機(jī)制很可能還沒有啟動,例如:

class B
{
 B(){this->vf();} //調(diào)用B::vf
 virtual void vf(){cout<<”in B::vf()/n”;
};
  現(xiàn)在,不管B身為哪個類的基類,B的構(gòu)造函數(shù)中調(diào)用的都是B::vf。細(xì)心的讀者會發(fā)現(xiàn):這是由于對象構(gòu)造順序的關(guān)系——C++明確規(guī)定,對象的“大廈”是“自底向上”構(gòu)建的,也就是說,從最底層的基類開始構(gòu)造,所以,在B中調(diào)用this->vf時,雖然this所指的對象確實(shí)(即將)是派生類對象,但是派生類對象的構(gòu)建行為還沒有開始,所以這次調(diào)用不可能跑到派生類的vf函數(shù)去,就似乎第二層樓還沒有建好,一層樓的人是無法跑到二樓去的一樣。

  說得更深一些,虛函數(shù)的調(diào)用是要經(jīng)過虛函數(shù)指針和虛函數(shù)表來間接推導(dǎo)的,在B的構(gòu)造函數(shù)中,編譯器會插入一些代碼,將對象頭部的vptr設(shè)置為指向B的虛函數(shù)表的指針,于是this->vf的推導(dǎo)使用的是B的虛函數(shù)表,當(dāng)然只能跑到B的vf那兒去。而后來,當(dāng)B構(gòu)建完畢,輪到派生類對象部分構(gòu)造時,派生類的構(gòu)造函數(shù)會將對象頭部的vptr改成指向派生類的虛函數(shù)表的指針,這時候虛函數(shù)調(diào)用機(jī)制才算是Enable了,以后的this->vf將使用派生類虛函數(shù)表來推導(dǎo),從而到達(dá)正確的函數(shù)。 QQread.com 推出Windows2003教程 win2003安裝介紹 win2003網(wǎng)絡(luò)優(yōu)化 win2003使用技巧 win2003系統(tǒng)故障 服務(wù)器配置 專家答疑
更多的請看:http://www.qqread.com/windows/2003/index.Html.NET 對象模型

  C++對象模型與.NET(或java)有個主要的區(qū)別——C++支持多重繼續(xù),不支持接口,而.NET(或Java)支持接口,不支持多重繼續(xù)。

  而.NET的虛函數(shù)調(diào)用機(jī)制與C++也比較相似,只不過由于接口和JIT(即時編譯)的介入而有一些不同。

  在.NET中,每一個類都有一個對應(yīng)的函數(shù)指針表(事實(shí)上,這個“表”是個數(shù)據(jù)結(jié)構(gòu),里面還有其它信息),與C++不同的是,該類的每個函數(shù)(不管是不是虛函數(shù))都在其中對應(yīng)一個表項。這是由于JIT(即時編譯)的需要——對每個函數(shù)的調(diào)用都是間接的,都會經(jīng)過該表推導(dǎo)一次,獲得函數(shù)代碼的地址。注重,第一次調(diào)用的時候,函數(shù)代碼還是中間代碼(.NET的中間語言MISL的代碼),所以將會跳至即時編譯器,編譯這些代碼并放到內(nèi)存中,然后將表中的對應(yīng)表項指向編譯后的native code,以后的每次調(diào)用都會直接跳到編譯后的代碼。

  以上只是想讓你對.NET的“虛函數(shù)表”有個大體的熟悉。下面就來具體剖析。

  假如沒有接口,.NET的虛函數(shù)調(diào)用機(jī)制將是很單純的——幾乎與C++一樣。只不過,接口加入以后就不同了——可以將對象引用轉(zhuǎn)化為接口引用,然后再調(diào)用接口中的虛函數(shù)。所以,勢必要對“虛函數(shù)表”作某種改動,例如,對于下面的繼續(xù)結(jié)構(gòu):

public interface IFirst
{
 void f1();
 void f2();
}

public interface ISecond
{
 void s1();
}

public class C:IFirst,Isecond
{
 public override void f1(){}
 public override void f2(){}
 public override void s1(){}
 public virtual void c1(){}
}
  類型C的內(nèi)存布局大體是這樣的(由于.NET是單根的繼續(xù)結(jié)構(gòu),每個類都隱式的繼續(xù)自O(shè)bject,所以,類型C的“虛函數(shù)表”中包含Object的所有成員函數(shù))

  ObjRef指向一個對象,在對象頂部(除了用于同步的sync#塊之外)是hType(可以看成對應(yīng)于C++對象頂部的虛函數(shù)表指針),它所指的結(jié)構(gòu)(CORINFO_CLASS_STRUCT,可以暫時將它看成虛函數(shù)表,盡管其中包含的信息不僅僅是虛函數(shù)指針)包含在C++中相當(dāng)于虛函數(shù)表的部分,以及用于對象的運(yùn)行時識別的信息。不同的是,在基于接口的.NET繼續(xù)風(fēng)格中,對接口的虛函數(shù)的分派是基于一個IOT(Interface Offset Table,即接口偏移表),圖中的pIOT就是指向這樣一個表,其中每一項都是一個偏移量,反指向該接口中的虛函數(shù)指針數(shù)組在CORINFO_CLASS_STRUCT中的位置。

  這樣,當(dāng)基于接口的引用調(diào)用虛函數(shù)時,其背后的機(jī)制是:先根據(jù)接口引用取得該類所對應(yīng)的CORINFO_CLASS_STRUCT結(jié)構(gòu)的地址,然后在pIOT所指的接口偏移表中索引相應(yīng)的虛函數(shù)指針數(shù)組的偏移量,最后經(jīng)過指針間接調(diào)用虛函數(shù)。 可以看出,基于接口引用調(diào)用虛函數(shù)時要經(jīng)過兩個間接層,第一,在IOT中索引對應(yīng)接口的虛函數(shù)指針數(shù)組的偏移量,第二,在虛函數(shù)指針數(shù)組中索引相應(yīng)的虛函數(shù)指針,最后才是調(diào)用。但是,當(dāng)基于對象引用調(diào)用虛函數(shù)時,只要經(jīng)過一個間接層——就像在C++中一樣——直接在虛函數(shù)表中索引對應(yīng)虛函數(shù)指針,接著調(diào)用。

  關(guān)于基于接口的引用調(diào)用虛函數(shù),還有一個細(xì)節(jié)就是,IOT里為每一個接口都預(yù)備了一個表項(包括該類并沒有實(shí)現(xiàn)的接口),原因是效率——.NET需要每個接口在IOT里都有一個固定的(或者說,編譯期確定的)偏移量,這樣,在為虛函數(shù)調(diào)用生成代碼的時候才能夠通過這個固定的偏移去查找某個接口的虛函數(shù)指針數(shù)組的所在。 另一方面,假如某個類的IOT僅僅包含它實(shí)現(xiàn)的接口,則經(jīng)由接口引用去調(diào)用虛函數(shù)時,必須先知道該接口在IOT中的相應(yīng)偏移,而這一信息必須通過運(yùn)行期的動態(tài)查詢才能夠知道(因為編譯器在手頭只有一個接口引用的情況下不可能知道它指向的是哪個類對象,從而也就不知道該類到底實(shí)現(xiàn)了哪些接口,所以要求助于運(yùn)行期的動態(tài)查詢,而在前面所說的方式(也就是.NET所用的方式)下,編譯器不用知道接口引用到底指向哪個類對象,因為在每個類的CORINFO_CLASS_STRUCT中的固定位置都有一個pIOT,指向一個IOT,其中每個接口都對應(yīng)一個固定的(編譯器知道的)表項)——顯然,在每次調(diào)用虛函數(shù)之前都進(jìn)行一次動態(tài)查詢是不可容忍的效率損傷,所以.NET寧可讓IOT多一些表項,以空間換時間。

  或許你認(rèn)為這過于復(fù)雜,但是這是必須的,.NET中的基于接口的繼續(xù)對應(yīng)于C++中的多重繼續(xù),后者的實(shí)現(xiàn)也有類似的復(fù)雜性——或許更復(fù)雜一些。

  最后,要說明的是,本文對于一個純粹的實(shí)用者或許顯得多余,但是對于想把一門語言使用得更好的人卻是有用的。知其然而知其所以然,才能夠游刃有余。而其實(shí)現(xiàn)機(jī)理在實(shí)際運(yùn)用中能起到拋磚引玉的作用也未可知。

發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 长兴县| 永修县| 博白县| 彩票| 东丰县| 彭阳县| 汤阴县| 张家川| 武穴市| 榆社县| 平凉市| 修水县| 汤阴县| 分宜县| 闻喜县| 比如县| 石棉县| 南平市| 芦山县| 梁山县| 调兵山市| 林西县| 鸡西市| 丰台区| 黄梅县| 石阡县| 临汾市| 南华县| 潍坊市| 东方市| 晋州市| 和平县| 三亚市| 建始县| 江西省| 阳泉市| 南投县| 兰考县| 黄龙县| 巩义市| 乌拉特中旗|