編譯器對(duì)于多態(tài)的實(shí)現(xiàn)是怎樣的呢?下面請(qǐng)看一個(gè)例子:
view plaincopy to clipboardprint?Class Point { Public: Virtual void print(); …… }; Class Point2D : public Point { Public: Virtual void print(); … }; (實(shí)現(xiàn)部分略) Point2D pt2d; Point *pt = &pt2d; Pt->print(); //這里的多態(tài)要求是要調(diào)用Point2D:: print( ); Class Point { Public: Virtual void print(); …… }; Class Point2D : public Point { Public: Virtual void print(); … }; (實(shí)現(xiàn)部分略) Point2D pt2d; Point *pt = &pt2d; Pt->print(); //這里的多態(tài)要求是要調(diào)用Point2D:: print( );
編譯器會(huì)怎么做呢?用上一篇筆記里面的name mangling是不行的。
當(dāng)然,在這個(gè)例子里面,如果你編譯的時(shí)候用優(yōu)化選項(xiàng),編譯器也許會(huì)把上面三條語句優(yōu)化如下:Point2D pt2d; pt2d.print( ); !?。∧阋苍S會(huì)驚訝:編譯器這么牛?!是的,編譯器會(huì)分基本快,然后對(duì)每一個(gè)基本塊進(jìn)行優(yōu)化合并;(相關(guān)知識(shí),請(qǐng)參考編譯原理,我也已經(jīng)忘的差不多了);
但是但對(duì)于下面的例子,估計(jì)再牛的編譯器也沒有辦法:
view plaincopy to clipboardprint?void printPoint(Point * pt) { pt->print(); …… } //在某處調(diào)用: Point pt; … printPoint(&pt); … Point2D pt2d; … //再另外的某處調(diào)用 printPoint(&pt); void printPoint(Point * pt) { pt->print(); …… } //在某處調(diào)用: Point pt; … printPoint(&pt); … Point2D pt2d; … //再另外的某處調(diào)用 printPoint(&pt);
可以看到,如果不用點(diǎn)措施,犧牲一點(diǎn)東西,printPoint里面是不知道那個(gè)Point指針的所指向的真正對(duì)象是哪個(gè)的。那么,怎么辦呢?(換了是你,你說怎么辦?)
如果某種技術(shù)解決不了某些問題,原因就是在這些問題里面還有一些信息是某些技術(shù)所沒有用到的。這就是技術(shù)的一般方法論(出處在我這里,呵呵)
那么,根據(jù)上面的方法論的指導(dǎo),只需要再增加某些信息,然后再增加某些中間層,把信息放到中間層里面去,也許就可以解決(廢話…)
這雖然是廢話,但是也顯示了多態(tài)的實(shí)質(zhì)。所以就叫做顯示多態(tài)實(shí)質(zhì)的廢話吧。
具體如下:
1、編譯器遇到了class Point的定義的時(shí)候,發(fā)現(xiàn)有里面有virtual的成員函數(shù),于是將這個(gè)類的定義轉(zhuǎn)換如下:
view plaincopy to clipboardprint?//c++ 偽代碼,實(shí)際的編譯器是不會(huì)這么做的,他會(huì)把這些直接轉(zhuǎn)成機(jī)器碼。 struct Point { void *vptr_point; //vptr,指向下面定義的全局變量vtable_Point; …. //其他數(shù)據(jù)成員 }; //虛函數(shù)Point::print經(jīng)過name mangling轉(zhuǎn)化后的全局函數(shù) void print_Point(const Point *p){….} //編譯器自動(dòng)生成的構(gòu)造函數(shù)經(jīng)name mangling轉(zhuǎn)換過來的全局函數(shù),以確 //保vptr正確初始化 void Point_constructor(Point *p) { p->vptr_point = vtable_Point; } void * vtable_Point[] = {&print_Point, } //虛函數(shù)表 //c++ 偽代碼,實(shí)際的編譯器是不會(huì)這么做的,他會(huì)把這些直接轉(zhuǎn)成機(jī)器碼。 struct Point { void *vptr_point; //vptr,指向下面定義的全局變量vtable_Point; …. //其他數(shù)據(jù)成員 }; //虛函數(shù)Point::print經(jīng)過name mangling轉(zhuǎn)化后的全局函數(shù) void print_Point(const Point *p){….} //編譯器自動(dòng)生成的構(gòu)造函數(shù)經(jīng)name mangling轉(zhuǎn)換過來的全局函數(shù),以確 //保vptr正確初始化 void Point_constructor(Point *p) { p->vptr_point = vtable_Point; } void * vtable_Point[] = {&print_Point, } //虛函數(shù)表
2、對(duì)Point2D的轉(zhuǎn)換如下的偽代碼所示:(注意,雖然Point2D的定義里面沒有定義vritual,但是其基類Point有成員函數(shù)定義了virtual,所以還是有虛函數(shù)表,即使Point2D什么都沒有寫,如下所示:class Point2D : public Point{}也有虛函數(shù)表,Point2D的每一個(gè)對(duì)象也還會(huì)有vptr成員。 )
view plaincopy to clipboardprint?//Point2D的偽代碼 struct Point2D { void *vptr_point; /*vptr,其實(shí),這個(gè)指針是從基類Point里面繼承下來的,所以這的名 字不變,還是vptr_point(再次強(qiáng)調(diào),這是偽代碼,不要以為實(shí)際編譯器 里面真的給vptr取了這個(gè)名字啊!)指向下面定義的全局變量.*/ …. //其他數(shù)據(jù)成員 }; //虛函數(shù)Point2D::print經(jīng)過name mangling轉(zhuǎn)化后的全局函數(shù) void print_Point2D(const Point2D *p){….} //編譯器自動(dòng)生成的構(gòu)造函數(shù)經(jīng)name mangling轉(zhuǎn)換過來的全局函數(shù),以確保 //vptr正確初始化 void Point2D_constructor(Point2D *p) { p->vptr_point2D = vtable_Point2D; } //Point2D的虛函數(shù)表 void * vtable_Point2D[] = {&print_Point2D }; /* 注意,如果Point2D里面沒有定義Print(也就是說派生類沒有override 虛函數(shù)),那么這個(gè)地方的初始化就變成 Void * vtable_Point2D[] = {&print_Point}; */ //Point2D的偽代碼 struct Point2D { void *vptr_point; /*vptr,其實(shí),這個(gè)指針是從基類Point里面繼承下來的,所以這的名 字不變,還是vptr_point(再次強(qiáng)調(diào),這是偽代碼,不要以為實(shí)際編譯器 里面真的給vptr取了這個(gè)名字?。?指向下面定義的全局變量.*/ …. //其他數(shù)據(jù)成員 }; //虛函數(shù)Point2D::print經(jīng)過name mangling轉(zhuǎn)化后的全局函數(shù) void print_Point2D(const Point2D *p){….} //編譯器自動(dòng)生成的構(gòu)造函數(shù)經(jīng)name mangling轉(zhuǎn)換過來的全局函數(shù),以確保 //vptr正確初始化 void Point2D_constructor(Point2D *p) { p->vptr_point2D = vtable_Point2D; } //Point2D的虛函數(shù)表 void * vtable_Point2D[] = {&print_Point2D }; /* 注意,如果Point2D里面沒有定義Print(也就是說派生類沒有override 虛函數(shù)),那么這個(gè)地方的初始化就變成 Void * vtable_Point2D[] = {&print_Point}; */
那么下面的語句:
view plaincopy to clipboardprint?Point2D pt2d; Point *p = &pt2d; p->print(); Point2D pt2d; Point *p = &pt2d; p->print();
就會(huì)變成類似于下面的偽代碼:(C式的,不是C++式的)
view plaincopy to clipboardprint?struct Point2D pt2d; //再次說明是C式的偽代碼,C語言的定義是沒有什么構(gòu)造函數(shù)的 Point2D_Constructor(&pt2d); //調(diào)用上面提過的由編譯器自動(dòng)生成的構(gòu)造函數(shù)轉(zhuǎn)換過來的全局函數(shù),作用是正確初始化pt2d中的指針,讓它指向Point2D的需函數(shù)表; //指針類型轉(zhuǎn)換,看我筆記:指針類型轉(zhuǎn)換; Point *p = (Point *)&pt2d; (p->vptr_point)[0] )(pt) //調(diào)用p里面的vptr_point[0], 注意上面的初始化中的vptr_point指向Point2D的虛函數(shù)表,這個(gè)表格的第一項(xiàng)就是放則print相關(guān)的入口地址:指向Point2D::print,后面那個(gè)pt是把參數(shù)(也就是this指針)傳進(jìn)去。 struct Point2D pt2d; //再次說明是C式的偽代碼,C語言的定義是沒有什么構(gòu)造函數(shù)的 Point2D_Constructor(&pt2d); //調(diào)用上面提過的由編譯器自動(dòng)生成的構(gòu)造函數(shù)轉(zhuǎn)換過來的全局函數(shù),作用是正確初始化pt2d中的指針,讓它指向Point2D的需函數(shù)表; //指針類型轉(zhuǎn)換,看我筆記:指針類型轉(zhuǎn)換; Point *p = (Point *)&pt2d; (p->vptr_point)[0] )(pt) //調(diào)用p里面的vptr_point[0], 注意上面的初始化中的vptr_point指向Point2D的虛函數(shù)表,這個(gè)表格的第一項(xiàng)就是放則print相關(guān)的入口地址:指向 Point2D::print,后面那個(gè)pt是把參數(shù)(也就是this指針)傳進(jìn)去。
看了之后是不是覺得有點(diǎn)無語啊,怎么虛函數(shù)的調(diào)用原來這么麻煩??!看來還是C語言好啊,起碼不會(huì)做這么多事……
注意,不是這樣的,以上的過程都是在編譯階段就做好了的,那些虛函數(shù)表在編譯階段就已經(jīng)做好了。所以對(duì)于多態(tài)的執(zhí)行的代價(jià)如下:
1、 對(duì)于空間來說,每一個(gè)定義了virtual的類,都在全局?jǐn)?shù)據(jù)區(qū)里面有一張?zhí)摵瘮?shù)表,虛函數(shù)表的大小決定于這個(gè)類的體系(就是這個(gè)類及其基類)中虛函數(shù)的個(gè)數(shù)。(這張表是在編譯階段就已經(jīng)定義好了)
2、 以上類的每一個(gè)對(duì)象實(shí)例,在空間上多了一個(gè)指針的空間。
3、在類的構(gòu)造函數(shù)里面,多了一條語句的開銷(這條語句就是初始化上面多出來的指針,指向相應(yīng)類型的虛函數(shù)表),這個(gè)要留意,如果類中沒有聲明構(gòu)造函數(shù),這個(gè)時(shí)候,編譯器會(huì)自動(dòng)生成一個(gè)(說到這里,不要以為編譯器無論在什么時(shí)候都會(huì)為你的類生成一個(gè)默認(rèn)構(gòu)造函數(shù)啊~,以后的筆記會(huì)對(duì)這個(gè)問題重點(diǎn)討論),因此,還多了一個(gè)你可能并不想要的調(diào)用函數(shù)的開銷(當(dāng)然,也可能是以內(nèi)聯(lián)的方式嵌到代碼當(dāng)中,這個(gè)就要看編譯器的能力了)
4、 在執(zhí)行語句p->print();的時(shí)候,由于編譯器已經(jīng)轉(zhuǎn)換為(p->vptr_point)[0] )(pt);實(shí)際上多個(gè)間接層,看出來沒有,一般的p->f()只需要f_Point()…做全局調(diào)用就可以了(name mangling轉(zhuǎn)換),現(xiàn)在卻要對(duì)p尋址,尋址了還要找vptr_point在取它指向的0-4的字節(jié),然后再調(diào)用那個(gè)地址….說起來好像間接層不止一個(gè)……
以上的4點(diǎn)就是c++中虛函數(shù)調(diào)用的運(yùn)行時(shí)候所付出的幾乎所有代價(jià)。
所以以后參加面試的時(shí)候,有人問起:class A , A *p; … p->func() 的內(nèi)部代價(jià)是怎樣的?你一定一定要答詳細(xì)一點(diǎn),有多詳細(xì)就答多詳細(xì),最好能說出前因后果,不要像我一樣,當(dāng)時(shí)就答:“當(dāng)func是A的虛函數(shù)的時(shí)候,代價(jià)會(huì)大一些…”……是不是無語了,呵呵
(注:他當(dāng)時(shí)問的是:a.func() 和pa->func()在實(shí)現(xiàn)上有什么不同。其實(shí)和上面的問題是一樣的,我心里也知道有什么不同,只不過答的時(shí)候就說了一句話……)
這種運(yùn)行時(shí)才查表來調(diào)用函數(shù)的機(jī)制,被稱為動(dòng)態(tài)綁定…,好像很有術(shù)語的味道,但其實(shí)也就這么回事而已,天下事有難易乎…
關(guān)于這種虛函數(shù)表的機(jī)制的內(nèi)存布局圖,這里就不畫了,在我上面的筆記:三種內(nèi)存布局里面有圖;
還有兩個(gè)地方需要說一下的:
1、 虛函數(shù)的實(shí)現(xiàn)機(jī)制不止一種,其實(shí)還有幾種機(jī)制;不過這一種最高效(c++的目標(biāo)之一?。?,所以幾乎所有的編譯器都用了這種方法,當(dāng)然,這種方法也是有缺點(diǎn)的,請(qǐng)參考我的“MFC消息映射原理”,或者是別的文章。
2、關(guān)于虛函數(shù)表的表項(xiàng)和函數(shù)的入口地址關(guān)系,在這里似乎用了一種硬編碼的方法,比如索引0的表項(xiàng)放的是print函數(shù)的,1放的是**函數(shù)的…而且所有派生類的虛函數(shù)表的表項(xiàng)也得這么做,這個(gè)順序應(yīng)該是按照類的定義里面那些虛函數(shù)的聲明次序來的。而且,如果派生類有新的虛函數(shù),這些新的虛函數(shù)要在虛函數(shù)表中往后插(不能前插,因?yàn)榍懊嬉呀?jīng)是硬編碼了,注意);這個(gè)我沒有看過別人的見解,是我自己推測(cè)出來的,不過想來也應(yīng)該如此,如有不對(duì)之處,請(qǐng)各位多多指教,小弟不勝感激。
3、關(guān)于各家編譯器實(shí)現(xiàn)的差異,關(guān)于vptr在對(duì)象中的安插位置,不同的編譯器中可能不同,比如g++ 3.4.3將其插入到每個(gè)對(duì)象的最前面一項(xiàng),在vs2005中是插在最后面一項(xiàng)的。至于有沒有插到中間的,我就不知道了。其實(shí)有一種情況是插到中間的,這個(gè)到討論多重繼承的多態(tài)性時(shí)候再說。
下面的代碼有個(gè)疑問,大家不妨看看:
view plaincopy to clipboardprint?class B { public: virtual void print(int); //注意,這個(gè)虛函數(shù)帶有參數(shù)int }; class D : public B { public: void print(float) //注意,這個(gè)print帶參數(shù)float }; /*如果有如下語句:*/ D d; B *pb = &d; D *pd = &d; Pb->print(1.4f); //這個(gè)調(diào)用了哪個(gè)函數(shù)? Pd->print(1.4f); //這個(gè)函數(shù)呢?調(diào)用了那個(gè)函數(shù)? class B { public: virtual void print(int); //注意,這個(gè)虛函數(shù)帶有參數(shù)int }; class D : public B { public: void print(float) //注意,這個(gè)print帶參數(shù)float }; /*如果有如下語句:*/ D d; B *pb = &d; D *pd = &d; Pb->print(1.4f); //這個(gè)調(diào)用了哪個(gè)函數(shù)? Pd->print(1.4f); //這個(gè)函數(shù)呢?調(diào)用了那個(gè)函數(shù)?
編譯器遇到這種情況,又是怎么做的呢?以上的情況,到底屬于怎樣的一種情況呢,請(qǐng)看下篇筆記,隱藏和二義性~,謝謝各位觀光,呵呵。
PS. 寫了這么多篇筆記,關(guān)于《深度探索C++對(duì)象模型》的內(nèi)容還沒有正式開始討論呢,還是在熱身階段,本來想看看大家對(duì)我寫的這些東西有什么反饋的,哪知道一點(diǎn)反饋都沒有,要么就是灌水的貼,比如“寫的好啊”之類的回帖。
那就有兩種可能,一是高手看了覺得我的文章錯(cuò)漏百出,不屑一看;二就是大家看不懂我的文章到底在寫些什么東西,覺得莫名其妙;
但是,這些文章代表的是我現(xiàn)在對(duì)c++的理解,如果各位發(fā)現(xiàn)了問題能告訴我一下,我實(shí)在是不勝感激,哪怕是評(píng)論說:“你這些文章文筆太爛,語句不通,一點(diǎn)都看不懂”都會(huì)對(duì)我有所幫助。
因?yàn)槲矣X得一個(gè)互動(dòng)的平臺(tái),比一個(gè)單獨(dú)的閱讀和筆記更有利于提高雙方的能力和理解。
另外一個(gè),是我覺得,學(xué)習(xí)別人文章的態(tài)度,并不是一味的全盤吸收,而應(yīng)該是有所懷疑,在懷疑的基礎(chǔ)上在加以論證,從懷疑出發(fā),經(jīng)過驗(yàn)證,到最后得到結(jié)論,這樣的學(xué)習(xí)的印象會(huì)比單獨(dú)吸收結(jié)論要深的多,而且不容易偏信;(再次說明,我在這里的很多推論性的東西都是我自己推猜出來的,沒有經(jīng)過任何權(quán)威的肯定,也沒有看過編譯器源碼去驗(yàn)證)
對(duì)人則不然,對(duì)人際交流來說應(yīng)該持相反的態(tài)度,是先相信你說的話,經(jīng)過考察,再慢慢的下結(jié)論;
但是我們中國(guó)人,卻恰好相反,對(duì)人是一開始持懷疑的態(tài)度,慢慢慢慢的熟悉了,才相信你;對(duì)別人網(wǎng)上貼的文章,則是一開始就全盤接收,在經(jīng)過錯(cuò)誤的教訓(xùn)之后,才開始提出反對(duì)意見(這個(gè)時(shí)候往往有對(duì)其作者的辱罵一起的傾向)。