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

首頁(yè) > 學(xué)院 > 開(kāi)發(fā)設(shè)計(jì) > 正文

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

2019-11-17 05:23:34
字體:
來(lái)源:轉(zhuǎn)載
供稿:網(wǎng)友

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

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

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

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

  多態(tài)的雛形:

class B
{
 public:
  int flag; //為表示簡(jiǎn)潔,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ù)具體的對(duì)象類型調(diào)用相應(yīng)的函數(shù)”的理念。但是這個(gè)原始方案有一些缺點(diǎn):;例如,分發(fā)“虛函數(shù)”的代碼要自己書寫,不夠優(yōu)雅,不具有可擴(kuò)展性(當(dāng)繼續(xù)體系擴(kuò)大時(shí),這堆代碼將變得臃腫無(wú)比),不具有封閉性(假如加入了一個(gè)新的派生類,則“虛函數(shù)”調(diào)用的代碼必須作改動(dòng),然而假如恰巧這個(gè)調(diào)用是無(wú)法改動(dòng)的(例如,庫(kù)函數(shù)),則意味著,一個(gè)用戶加入的派生類將無(wú)法兼容于那個(gè)庫(kù)函數(shù))等等。結(jié)果就是——這個(gè)方案不具有通用性。

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

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

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

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

  假如一個(gè)C++類中有虛函數(shù),則該類將會(huì)擁有一個(gè)虛函數(shù)表(vtbl),并且,該類的對(duì)象中(一般在頭部)有一個(gè)隱含的指向虛函數(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ù)
}
  這樣一來(lái),假如pb指向的是D對(duì)象,則獲得的是指向D::f1的函數(shù)指針(參考上面的第二幅圖),假如pb確實(shí)指向B對(duì)象,根據(jù)B對(duì)象內(nèi)的vptr所指的虛函數(shù)表,獲得的是指向B::f1的函數(shù)指針。

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

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

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

  說(shuō)得更深一些,虛函數(shù)的調(diào)用是要經(jīng)過(guò)虛函數(shù)指針和虛函數(shù)表來(lái)間接推導(dǎo)的,在B的構(gòu)造函數(shù)中,編譯器會(huì)插入一些代碼,將對(duì)象頭部的vptr設(shè)置為指向B的虛函數(shù)表的指針,于是this->vf的推導(dǎo)使用的是B的虛函數(shù)表,當(dāng)然只能跑到B的vf那兒去。而后來(lái),當(dāng)B構(gòu)建完畢,輪到派生類對(duì)象部分構(gòu)造時(shí),派生類的構(gòu)造函數(shù)會(huì)將對(duì)象頭部的vptr改成指向派生類的虛函數(shù)表的指針,這時(shí)候虛函數(shù)調(diào)用機(jī)制才算是Enable了,以后的this->vf將使用派生類虛函數(shù)表來(lái)推導(dǎo),從而到達(dá)正確的函數(shù)。 photoshop教程 數(shù)據(jù)結(jié)構(gòu) 五筆輸入法專題 QQ病毒專題 共享上網(wǎng)專題 Google工具和服務(wù)專題
  .NET 對(duì)象模型

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

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

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

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

  假如沒(méi)有接口,.NET的虛函數(shù)調(diào)用機(jī)制將是很單純的——幾乎與C++一樣。只不過(guò),接口加入以后就不同了——可以將對(duì)象引用轉(zhuǎn)化為接口引用,然后再調(diào)用接口中的虛函數(shù)。所以,勢(shì)必要對(duì)“虛函數(shù)表”作某種改動(dòng),例如,對(duì)于下面的繼續(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),每個(gè)類都隱式的繼續(xù)自O(shè)bject,所以,類型C的“虛函數(shù)表”中包含Object的所有成員函數(shù))

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

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

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

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

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

發(fā)表評(píng)論 共有條評(píng)論
用戶名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 武陟县| 徐水县| 北宁市| 曲靖市| 建昌县| 鹤峰县| 鄄城县| 阜阳市| 疏勒县| 德安县| 恭城| 关岭| 巩留县| 湟中县| 阳谷县| 松滋市| 年辖:市辖区| 仁寿县| 嘉兴市| 贺兰县| 黄浦区| 仁化县| 保德县| 团风县| 新沂市| 嵊州市| 阜新| 通化市| 兴仁县| 个旧市| 武穴市| 江西省| 聂拉木县| 长宁区| 中西区| 天门市| 宁远县| 普格县| 伊通| 宁津县| 河源市|