虛函數-運行時多態的理解

形狀對外公開一個函數來把自己繪製出來。這是合理的,形狀就應該能繪製出來,對吧?由於繼承的原因,多邊形和圓形也有了繪製自己這個函數。
現在我們來討論在這三個類中的繪製自己的函數都應該怎麼實現。在形狀中嘛,什麼也不做就行了。在多邊形中嘛,只要把它所有的頂點首尾相連起來就行了。在圓形中嘛,依據它的圓心和它的半徑畫一個360度的圓弧就行了。
可是現在的問題是:多邊形和圓形的繪製自己的函數是從形狀繼承而來的,並不能做連接頂點和畫圓弧的工作。
怎麼辦呢?覆蓋它,覆蓋形狀中的繪製自己這個函數。於是我們在多邊形和圓形中各做一個繪製自己的函數,覆蓋形狀中的繪製自己的函數。爲了實現覆蓋,我們需要把形狀中的繪製自己這個函數用virtual修飾。而且形狀中的繪製自己這個函數什麼也不幹,我們就把它做成一個純虛函數。純虛函數還有一個作用,就是讓它所在的類成爲抽象類。形狀理應是一個抽象類,不是嗎?於是我們很快寫出這三個類的代碼如下:
class Shape//形狀
{
public:
    virtualvoid DrawSelf()//繪製自己
    {
       cout << "我是一個什麼也繪不出的圖形" << endl;
    }
};

class Polygo:public Shape//多邊形
{
public:
    void DrawSelf()   //繪製自己
    {
       cout << "連接各頂點" << endl;
    }
};

class Circ:public Shape//圓
{
public:
    void DrawSelf()   //繪製自己
    {
       cout << "以圓心和半徑爲依據畫弧" << endl;
    }
};
下面,我們將以上面的這三個類爲基礎來說明動態多態。在進行更進一步的說明之前,我們先來說一個不得不說的兩個概念:“子類型”和“向上轉型”。
7.2.向上轉型
子類型很好理解,比如上面的多邊形和圓形就是形狀的子類型。關於子類型還有一個確切的定義爲:如果類型X擴充或實現了類型Y,那麼就說X是Y的子類型。
向上轉型的意思是說把一個子類型轉的對象換爲父類型的對象。就好比把一個多邊形轉爲一個形狀。向上轉型的意思就這麼簡單,但它的意義卻很深遠。向上轉型中有三點需要我們特別注意。第一,向上轉型是安全的。第二,向上轉型可以自動完成。第三,向上轉型的過程中會丟失子類型信息。這三點在整個動態多態中發揮着重要的作用。
假如我們有如下的一個函數:
void OutputShape( Shape arg)//專門負責調用形狀的繪製自己的函數
{
    arg.DrawSelf();
}
那麼現在我們可以這樣使用OutputShape這個函數:
    Polygon shape1;
    Circ shape2;
    OutputShape(shape1);
    OutputShape(shape2);
我們之所以可以這樣使用OutputShape函數,正是由於向上轉型是安全的(不會有任何的編譯警告),是由於向上轉弄是自動的(我們沒有自己把shape1和shape2轉爲Shape類型再傳給OutputShape函數)。可是上面這段程序運行後的輸出結果是這樣的:
我是一個什麼也繪不出的圖形
我是一個什麼也繪不出的圖形
明明是一個多邊形和一個圓呀,應該是輸出這下面這個樣子才合理呀!
連接各頂點
以圓心和半徑爲依據畫弧
造成前面的不合理的輸出的罪魁禍首正是‘向上轉型中的子類型信息丟失’。爲了得到一個合理的輸出,得想個辦法來找回那些丟失的子類型信息。C++中用一種比較巧妙的辦法來找回那些丟失的子類型信息。這個辦法就是採用指針或引用。
7.3.爲什麼要用指針或引用來實現動態多態
對於一個對象來說無論有多少個指針指向它,這些個指針所指的都是同一個對象。(即使你用一個void的指針指向一個對象也是這樣的,不是嗎?)同理對於引用也一樣。
這究竟有多少深層次的意義呢?這裏的深層的意義是這樣的:子類型的信息本來就在它本身中存在,所以我們用一個基類的指針來指出它,這個子類型的信息也會被找到,同理引用也是一樣的。C++正是利用了指針的這一特性。來做到動態多態的。注2現在讓我們來改寫OutputShape函數爲這樣:
void OutputShape( Shape& arg)//專門負責調用形狀的繪製自己的函數
{
    arg.DrawSelf();
}
現在我們的程序的輸出爲:
連接各頂點
以圓心和半徑爲依據畫弧
這樣的輸出纔是我們真正的想要的。我們實現的這種真正想要的輸出就是動態多態的實質。
7.4.爲什麼動態多態要用public繼承
在我們上面的代碼中,圓和多邊形都是從形狀公有繼承而來的。要是我們把圓的繼承改爲私有或保護會怎麼樣呢?我們來試一試。哇,我們得到一個編譯錯誤。這個錯誤的大致意思是說:“請不要用一個私有的方法”。怎麼回事呢?
是這麼回事。它的意思是說下面這樣說不合理。
所有的形狀都可以畫出來,圓這種形狀是不能畫出來的。
這樣合理嗎?不合理。所以請在多態中使用公有繼承吧。
8.總結
多態的思想其實早在面向對象的編程出現之前就有了。比如C語言中的+運算符。這個運算符可以對兩個int型的變量求和,也可以對兩個char的變量求和,也可以對一個int型一個char型的兩個變量求和。加法運算的這種特性就是典型的多態。所以說多態的本質是同樣的用法在實現上卻是不同的。
9.附錄:
注1:嚴格地講返回值可以不同,但這種不同是有限制的。詳細情況請看有關協變的內容。
注2:C++會悄悄地在含有虛函數的類裏面加一個指針。用這個指針來指向一個表格。這個表格會包含每一個虛函數的索引。用這個索引來找出相應的虛函數的入口地址。對於我們所舉的形狀的例子來說,C++會悄悄的做三個表,Shape一個,Polygon一個,Circ一個。它們分別記錄一個 DrawSelf函數的入口地址。在程序運行的過程中,C++會先通過類中的那個指針來找到這個表格。再從這個表格中查出DrawSelf的入口地址。然後現通過這個入口地址來調用正直的DrawSelf。正是由於這個查找的過程,是在運行時完成的。所以這樣的多態纔會被叫做動態多態(運行時多態)
發佈了12 篇原創文章 · 獲贊 23 · 訪問量 32萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章