1. 程式人生 > >深入理解C++的多型性

深入理解C++的多型性

C++程式語言是一款應用廣泛,支援多種程式設計的計算機程式語言。我們今天就會為大家詳細介紹其中C++多型性的一些基本知識,以方便大家在學習過程中對此能夠有一個充分的掌握。

多型性可以簡單地概括為“一個介面,多種方法”,程式在執行時才決定呼叫的函式,它是面向物件程式設計領域的核心概念。多型(polymorphisn),字面意思多種形狀。

C++多型性是通過虛擬函式來實現的,虛擬函式允許子類重新定義成員函式,而子類重新定義父類的做法稱為覆蓋(override),或者稱為重寫。(這裡我覺得要補充,重寫的話可以有兩種,直接重寫成員函式和重寫虛擬函式,只有重寫了虛擬函式的才能算作是體現了C++多型性)而過載則是允許有多個同名的函式,而這些函式的引數列表不同,允許引數個數不同,引數型別不同,或者兩者都不同。編譯器會根據這些函式的不同列表,將同名的函式的名稱做修飾,從而生成一些不同名稱的預處理函式,來實現同名函式呼叫時的過載問題。但這並沒有體現多型性。

多型與非多型的實質區別就是函式地址是早繫結還是晚繫結。如果函式的呼叫,在編譯器編譯期間就可以確定函式的呼叫地址,並生產程式碼,是靜態的,就是說地址是早繫結的。而如果函式呼叫的地址不能在編譯器期間確定,需要在執行時才確定,這就屬於晚繫結。

那麼多型的作用是什麼呢,封裝可以使得程式碼模組化,繼承可以擴充套件已存在的程式碼,他們的目的都是為了程式碼重用。而多型的目的則是為了介面重用。也就是說,不論傳遞過來的究竟是那個類的物件,函式都能夠通過同一個介面呼叫到適應各自物件的實現方法。

最常見的用法就是宣告基類的指標,利用該指標指向任意一個子類物件,呼叫相應的虛擬函式,可以根據指向的子類的不同而實現不同的方法。如果沒有使用虛擬函式的話,即沒有利用C++多型性,則利用基類指標呼叫相應的函式的時候,將總被限制在基類函式本身,而無法呼叫到子類中被重寫過的函式。因為沒有多型性,函式呼叫的地址將是一定的,而固定的地址將始終呼叫到同一個函式,這就無法實現一個介面,多種方法的目的了。

例:
#include<iostream>
using namespace std;
class A
{
public:
 void foo()
 {
  printf("1\n");
 }
 virtual void fun()
 {
  printf("2\n");
 }
};
class B : public A
{
public:
 void foo()
 {
  printf("3\n");
 }
 void fun()
 {
  printf("4\n");
 }
};
int main(void)
{
 A a;

 A *p = &a;
 p->foo();
 p->fun();
 p = &b;
 p->foo();
 p->fun();
 return
0; }
第一個p->foo()和p->fuu()都很好理解,本身是基類指標,指向的又是基類物件,呼叫的都是基類本身的函式,因此輸出結果就是1、2。
第二個輸出結果就是1、4。p->foo()和p->fuu()則是基類指標指向子類物件,正式體現多型的用法,p->foo()由於指標是個基類指標,指向是一個固定偏移量的函式,因此此時指向的就只能是基類的foo()函式的程式碼了,因此輸出的結果還是1。而p->fun()指標是基類指標,指向的fun是一個虛擬函式,由於每個虛擬函式都有一個虛擬函式列表,此時p呼叫fun()並不是直接呼叫函式,而是通過虛擬函式列表找到相應的函式的地址,因此根據指向的物件不同,函式地址也將不同,這裡將找到對應的子類的fun()函式的地址,因此輸出的結果也會是子類的結果4。
另類測試方法。即
B *ptr = (B *)&a;  ptr->foo();  ptr->fun();
問這兩呼叫的輸出結果。這是一個用子類的指標去指向一個強制轉換為子類地址的基類物件。結果,這兩句呼叫的輸出結果是3,2。
並不是很理解這種用法,從原理上來解釋,由於B是子類指標,雖然被賦予了基類物件地址,但是ptr->foo()在呼叫的時候,由於地址偏移量固定,偏移量是子類物件的偏移量,於是即使在指向了一個基類物件的情況下,還是呼叫到了子類的函式,雖然可能從始到終都沒有子類物件的例項化出現。
而ptr->fun()的呼叫,可能還是因為C++多型性的原因,由於指向的是一個基類物件,通過虛擬函式列表的引用,找到了基類中fun()函式的地址,因此呼叫了基類的函式。由此可見多型性的強大,可以適應各種變化,不論指標是基類的還是子類的,都能找到正確的實現方法。
例 :
//小結:1、有virtual才可能發生多型現象
// 2、不發生多型(無virtual)呼叫就按原型別呼叫
#include<iostream>
using namespace std;
class Base
{
public:
 virtual void f(float x)
 {
  cout<<"Base::f(float)"<< x <<endl;
 }
 void g(float x)
 {
  cout<<"Base::g(float)"<< x <<endl;
 }
 void h(float x)
 {
  cout<<"Base::h(float)"<< x <<endl;
 }
};
class Derived : public Base
{
public:
 virtual void f(float x)
 {
  cout<<"Derived::f(float)"<< x <<endl;   //多型、覆蓋
 }
 void g(int x)
 {
  cout<<"Derived::g(int)"<< x <<endl;     //隱藏
 }
 void h(float x)
 {
  cout<<"Derived::h(float)"<< x <<endl;   //隱藏
 }
};
int main(void)
{
 Derived d;
 Base *pb = &d;
 Derived *pd = &d;
 // Good : behavior depends solely on type of the object
 pb->f(3.14f);   // Derived::f(float) 3.14
 pd->f(3.14f);   // Derived::f(float) 3.14
 // Bad : behavior depends on type of the pointer
 pb->g(3.14f);   // Base::g(float)  3.14
 pd->g(3.14f);   // Derived::g(int) 3 
 // Bad : behavior depends on type of the pointer
 pb->h(3.14f);   // Base::h(float) 3.14
 pd->h(3.14f);   // Derived::h(float) 3.14
 return 0;
}

