詳解C++中的多態、虛函數、父類子類

來源:互聯網
上載者:User

 這一篇主要是想講解一下C++中的多態性,這也是我在學習Win32和MFC編程中碰到的,算是總結一下吧。

 

首先來看一段程式:

 

#include <iostream><br />using namespace std;</p><p>class CObject{</p><p>public:<br />virtual void Serialize(){</p><p>cout<<"CObject::Serial() /n/n";<br />}</p><p>};</p><p>class CDocument: public CObject{</p><p>public :<br />int m_data1 ;<br />void func(){<br />cout<<"CDocument::func()"<<endl;<br />Serialize();<br />}</p><p>virtual void Serialize(){<br />cout<<"CDocument::Serial() /n/n";<br />}<br />};</p><p>class CMyDoc : public CDocument{</p><p>public:<br />int m_data2;<br />virtual void Serialize(){</p><p>cout<<"CMyDoc::Serialize() /n/n";</p><p>}</p><p>};</p><p>int main(void){</p><p>CMyDoc myDoc ;<br />CMyDoc* pMyDoc = new CMyDoc();</p><p>cout<<"#1 testing"<<endl;<br />myDoc.func();</p><p>cout<<"#2 testing"<<endl;<br />((CDocument*)(&myDoc))->func();</p><p>cout<<"#3 testing"<<endl;<br />pMyDoc->func();</p><p>cout<<"#4 testing"<<endl;<br />((CDocument)myDoc).func();</p><p>return 0;<br />}<br />

 

從程式中可以看出這樣的繼承關係:

CMyDoc -> CDocument -> CObject,這裡要注意由於繼承關係的存在,CMyDoc類中其實是存在如下的成員函數和變數的:

1、func() 從CDocument繼承得到的

2、m_data1也是從CDocument繼承得到的

但是CMyDoc和CDocument都重寫了各自父類的虛函數Serialize().

 

它的運行結果為:

#1 testing
CDocument::func()
CMyDoc::Serialize()

 

#2 testing
CDocument::func()
CMyDoc::Serialize()

 

#3 testing
CDocument::func()
CMyDoc::Serialize()

 

#4 testing
CDocument::func()
CDocument::Serial()

 

從前三個運行結果可以得出這樣的結論:由於myDoc是CMyDoc類,而pMyDoc是指向CMyDoc的類的指標,兩者都是和MyDoc類有關聯的,所以前三種情況在調用CDocument::func()中的Serialize()時,由於子類CMyDoc中已經重寫了父類的Serialize(),所以都會最終落實到對子類CMyDoc::Serialize()的調用,而不是執行父類CDocument::Serialize().

但是在執行第四個測試時,情況不一樣了,這裡直接把CMyDoc類型對象upcast強制轉化為了CDocument類型對象,這種由子類強制轉化為父類的過程,就稱為對象切割。

一般情況下,從記憶體佔用的角度來看,子類對象要比父類對象大,因為子類會從父類那邊繼承相關的成員變數以及成員函數,同時又會在自己類內部增加自己的成員變數以及成員函數。所以這裡當通過 ((CDocument)myDoc).func();調用Serialize()時,調用的就是CDocument::Serial() 了,我的理解是子類CMyDoc::Serialize() 在進行upcast的時候,把這些自己的資訊丟失掉了。

 

好,接下來就要說一下,虛函數這樣的機制是如何?的。

虛函數其實是一種動態綁定機制,因為在編譯時間,編譯器是不知道是該調用父類中的虛函數還是子類中的虛函數的,而是在程式執行過程中,動態確定的。虛函數的本質是,C++編譯器透過某個表格,在執行時期「間接」調用實際上欲綁定的函數(注意「間接」這個字眼)。這樣的表格稱為虛擬函數表(常被稱為vtable)。每一個「內含虛擬函數的類」,編譯器都會為它做出一個虛擬函數表,表中的每一筆元素都指向一個虛擬函數的地址。此外,編譯器當然也會為類別加上一項成員變數,是一個指向該虛擬函數表的指標(常被稱為vptr)。

每一個由此類衍生出來的對象,都有這麼一個vptr。當我們透過這個對象調用虛擬函數,事實上是透過vptr 找到虛擬函數表,再找出虛擬函數的真正地址。

 

好了,到這裡我們至少對虛函數的實現機制有了一個補充瞭解,那麼像上面樣本程式的原理是怎麼一回事呢?

奧妙在於這個虛擬函數表以及這種間接調用方式。虛擬函數表的內容是依據類別中的虛擬函式宣告次序,一一填入函數指標。衍生類別會繼承基礎類別的虛擬函數表(以及所有其它可以繼承的成員),當我們在衍生類別中改寫虛擬函數時,虛擬函數表就受了影響:表中元素所指的函數地址將不再是基礎類別的函數地址,而是衍生類別的函數地址。

 

