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