1. 程式人生 > 程式設計 >深入淺析C++多型性與虛擬函式

深入淺析C++多型性與虛擬函式

派生一個類的原因並非總是為了繼承或是新增新的成員,有時是為了重新定義基類的成員,使得基類成員“獲得新生”。面向物件的程式設計真正的力量不僅僅是繼承,而且還在於允許派生類物件像基類物件一樣處理,其核心機制就是多型和動態聯編。

(一)多型性

  多型是指同樣的訊息被不同的物件接收時導致不同的行為。所謂訊息是指對類成員函式的呼叫,不同的行為是指的不同的實現,也就是呼叫了不同的函式。

1)多型的分類

  廣義上說,多型性是指一段程式能夠處理多種型別物件的能力。在C++中,這種多型性可以通過過載多型(函式和運算子過載),強制過載(型別強制轉換),型別引數化多型(模板)

,包含多型(繼承與虛擬函式)四種方式來實現。型別引數化多型和包含多型稱為一般多型性,是用來系統地刻畫語義上相關的一組型別;過載多型和強制多型性稱為特殊多型性,用來刻畫語義上無關連的型別間關係。

  C++中採用虛擬函式實現包含多型。虛擬函式為C++提供了更為靈活的多型機制,這種多型性在程式執行時才能夠確定,因此虛擬函式是多型性的精華,至少含有一個虛擬函式的類稱為多型類。包含多型在面向物件的程式設計中使用很頻繁。

2)靜態聯編

  聯編又稱為繫結,就是將模組或函式合併在一起生成可執行程式碼的處理過程,同時對每個模組或函式分配記憶體地址,對外部訪問也提供正確的記憶體地址。

  在編譯階段就將函式實現與函式呼叫繫結起來稱為靜態聯編。靜態聯編在編譯階段就必須瞭解所有函式與模組執行所需要的資訊,它對函式的選擇是基於指向物件的指標(或引用)的型別。在C語言中所有的聯編都是靜態聯編;C++中一般情況也是靜態聯編。

class Point{
  public:
    void area(){cout<<"point";}
  };
  class Circle:public Point{
  public:
    void area(){cout<<"circle";}
  };
  Point a; Circle c;
  a.area();   //呼叫a.Point::area() 
  c.area();   //呼叫c.Circle::area(),名字支配規則
  Point * pc=&c,&rc=c;  //上篇所講的賦值相容性規則
  pc->area();  //呼叫pc->Point::area()
  rc.area();   //呼叫rc.Point::area()

3)動態聯編

  如果程式在執行時候才進行函式實現和函式呼叫的繫結稱為動態聯編。以上面的例子為例,在編譯時如果只根據相容性規則檢查它的合理性,即檢查它是否符合派生類物件地址可以賦值給基類指標變數的條件。至於pc->area()呼叫哪個函式等到程式執行到這裡才做決定。如果希望其呼叫Circle::area(),那麼需要將Point類的area()函式指定為虛擬函式。定義形式為:

  virtual void area(){cout<<"point";}

  當編譯器編譯含有虛擬函式的類時候,將為他建立一個虛擬函式表VTABLE,它相當於一個指標陣列,存放每一個虛擬函式的入口地址。編譯器為該類增減一個額外的資料成員,這個資料成員是一個指向虛擬函式表的指標,稱為vptr。

  如果派生類沒有重寫這個虛擬函式,則派生類的虛擬函式列表裡元素指向的地址就是基函式area()的地址,即派生類僅僅繼承基類的虛擬函式

  如果派生類重新寫這個虛擬函式如下:

  virtual void area() {cout<<"circle";}

  那麼這時編譯器將派生類虛擬函式表裡的元素指向Circle::area()

   編譯器為含有虛擬函式的物件先建立一個函式入口地址,這個地址用來存放指向虛擬函式表的指標vptr,然後按照類中虛擬函式的宣告次序一一填入函式指標。當呼叫虛擬函式時候,先通過vptr找到虛擬函式表,然後找出虛擬函式真正的地址。

  派生類能夠繼承基類的虛擬函式表,而且只要是和基類同名(引數也相同)的成員函式,無論是否使用virtual宣告,它們都自動生成虛擬函式。如果派生類沒有改寫繼承基類的虛擬函式,則函式指標將呼叫基類的虛擬函式。 