這就是為什麼,在前三個測試中,CDocument::func()函數中調用Serilize()時,調用的都是被子類CMyDoc重寫的Serilize()虛函數。

 

那麼在具體的程式設計中我們應該如何利用虛函數所具有的性質,以達到介面統一的目的的?

方法如下:

在基類中聲明一個虛函數(最好,聲明成純虛函數,這樣基類就成為了抽象基類),但不用聲明它的方法體,讓所有繼承於基類的子類重寫這個虛函數。以後要想統一調用這些子類的這個介面函數時,只要先獲得抽象基類的指標,然後擷取各個子類對象的地址,賦值給基類的指標,最後通過基類指標調用這個介面函數。

 

我來舉個例子吧,這樣比較清晰:

#include <iostream><br />#include <fstream><br />using namespace std;</p><p>ofstream out("out.txt");</p><p>class CShape{</p><p>public :<br />virtual void display() =0;<br />};</p><p>class CCircle : public CShape{</p><p>public:<br />virtual void display(){<br />out<<"Display Circle /n/n";<br />}<br />};</p><p>class CRectangle : public CShape{</p><p>public:<br />virtual void display(){<br />out<<"Display Rectangle /n/n";<br />}</p><p>};</p><p>class CStar : public CShape {</p><p>public:<br />virtual void display(){<br />out<<"Display Star /n/n";<br />}<br />};</p><p>int main(void) {</p><p>CShape* array[]={<br />new CRectangle(),<br />new CCircle(),<br />new CStar()<br />};<br />int arraySize = sizeof(array)/sizeof(*array[0]);//3<br />cout<<arraySize<<endl;</p><p>for(int i=0 ; i < arraySize ; i++)<br />array[i]->display();</p><p>return 0;<br />}<br />

 

運行結果如下,重點就是在main函數中的array數組,呵呵,就是這麼方便。

 

Display Rectangle

 

Display Circle

 

Display Star

 

好,接下來,就說說幾個關於虛函數的小總結吧:)這些都是從《深入淺出MFC》中的,呵呵。

1、 如果你期望衍生類別重新定義一個成員函數,那麼你應該在基礎類別中把此函 數設為virtual。

2、以單一指令喚起不同函數,這種性質稱為Polymorphism,意思是"the ability to  assume many forms",也就是多態。

3、既然抽象類別中的虛擬函數不打算被調用,我們就不應該定義它,應該把它設為純虛擬函數(在函式宣告之後加上"=0" 即可)

4、抽象類別不能產生出對象實體,但是我們可以擁有指向抽象類別之指標,以便於操作抽象類別的各個衍生類別。
     虛擬函數衍生下去仍為虛擬函數,而且可以省略virtual 關鍵詞。

 

 

好,接下來我們再看一個例子,這個例子也是關於父類與子類的:

 

#include <iostream><br />#include <fstream><br />using namespace std;</p><p>ofstream out("out.txt");</p><p>class CShape{</p><p>public :<br />void display();<br />void OutputName(){<br />out<<"Shape /n/n";<br />}<br />};</p><p>class CCircle : public CShape{</p><p>public:<br />void display(){<br />out<<"Display Circle /n/n";<br />}<br />void OutputName(){<br />out<<"Circle /n/n";<br />}<br />void hello(){</p><p>out<<"hello !/n/n";<br />}<br />};</p><p>int main(void) {</p><p>CShape* shape;<br />CCircle circle;</p><p>shape = &circle;<br />shape->OutputName();<br />//shape->hello();</p><p>return 0;<br />}</p><p>

 

運行結果如下:

 

Shape

從這樣的一個小程式可以看出,如果將CCircle的對象地址賦值給它的父類CShape的指標,那麼這個指標只能調用父類CShape中的一些成員函數,而不能調用子類CCircle中的成員函數。所以可以得出下面的幾個結論:

1、 如果你以一個「基礎類別之指標」指向「衍生類別之對象」,那麼經由該指標你只能夠調用基礎類別所定義的函數。

2、 如果你以一個「衍生類別之指標」指向一個「基礎類別之對象」,你必須先做明顯的轉型動作(explicit cast)。這種作法很危險,不符合真實生活經驗,在程式設計上也會帶給程式員困惑。

3、 如果基礎類別和衍生類別都定義了「相同名稱之成員函數」,那麼透過對象指標調用成員函數時,到底調用到哪一個函數,必須視該指標的原始型別而定,而不是視指標實際所指之對象的型別而定。

 

綜上所述,我們對C++中的多態和虛函數機制,以及父類之類指標變換後的結果有了更深的認識了。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.