|
1. 什么是多態(tài)多態(tài)是C++中的一個(gè)重要的基礎(chǔ),可以這樣說(shuō),不掌握多態(tài)就是C++的門(mén)個(gè)漢。我就給它定一個(gè)這樣的名字-- “調(diào)用’同名函數(shù)’卻會(huì)因上下文不同會(huì)有不同的實(shí)現(xiàn)的一種機(jī)制”。這個(gè)名字長(zhǎng)是長(zhǎng)了點(diǎn)兒,可是比“多態(tài)”清楚多了。看這個(gè)長(zhǎng)的定義,我們可以從中找出多態(tài) 的三個(gè)重要的部分。一是“相同函數(shù)名”,二是“依據(jù)上下文”,三是“實(shí)現(xiàn)卻不同”。我們且把它們叫做多態(tài)三要素吧。
2. 多態(tài)帶來(lái)的好處多態(tài)帶來(lái)兩個(gè)明顯的好處:一是不用記大量的函數(shù)名了,二是它會(huì)依據(jù)調(diào)用時(shí)的上下文來(lái)確定實(shí)現(xiàn)。確定實(shí)現(xiàn)的過(guò)程由C++本身完成,另外還有一個(gè)不明顯但卻很重要的好處是:帶來(lái)了面向?qū)ο蟮木幊獭?/font>
3. C++中實(shí)現(xiàn)多態(tài)的方式C++中共有三種實(shí)現(xiàn)多態(tài)的方式。由“容易說(shuō)明白”到“不容易說(shuō)明白”排序分別為: 第一種是函數(shù)重載;第二種是模板函數(shù);第三種是虛函數(shù)。
4. 細(xì)說(shuō)用函數(shù)重載實(shí)現(xiàn)的多態(tài)函數(shù)重載是這樣一種機(jī)制:允許有不同參數(shù)的函數(shù)有相同的名字。具體一點(diǎn)講就是:假如有如下三個(gè)函數(shù):void test(int arg){} //函數(shù)1void test(char arg){} //函數(shù)2void test(int arg1,int arg2){} //函數(shù)3如果在C中編譯,將會(huì)得到一個(gè)名字沖突的錯(cuò)誤而不能編譯通過(guò)。在C++中這樣做是合法的。可是當(dāng)我們調(diào)用test的時(shí)候到底是會(huì)調(diào)用上面三個(gè)函數(shù)中的哪一個(gè)呢?這要依據(jù)你在調(diào)用時(shí)給的出的參數(shù)來(lái)決定。如下: test(5); //調(diào)用函數(shù)1 test('c');//調(diào)用函數(shù)2 test(4,5); //調(diào)用函數(shù)3
C++是如何做到這一點(diǎn)的呢?原來(lái)聰明的C++編譯器在編譯的時(shí)候悄悄的在我們的函數(shù)名上根據(jù)函數(shù)的參數(shù)的不同做了一些不同的記號(hào)。具體說(shuō)如下:void test(int arg) //被標(biāo)記為 ‘test有一個(gè)int型參數(shù)’void test(char arg) //被標(biāo)記為 ‘test有一個(gè)char型的參數(shù)’void test(int arg1,int arg2) //被標(biāo)記為 ‘test第一個(gè)參數(shù)是int型,第二個(gè)參數(shù)為int型’這 樣一來(lái)當(dāng)我們進(jìn)行對(duì)test的調(diào)用時(shí),C++就可以根據(jù)調(diào)用時(shí)的參數(shù)來(lái)確定到底該用哪一個(gè)test函數(shù)了。噢,聰明的C++編譯器。其實(shí)C++做標(biāo)記做的 比我上面所做的更聰明。我上面哪樣的標(biāo)記太長(zhǎng)了。C++編譯器用的標(biāo)記要比我的短小的多。看看這個(gè)真正的C++的對(duì)這三個(gè)函數(shù)的標(biāo)記:?test@@YAXD@Z?test@@YAXH@Z?test@@YAXHH@Z
是不是短多了。但卻不好看明白了。好在這是給計(jì)算機(jī)看的,人看不大明白是可以理解的。還記得cout吧。我們用<<可以讓它把任意類型的數(shù)據(jù)輸出。比如可以象下面那樣: cout << 1; //輸出int型 cout << 8.9; //輸出double型 cout << 'a'; //輸出char型 cout << "abc";//輸出char數(shù)組型 cout << endl; //輸出一個(gè)函數(shù)cout之所以能夠用一個(gè)函數(shù)名<<(<<是一個(gè)函數(shù)名)就能做到這些全是函數(shù)重載的功能。要是沒(méi)有函數(shù)重載,我們也許會(huì)這樣使用cout,如下: cout int<< 1; //輸出int型 cout double<< 8.9; //輸出double型 cout char<< 'a'; //輸出char型 cout charArray<< "abc"; //輸出char數(shù)組型 cout function(…)<< endl; //輸出函數(shù)為每一種要輸出的類型起一個(gè)函數(shù)名,這豈不是很麻煩呀。
不過(guò)函數(shù)重載有一個(gè)美中不足之處就是不能為返回值不同的函數(shù)進(jìn)行重載。那是因?yàn)槿藗兂32粸楹瘮?shù)調(diào)用指出返回值。并不是技術(shù)上不能通過(guò)返回值來(lái)進(jìn)行重載。
5. 細(xì)說(shuō)用模板函數(shù)實(shí)現(xiàn)的多態(tài)所謂模板函數(shù)(也有人叫函數(shù)模板)是這樣一個(gè)概念:函數(shù)的內(nèi)容有了,但函數(shù)的參數(shù)類型卻是待定的(注意:參數(shù)個(gè)數(shù)不是待定的)。比如說(shuō)一個(gè)(準(zhǔn)確的說(shuō)是一類或一群)函數(shù)帶有兩個(gè)參數(shù),它的功能是返回其中的大值。這樣的函數(shù)用模板函數(shù)來(lái)實(shí)現(xiàn)是適合不過(guò)的了。如下。template < typename T>T getMax(T arg1, T arg2){ return arg1 > arg2 ? arg1:arg2; //代碼段1}這 就是基于模板的多態(tài)嗎?不是。因?yàn)楝F(xiàn)在我們不論是調(diào)用getMax(1, 2)還是調(diào)用getMax(3.0, 5.0)都是走的上面的函數(shù)定義。它沒(méi)有根據(jù)調(diào)用時(shí)的上下文不同而執(zhí)行不同的實(shí)現(xiàn)。所以這充其量也就是用了一個(gè)模板函數(shù),和多態(tài)不沾邊。怎樣才能和多態(tài)沾 上邊呢?用模板特化呀!象這樣:template<>char* getMax(char* arg1, char* arg2){ return (strcmp(arg1, arg2) > 0)?arg1:arg2;//代碼段2}這樣一來(lái)當(dāng)我們調(diào)用getMax(“abc”, “efg”)的時(shí)候,就會(huì)執(zhí)行代碼段2,而不是代碼段1。這樣就是多態(tài)了。更有意思的是如果我們?cè)賹?xiě)這樣一個(gè)函數(shù):char getMax(char arg1, char arg2){ return arg1>arg2?arg1:arg2; //代碼段3}當(dāng)我們調(diào)用getMax(‘a(chǎn)’, ‘b’)的時(shí)候,執(zhí)行的會(huì)是代碼段3,而不是代碼段1或代碼段2。C++允許對(duì)模板函數(shù)進(jìn)行函數(shù)重載,就象這個(gè)模板函數(shù)是一個(gè)普通的函數(shù)一樣。于是我們馬上能想到寫(xiě)下面這樣一個(gè)函數(shù)來(lái)做三個(gè)數(shù)中取大值的處理:int getMax( int arg1, int arg2, int arg3){ return getMax(arg1, max(arg2, arg3) ); //代碼段4}同樣我們還可以這樣寫(xiě):template <typename T>T getMax(T arg1, T arg2, T arg3){ return getMax(arg1, getMax(arg2, arg3) ); //代碼段5}現(xiàn)在看到結(jié)合了模板的多態(tài)的威力了吧。比只用函數(shù)重載厲害多了。
6. 小結(jié)上 面的兩種多態(tài)在C++中有一個(gè)總稱:靜態(tài)多態(tài)。之所以叫它們靜態(tài)多態(tài)是因?yàn)樗鼈兊亩鄳B(tài)是在編譯期間就確定了。也就是說(shuō)前面所說(shuō)的函數(shù)1,2,3代碼段1, 2,3,4,5這些,在編譯完成后,應(yīng)該在什么樣的上下文的調(diào)用中執(zhí)行哪一些就確定了。比如:如果調(diào)用getMax(0.1, 0.2, 0.3)就會(huì)執(zhí)行代碼段5。如果調(diào)用test(5)就執(zhí)行函數(shù)1。這些是在編譯期間就能確定下來(lái)的。靜態(tài)多態(tài)還有一個(gè)特點(diǎn),就是:“總和參數(shù)較勁兒”。下面所要講的一種多態(tài)就是必需是在程序的執(zhí)行過(guò)程中才能確定要真正執(zhí)行的函數(shù)。所以這種多態(tài)在C++中也被叫做動(dòng)態(tài)多態(tài)。
7. 細(xì)說(shuō)用虛函數(shù)實(shí)現(xiàn)的多態(tài)7.1.虛函數(shù)是怎么回事首先來(lái)說(shuō)一說(shuō)虛函數(shù),所謂虛函數(shù)是這樣一個(gè)概念:基類中有這么一些函數(shù),這些函數(shù)允許在派生類中其實(shí)現(xiàn)可以和基類的不一樣。在C++中用關(guān)鍵字virtual來(lái)表示一個(gè)函數(shù)是虛函數(shù)。C++中還有一個(gè)術(shù)語(yǔ) “覆蓋”與虛函數(shù)關(guān)系密切。所謂覆蓋就是說(shuō),派生類中的一個(gè)函數(shù)的聲明,與基類中某一個(gè)函數(shù)的聲明一模一樣,包括返回值,函數(shù)名,參數(shù)個(gè)數(shù),參數(shù)類型,參數(shù)次序都不能有差異。(注1)說(shuō)覆蓋和虛函數(shù)關(guān)系密切的原因有兩個(gè):一個(gè)原因是,只有覆蓋基類的虛函數(shù)才是安全的。第二個(gè)原因是,要想實(shí)現(xiàn)基于虛函數(shù)的多態(tài)就必須在派生類中覆蓋基類的虛函數(shù)。接下來(lái)讓我們說(shuō)一說(shuō)為什么要有虛函數(shù),分析一下為什么派生類非要在某些情況下覆蓋基類的虛函數(shù)。就以那個(gè)非常著名的圖形繪制的例子來(lái)說(shuō)吧。假設(shè)我們?cè)跒橐粋(gè)圖形系統(tǒng)編程。我們可能有如下的一個(gè)類結(jié)構(gòu)。 圖7-1形狀對(duì)外公開(kāi)一個(gè)函數(shù)來(lái)把自己繪制出來(lái)。這是合理的,形狀就應(yīng)該能繪制出來(lái),對(duì)吧?由于繼承的原因,多邊形和圓形也有了繪制自己這個(gè)函數(shù)。現(xiàn)在我們來(lái)討論在這三個(gè)類中的繪制自己的函數(shù)都應(yīng)該怎么實(shí)現(xiàn)。在形狀中嘛,什么也不做就行了。在多邊形中嘛,只要把它所有的頂點(diǎn)首尾相連起來(lái)就行了。在圓形中嘛,依據(jù)它的圓心和它的半徑畫(huà)一個(gè)360度的圓弧就行了。可是現(xiàn)在的問(wèn)題是:多邊形和圓形的繪制自己的函數(shù)是從形狀繼承而來(lái)的,并不能做連接頂點(diǎn)和畫(huà)圓弧的工作。怎 么辦呢?覆蓋它,覆蓋形狀中的繪制自己這個(gè)函數(shù)。于是我們?cè)诙噙呅魏蛨A形中各做一個(gè)繪制自己的函數(shù),覆蓋形狀中的繪制自己的函數(shù)。為了實(shí)現(xiàn)覆蓋,我們需要 把形狀中的繪制自己這個(gè)函數(shù)用virtual修飾。而且形狀中的繪制自己這個(gè)函數(shù)什么也不干,我們就把它做成一個(gè)純虛函數(shù)。純虛函數(shù)還有一個(gè)作用,就是讓 它所在的類成為抽象類。形狀理應(yīng)是一個(gè)抽象類,不是嗎?于是我們很快寫(xiě)出這三個(gè)類的代碼如下:class Shape//形狀{public: virtualvoid DrawSelf()//繪制自己 { cout << "我是一個(gè)什么也繪不出的圖形" << endl; }}; class Polygo:public Shape//多邊形{public: void DrawSelf() //繪制自己 { cout << "連接各頂點(diǎn)" << endl; }}; class Circ:public Shape//圓{public: void DrawSelf() //繪制自己 { cout << "以圓心和半徑為依據(jù)畫(huà)弧" << endl; }};下面,我們將以上面的這三個(gè)類為基礎(chǔ)來(lái)說(shuō)明動(dòng)態(tài)多態(tài)。在進(jìn)行更進(jìn)一步的說(shuō)明之前,我們先來(lái)說(shuō)一個(gè)不得不說(shuō)的兩個(gè)概念:“子類型”和“向上轉(zhuǎn)型”。
7.2.向上轉(zhuǎn)型子類型很好理解,比如上面的多邊形和圓形就是形狀的子類型。關(guān)于子類型還有一個(gè)確切的定義為:如果類型X擴(kuò)充或?qū)崿F(xiàn)了類型Y,那么就說(shuō)X是Y的子類型。向 上轉(zhuǎn)型的意思是說(shuō)把一個(gè)子類型轉(zhuǎn)的對(duì)象換為父類型的對(duì)象。就好比把一個(gè)多邊形轉(zhuǎn)為一個(gè)形狀。向上轉(zhuǎn)型的意思就這么簡(jiǎn)單,但它的意義卻很深遠(yuǎn)。向上轉(zhuǎn)型中有 三點(diǎn)需要我們特別注意。第一,向上轉(zhuǎn)型是安全的。第二,向上轉(zhuǎn)型可以自動(dòng)完成。第三,向上轉(zhuǎn)型的過(guò)程中會(huì)丟失子類型信息。這三點(diǎn)在整個(gè)動(dòng)態(tài)多態(tài)中發(fā)揮著重 要的作用。假如我們有如下的一個(gè)函數(shù):void OutputShape( Shape arg)//專門(mén)負(fù)責(zé)調(diào)用形狀的繪制自己的函數(shù){ arg.DrawSelf();}那么現(xiàn)在我們可以這樣使用OutputShape這個(gè)函數(shù): Polygon shape1; Circ shape2; OutputShape(shape1); OutputShape(shape2);我們之所以可以這樣使用OutputShape函數(shù),正是由于向上轉(zhuǎn)型是安全的(不會(huì)有任何的編譯警告),是由于向上轉(zhuǎn)弄是自動(dòng)的(我們沒(méi)有自己把shape1和shape2轉(zhuǎn)為Shape類型再傳給OutputShape函數(shù))。可是上面這段程序運(yùn)行后的輸出結(jié)果是這樣的:我是一個(gè)什么也繪不出的圖形我是一個(gè)什么也繪不出的圖形明明是一個(gè)多邊形和一個(gè)圓呀,應(yīng)該是輸出這下面這個(gè)樣子才合理呀!連接各頂點(diǎn)以圓心和半徑為依據(jù)畫(huà)弧造成前面的不合理的輸出的罪魁禍?zhǔn)渍恰蛏限D(zhuǎn)型中的子類型信息丟失’。為了得到一個(gè)合理的輸出,得想個(gè)辦法來(lái)找回那些丟失的子類型信息。C++中用一種比較巧妙的辦法來(lái)找回那些丟失的子類型信息。這個(gè)辦法就是采用指針或引用。
7.3.為什么要用指針或引用來(lái)實(shí)現(xiàn)動(dòng)態(tài)多態(tài)對(duì)于一個(gè)對(duì)象來(lái)說(shuō)無(wú)論有多少個(gè)指針指向它,這些個(gè)指針?biāo)傅亩际峭粋(gè)對(duì)象。(即使你用一個(gè)void的指針指向一個(gè)對(duì)象也是這樣的,不是嗎?)同理對(duì)于引用也一樣。這究竟有多少深層次的意義呢?這里的深層的意義是這樣的:子類型的信息本來(lái)就在它本身中存在,所以我們用一個(gè)基類的指針來(lái)指出它,這個(gè)子類型的信息也會(huì)被找到,同理引用也是一樣的。C++正是利用了指針的這一特性。來(lái)做到動(dòng)態(tài)多態(tài)的。注2現(xiàn)在讓我們來(lái)改寫(xiě)OutputShape函數(shù)為這樣:void OutputShape( Shape& arg)//專門(mén)負(fù)責(zé)調(diào)用形狀的繪制自己的函數(shù){ arg.DrawSelf();}現(xiàn)在我們的程序的輸出為:連接各頂點(diǎn)以圓心和半徑為依據(jù)畫(huà)弧這樣的輸出才是我們真正的想要的。我們實(shí)現(xiàn)的這種真正想要的輸出就是動(dòng)態(tài)多態(tài)的實(shí)質(zhì)。
7.4.為什么動(dòng)態(tài)多態(tài)要用public繼承在我們上面的代碼中,圓和多邊形都是從形狀公有繼承而來(lái)的。要是我們把圓的繼承改為私有或保護(hù)會(huì)怎么樣呢?我們來(lái)試一試。哇,我們得到一個(gè)編譯錯(cuò)誤。這個(gè)錯(cuò)誤的大致意思是說(shuō):“請(qǐng)不要用一個(gè)私有的方法”。怎么回事呢?是這么回事。它的意思是說(shuō)下面這樣說(shuō)不合理。所有的形狀都可以畫(huà)出來(lái),圓這種形狀是不能畫(huà)出來(lái)的。這樣合理嗎?不合理。所以請(qǐng)?jiān)诙鄳B(tài)中使用公有繼承吧。
8. 總結(jié)多態(tài)的思想其實(shí)早在面向?qū)ο蟮木幊坛霈F(xiàn)之前就有了。比如C語(yǔ)言中的+運(yùn)算符。這個(gè)運(yùn)算符可以對(duì)兩個(gè)int型的變量求和,也可以對(duì)兩個(gè)char的變量求和,也可以對(duì)一個(gè)int型一個(gè)char型的兩個(gè)變量求和。加法運(yùn)算的這種特性就是典型的多態(tài)。所以說(shuō)多態(tài)的本質(zhì)是同樣的用法在實(shí)現(xiàn)上卻是不同的。
9. 附錄:注1:嚴(yán)格地講返回值可以不同,但這種不同是有限制的。詳細(xì)情況請(qǐng)看有關(guān)協(xié)變的內(nèi)容。注2: C++會(huì)悄悄地在含有虛函數(shù)的類里面加一個(gè)指針。用這個(gè)指針來(lái)指向一個(gè)表格。這個(gè)表格會(huì)包含每一個(gè)虛函數(shù)的索引。用這個(gè)索引來(lái)找出相應(yīng)的虛函數(shù)的入口地 址。對(duì)于我們所舉的形狀的例子來(lái)說(shuō),C++會(huì)悄悄的做三個(gè)表,Shape一個(gè),Polygon一個(gè),Circ一個(gè)。它們分別記錄一個(gè)DrawSelf函數(shù) 的入口地址。在程序運(yùn)行的過(guò)程中,C++會(huì)先通過(guò)類中的那個(gè)指針來(lái)找到這個(gè)表格。再?gòu)倪@個(gè)表格中查出DrawSelf的入口地址。然后現(xiàn)通過(guò)這個(gè)入口地址 來(lái)調(diào)用正直的DrawSelf。正是由于這個(gè)查找的過(guò)程,是在運(yùn)行時(shí)完成的。所以這樣的多態(tài)才會(huì)被叫做動(dòng)態(tài)多態(tài)(運(yùn)行時(shí)多態(tài))
|
|