找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

搜索
查看: 2041|回復(fù): 0
收起左側(cè)

C++中的多態(tài)

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

回復(fù)

使用道具 舉報

您需要登錄后才可以回帖 登錄 | 立即注冊

本版積分規(guī)則

手機(jī)版|小黑屋|51黑電子論壇 |51黑電子論壇6群 QQ 管理員QQ:125739409;技術(shù)交流QQ群281945664

Powered by 單片機(jī)教程網(wǎng)

快速回復(fù) 返回頂部 返回列表