令人迷惑的隱藏規則

本來僅僅區別過載與覆蓋並不算困難,但是C++的隱藏規則使問題複雜性陡然增加。
這裡“隱藏”是指派生類的函式遮蔽了與其同名的基類函式,規則如下:
(1)如果派生類的函式與基類的函式同名,但是引數不同。此時,不論有無virtual關鍵字,基類的函式將被隱藏(注意別與過載混淆)。
(2)如果派生類的函式與基類的函式同名,並且引數也相同,但是基類函式沒有virtual關鍵字。此時,基類的函式被隱藏(注意別與覆蓋混淆)。
上面的程式中:
(1)函式Derived::f(float)覆蓋了Base::f(float)。
(2)函式Derived::g(int)隱藏了Base::g(float),而不是過載。
(3)函式Derived::h(float)隱藏了Base::h(float),而不是覆蓋。 

C++純虛擬函式

一、定義
純虛擬函式是在基類中宣告的虛擬函式,它在基類中沒有定義,但要求任何派生類都要定義自己的實現方法。在基類中實現純虛擬函式的方法是在函式原型後加“=0” 
virtual void funtion()=0 
二、引入原因
1、為了方便使用多型特性,我們常常需要在基類中定義虛擬函式。 
2、在很多情況下,基類本身生成物件是不合情理的。例如,動物作為一個基類可以派生出老虎、孔雀等子類,但動物本身生成物件明顯不合常理。 
為了解決上述問題,引入了純虛擬函式的概念,將函式定義為純虛擬函式(方法:`virtual ReturnType Function()= 0;`),則編譯器要求在派生類中必須予以重寫以實現多型性。同時含有純虛擬函式的類稱為抽象類,它不能生成物件。這樣就很好地解決了上述兩個問題。
三、相似概念
1、多型性 
指相同物件收到不同訊息或不同物件收到相同訊息時產生不同的實現動作。C++支援兩種多型性:編譯時多型性,執行時多型性。 
    a、編譯時多型性(靜態多型):通過過載函式實現 
    靜態多型 使用的是靜態聯編方式,在編譯時函式和函式的實現就關聯在一起,主要是通過過載和模板實現,在巨集多型中,是通過定義變數,編譯時直接把變數替換,實現巨集多型.

    b、執行時多型性(動態多型):通過虛擬函式實現。 
    動態多型主要通過繼承和虛擬函式實現,父類指標或者引用能夠指向子類物件,呼叫子類的虛擬函式,所有在編譯時是無法確定呼叫哪個虛擬函式,每個子類都維護著一張虛擬函式表,程式執行時查詢虛擬函式表來確定呼叫哪個虛擬函式;存在這樣的多型特性,所有最好是把類的解構函式定義成virtual型別,否則在釋放時無法呼叫解構函式

2、虛擬函式 
虛擬函式是在基類中被宣告為virtual,並在派生類中重新定義的成員函式,可實現成員函式的動態覆蓋(Override)
3、抽象類 
包含純虛擬函式的類稱為抽象類。由於抽象類包含了沒有定義的純虛擬函式,所以不能定義抽象類的物件。