(二)虛擬函式

1)虛擬函式定義

  虛擬函式只是類中的一個成員函式,且不能是靜態的。在成員函式定義或宣告之前加上關鍵字virtual,即定義了虛擬函式:

class類名{
    ...
    virtual 返回型別 函式名 (形式引數列表)//虛擬函式
    ...
  };
  class Point
  {
    virtual void area ();    //虛擬函式宣告
    virtual double volumn(){}  //虛擬函式定義 
  };

  需要注意virtual關鍵字只在類體中使用。

  利用虛擬函式可以在基類和派生類中使用相同的函式名定義函式不同的實現,從而實現“一個介面,多種方式”。當基類指標或引用對虛擬函式進行訪問時,系統將根據執行時指標或引用所指向或引用的實際物件來自動確定呼叫物件所在類的虛擬函式版本。

2)虛擬函式實現多型的條件

  關鍵字virtual指示C++編譯器對呼叫虛擬函式進行動態聯編。這種多型性是程式執行到相應語句才動態確定的,稱為執行時的多型。不過,使用虛擬函式不一定產生多型性,也不一定使用動態聯編。例如,在呼叫中對虛擬函式使用成員名限定,可以強制C++對該函式的呼叫使用靜態聯編。

  虛擬函式產生執行時的多型性必須有2個條件。

  a)派生類改寫了同名的虛擬函式

  b)根據賦值相容性規則使用指標或引用

Point *p=new Circle;  //基類指標指向派生類
  cout<<p->area();   //動態聯編
  void fun(Point *p)
  {cout<<p->area();}   //動態聯編

3)在一個派生類中,當一個指向基類成員函式的指標指向一個虛擬函式,並且通過指向物件的指標或引用訪問這個虛擬函式時候將發生多型性。

#include<iostream>
using namespace std;
class Base{
public: virtual void print(){cout<<"base"<<endl;}
};

class Derived :public Base{
public:
 void print(){cout<<"derive"<<endl;}
};

//void(Base::*pf)();
void display(Base *p,void(Base::*pf)())
{
(p->*pf)();

}

int main()
{
 Derived d;
 Base b;
 display(&d,&Base::print);
 display(&b,&Base::print);
 return 0;
}

[email protected]:~/classic_lib/C++_learning$ g++ 427.cpp
[email protected]:~/classic_lib/C++_learning$ ./a.out 
derive
base

  display有兩個函式,第一個引數是基類指標,第二個引數是指向類成員函式的指標。display使用基類指標呼叫指向成員函式的指標所指向的成員函式。是呼叫基類的虛擬函式還是派生類的虛擬函式,取決於基類指標指向的物件。 

補充:

面向物件的三個特徵:封裝、繼承和多型。

什麼是多型

多型的意思是一個事物有多種形態,英文單詞為polymorphism,向不同的物件傳送同一個訊息,不同的物件在接收時會產生不同的行為(方法)。也就是說,每個物件可以用自己的方式去相應共同的訊息。

例如函式的過載、運算子的過載都是多型現象。

一個生活中的例子,比如學生開學,校長髮布一條哪一天開學的資訊,不同的物件會產生不同的反應,學生就要準備上學,家長要準備學費,老師也要開始備課,學校食堂開始採購食材,這就是多型性,如果沒有多型性的話,校長就需要分別對學生、教師和家長等不同的物件單獨發通知。

在C++中,多型性表現形式之一是:具有不同功能的函式可以用同一個函式名,這樣就可以實現用一個函式名呼叫不同內容的函式。

從系統實現的角度來看,多型性分為兩類:靜多型性和動多型性
靜多型性是通過函式過載實現的,動多型性是通過虛擬函式實現的。

什麼是虛擬函式

C++中的虛擬函式就是用來解決動態多型問題的,所謂虛擬函式,就是在基類宣告函式是虛擬的,並不是實際存在的,然後在派生類中才正式定義此函式,在程式執行期間,用指標指向某一派生類物件,這樣就能呼叫指標指向的派生類物件中的函式,而不對呼叫其他派生類中的函式。

總結

以上所述是小編給大家介紹的C++多型性與虛擬函式,希望對大家有所幫助!