多態


多態,以專業術語來講,多態是一種運行期綁定(run-time binding)機制,通過這種機制,實現將函數名綁定到函數具體實現代碼目的。

多態就是就是將函數名稱動態地綁定到函數入口地址的運行期綁定機制

 

一個函數的名稱和其入口地址是緊密相連的,入口地址是該函數在內存中的起始地址

由於函數被調用時,到底應該執行哪一段代碼是由編譯器在編譯階段就決定了的,因此我們將這種對函數的綁定方式稱為編譯器綁定(compile-time  bindinig):專業術語:編譯器將所以對函數的調用綁定到函數的入口地址

 

與編譯器綁定不同的時,運行期綁定是直到程序運行之時,才將函數名稱綁定到其入口地址。

如果對一個函數的綁定發生在運行期而非編譯器,我們就稱該函數是   多態

在Smalltalk這樣的純面向對象語言中,所有函數都是多態的

在C++這樣混合語言中,函數既可以是多態的,也可以是非多態的,這要由綁定的時機是編譯時刻還是運行時刻來決定

在C++中,只有滿足某些特定條件的成員函數才可能是多態的

 

C++中多態有以下三個前提條件:

1,必須存在一個繼承體系結構

2,繼承體系結構中的一些類必須具有同名的virtual成員函數(virtual是關鍵字)

3,至少有一個基類類型的指針或基類類型的引用。這個指針或引用可用來對virtual成員函數進行調用

看例子:

 

結果:

 

基類類型的指針可以指向任何基類對象或派生類對象

 

再看一個:

結果:

 

在sayHi的三個版本中,程序都使用了關鍵字virtual。這對練習使用virtual關鍵字是有益的,但實際上並沒有必要,因為當聲明了基類的一個成員函數為虛函數后,那么即使該成員函數沒有在派生類中被顯式地聲明為虛函數,但它在所有派生類中也將自動稱為虛函數。

如果在派生類中sayHi成員函數的聲明沒有使用關鍵字virtual,該派生類的用戶為了確定它是否為虛函數,不得不檢查sayHi在基類中的聲明,將函數在所有派生類中聲明為虛函數,就可以避免這種不便

如果虛函數在類聲明外定義,關鍵字僅在函數聲明時需要,不需在函數定義中使用virtual關鍵字

 

C++僅允許將成員函數定義為虛函數,頂層函數(可以理解為全局函數,因為不在類中,所以定義為虛函數沒有什么意義)不能為虛函數

 

在派生類中虛成員函數也可以從基類繼承

 

C++使用vtable(虛成員函數表)來實現虛成員函數的運行期綁定。虛成員函數表存在的用途是支持運行時查詢,使得系統可以將某一函數名綁定到虛成員函數表中的特定入口地址。需成員函數表的實現是與系統無關的。

 

使用動態綁定的程序會影響效率,因為虛成員函數表需要額外的存儲空間,而且對虛成員函數表進行查詢也需要額外的時間。

純面向對象語言由於所有的函數都以動態方式運行,因而效率的降低會相當大,而在C++中,程序員可以選擇性的執行哪些函數是虛成員函數,因而既不會導致太大的效率降低,又充分利用了運行期綁定機制

class B{

public:

          virtual void m1() { /*...*/}

      virtual void m2() { /*...*/ }

};

class D : public B{

public:

         virtual void m1() {/*...*/}

};

 

構造函數不能是虛成員函數(因為如果是虛函數,必須在派生類提供一個同名的函數覆蓋,或者不提供而繼承下來,這兩種情況都沒有必要,因為構造函數只使用於類本身)

析構函數可以是虛成員函數

 

 

虛函數必須是基類函數中的非靜態函數,訪問權限可以是public或者protected

 

看一個例子分析為什么析構函數可以為虛函數:

#include <iostream>
using namespace std;
class A{
public:
    A(){
        cout << endl << "A() firing" << endl;
        p = new char[5];
    }
    ~A(){
        cout << "~A() firing" << endl;
        delete []p;
    }
private:
    char *p;
};
class Z : public A{
public:
    Z() {
        cout << "Z() firing" << endl;
        q = new char[5000];
    }
    ~Z() {
        cout << "~Z() firinig" << endl;
        delete []q;
    }
private:
    char *q;
};
void f();
int main()
{
    for(unsigned i = 0 ;i < 3 ;i ++)
        f();
    return 0;
}

void f(){
    A *ptr;
    ptr = new Z();
    delete ptr;
}

結果:

 

 

看出來沒有!!

上述new操作符將導致構造函數A()和Z()被調用(Z的構造函數沒有顯式調用A的構造函數,但編譯器會確保A的默認構造函數被調用),當我們通過ptr進行delete操作時,盡管ptr實際指向一個Z的對象,但只有~A()被調用,這是因為它們的析構函數不是虛函數,所以編譯器實施的是靜態綁定。編譯器根據ptr的數據類型A*來決定調用哪一個析構函數,因此,僅調用了~A(),而沒有調用~Z(),這樣Z()中分配的5000字節就不會被釋放。

看修改后的程序:

#include <iostream>
using namespace std;
class A{
public:
    A(){
        cout << endl << "A() firing" << endl;
        p = new char[5];
    }
virtual    ~A(){
        cout << "~A() firing" << endl;
        delete []p;
    }
private:
    char *p;
};
class Z : public A{
public:
    Z() {
        cout << "Z() firing" << endl;
        q = new char[5000];
    }
    ~Z() {
        cout << "~Z() firinig" << endl;
        delete []q;
    }
private:
    char *q;
};
void f();
int main()
{
    for(unsigned i = 0 ;i < 3 ;i ++)
        f();
    return 0;
}

void f(){
    A *ptr;
    ptr = new Z();
    delete ptr;
}
結果:

 

當然也可以把~Z()定義為virtual了,前面講過

現在由於析構函數已經聲明為虛成員函數,當通過ptr來刪除其所指向的對象時,編譯器進行的是運行時期綁定。在這里,因為ptr指向Z類型的對象,所以~Z()被調用;我們隨后看到~A()也被調用了,這是因為析構函數的調用是沿着繼承樹自下而上延伸的。通過將析構函數定義為虛函數,我們就保證了在調用f時不會產生內存遺漏

 

通常來說,如果基類有一個指向動態分配內存的數據成員,並定義了負責釋放這塊內存的析構函數,就應該將這個析構函數聲明為虛成員函數,這樣做可以保證在以后添加該類的派生類時發揮多態性的作用。

只有非靜態成員函數才可以是虛成員函數。換句話說,只有對象成員函數才可以是虛成員函數。

 

 

看兩個例子:

 

 

 

 

下面看一個大的程序,充分體現了虛成員函數和多態:

程序總括:

1,提供一個多態的成員函數input,可以從某個輸入文件中讀入有關Films、DirectorCuts和ForeignFilms的記錄,輸入文件中的每條記錄都可映射到某個Film類層次中的對象,假設輸入文件的格式是正確的

2,依據輸入文件中的記錄功能動態的創建Film類層次對象

3,將Film類層次中的input函數設計為虛函數,使其具有多態性。動態創建的對象通過input函數可從輸入流中正確的讀入數據

4,將Film類層次中的output函數設計為虛函數,使其具有多態性。動態創建的對象通過output函數可將信息正確的輸出到標准輸出流

 

頭文件films.h

#include <iostream>
#include <fstream>
#include <string>
#include <cctype>

using namespace std;

class Film
{
public:
   Film()
   {
    store_title();
    store_director();
    store_time();
    store_quality();
   }
   void store_title(const string &t) { title = t;}
   void store_title(const char * t = "") { title = t;}
   void store_director(const string &d) { director = d;}
   void store_director(const char * d = "") { director = d;}
   void store_time(int t=0) { time = t;}
   void store_quality(int q = 0) { quality = q;}
   virtual void output();
   virtual void input(ifstream &);
   static bool read_input(const char *,Film *[],int);
private:
   string title;
   string director;
   int time;
   int quality;
};

void Film::input(ifstream & fin)
{
   string inbuff;
   getline(fin,inbuff);
   store_title(inbuff);
   getline(fin,inbuff);
   store_director(inbuff);
   getline(fin,inbuff);
   store_time(atoi(inbuff.c_str()));
   getline(fin,inbuff);
   store_quality(atoi(inbuff.c_str()));
}

void Film::output()
{
   cout << "Title: " << title << endl;
   cout << "Director: " << director <<endl;
   cout << "Time: " << time << " mins" << endl;
   cout << "Quality: ";
   for(int i = 0 ; i < quality ; i ++)
      cout << '*';
   cout << endl;
}

class DirectorCut : public Film
{
public:
   DirectorCut()
   {
      store_rev_time();
      store_changes();
   }
   void store_rev_time(int t=0) { rev_time = t;}
   void store_changes(const string &c) {changes = c;}
   void store_changes(const char *c = "") { changes = c; }
   virtual void output();
   virtual void input(ifstream &);
private:
   int rev_time;
   string changes;
};

void DirectorCut::input(ifstream &fin)
{
   Film::input(fin);
   string inbuff;
   getline(fin,inbuff);
   store_rev_time(atoi(inbuff.c_str() ));
   getline(fin,inbuff);
   store_changes(inbuff);
}

void DirectorCut::output()
{
   Film::output();
   cout << "Revised: time: " << rev_time << endl;
   cout << "Changes: " << changes << endl;
}

class ForeignFilm : public Film
{
public:
   ForeignFilm() { store_language(); }
   void store_language(const string &l) { language = l;}
   void store_language(const char *l = "") {language = l;}
   virtual void output();
   virtual void input(ifstream &);
private:
   string language;
};

void ForeignFilm::input(ifstream & fin)
{
   Film::input(fin);
   string inbuff;
   getline(fin,inbuff);
   store_language(inbuff);
}

void ForeignFilm::output()
{
   Film::output();
   cout << "Language: " << language << endl;
}

bool Film::read_input(const char *file,Film * films[],int n)
{
   string inbuff;
   ifstream fin(file);
   if(!fin)
    return false;
   int next = 0;
 
   while(getline(fin,inbuff) && next < n)
   {
      if(inbuff == "Film")
         films[next] = new Film();
      else if( inbuff == "ForeignFilm")
         films[next] = new ForeignFilm();
      else if( inbuff == "DirectorCut")
         films[next] = new DirectorCut();
      else
         continue;
      films[next ++] -> input(fin);
   }
   fin.close();
   return true;
}

 

測試文件和結果:

 

我們所描述的多態函數指的是運行期進行綁定的函數,在C++中,僅有虛函數是在運行期進行綁定的,因此,僅有虛函數才具有真正意義上的多態

 

參數個數和類型不同的同名函數被成為重載函數,都是頂層函數或者都是成員函數 ====== 重載與編譯期綁定相對應,編譯器依據函數簽名來進行綁定

 

假定基類B有一個成員函數m,其派生類D也有一個具有相同函數簽名的成員函數m,如果這個成員函數是虛函數,則任何通過指針或引用對m的調用都會激活運行期綁定。對於這種情況,叫做派生類的成員函數D::m覆蓋了其基類的成員函數B::m。如果成員函數不是虛函數,對m的任何調用均為編譯期綁定

下面看下面這若干例子:注意代碼的細節:(有很多情況沒有必要甚至重復,僅僅是列出來,做個比較,參考)

1,

2,

3,

4,

5,

6,

7,

8,

9,

10,

11,

12,

13,

14,

15,

16,

17,

18,

19,

20,

21,

22,

23,

24,

只有知識點清楚,上面的都能解釋!

 

 假定基類B擁有一個非虛函數m,其派生類D也有一個成員函數m,我們說函數D::m遮蔽了繼承而來的函數B::m。如果派生類的同名成員函數與其基類的這個成員有不同的函數簽名,那么這種遮蔽情況會相當復雜

先看一個例子:

看修改過的,之所以能修復,是因為是遮蔽,不是覆蓋!!!

 

虛函數和非虛函數都有可能產生名字遮蔽,實際上一旦派生類的虛函數不能覆蓋基類的虛函數,就會產生虛函數遮蔽

eg:

與此相對比,再來看2個:

1,NO1

2,NO2

看出點什么沒有?

 

名字共享:

上面可以看到函數共享一個函數名,可能會引發一些問題,然而有時我們又希望幾個函數共享一個函數名。

下面幾種情況就需要共享函數名:

1,重載函數名的頂層函數。對於程序員來說,只使用一個函數名就可以執行不同的函數體,非常方便。也就是說,對於函數名相同但函數簽名不相同的函數,使用起來是非常便利的。另外,我們通常將一些操作符設計為頂層重載函數 (可能指的是非類成員函數)

2,重載構造函數。一個類經常有幾個構造函數,這種情況也需要函數重載

3,非構造函數是同一個類中名字相同的成員函數。這種方法和頂層函數的方式一樣,知識處於不同的域

4,繼承層次中的同名函數(特別是虛函數)。為了發揮多態性的作用,虛函數必須具有相同的函數簽名(具有相同的函數名)。在典型的多態情況下,派生類的虛函數覆蓋了從基類繼承來的虛函數,要形成覆蓋,成員函數必須為函數簽名相同的虛函數

 

在類層次中共享函數名但函數簽名不同時,將產生遮蔽,而遮蔽通常是非常危險的,因此要謹慎地運行這種遮蔽類型的名字共享機制

 

抽象基類確保其派生類必須定義某些指定的函數,否則這個派生類就不能被實例化

抽象基類之所以是抽象的,是因為不能實例化抽象基類,抽象基類可以用來指明某些必須被派生類覆蓋的虛函數,如果這些派生類想要擁有對象的話。只有符合下面條件的類才可以稱為抽象基類:

      類必須擁有一個純虛成員函數

在虛成員函數聲明的結尾加上=0就可將這個函數定義為純虛成員函數

雖然不能創建一個抽象基類的對象,但抽象基類可以擁有派生類,從抽象基類派生出來的類必須覆蓋基類的所有純虛成員函數,否則派生類也是抽象類,因而也不能用來創建對象

 

一個純虛成員函數就可以使一個類成為抽象基類,一個抽象基類可以有其他不是純虛成員函數或甚至不是虛函數的成員函數,還可以有數據成員。

抽象基類的成員可以使private、protected或public

 

只有虛函數才可以成為純虛成員函數,非虛函數或頂層函數都不能聲明為純虛成員函數

 

抽象基類的作用很大,通過這種機制,可以用來指明某些虛函數必須被派生類覆蓋,否則這些派生類就不能擁有對象。從這種意義上來看,抽象基類實際上定義了一個公共接口,這個接口被所有從抽象基類派生的類共享

因為抽象基類通常只有public成員函數,所以經常使用關鍵字struct來聲明抽象基類

 

微軟的IUnknown接口:

微軟的COM(Componet Object Model)模型提供了一種應用程序構造框架

IUnknown是一個標准的COM接口

 

C++支持運行期類型識別(RTTI  Run-Time Type Identification),運行期類型識別提供如下功能:

1,在運行期對類型轉換操作進行檢查

2,在運行期確定對象的類型

3,擴展C++提供RTTI

 

在C++中,編譯期合法的類型轉換操作可能會在運行期引發錯誤,當轉換操作涉及對象指針或引用時,更易發生錯誤。使用dynamic_cast操作符可用來在運行期對可疑的轉換操作進行測試:

 

一個基類指針不經過明確的轉換操作,就能指向基類或派生類對象;反過來就大不一樣了額,將一個派生類指針指向基類對象是一種相當不明智的做法。當然,通過明確的轉型操作可以強制地做的這一點:

 

static_cast是合法的,但這種轉型操作是相當危險,可能會造成難以跟蹤的運行期錯誤

 

過程雖然不會導致編譯錯誤,不過請注意,由於p現在指向一個B的對象,而B並沒有成員函數m,這就導致一個運行期錯誤

因此我們發現static_cast不能保證類型安全

 

C++的dynamic_cast操作符可以再運行期檢查某個轉型動作是否類型安全。

dynamic_cast和static_cast有相同的語法,不過dynamic_cast僅對多態類型(即至少有一個虛函數的類)有效

上面這段代碼有問題,因為它對非多態類型C(因為C不含虛函數)實施了dynamic_cast操作。dynamic_cast操作是否正確與轉型的目標類型是否多態無關,但轉型的源類型必須是多態。

可做下面的改正:

class C{

public:

          virtual void m() {};

};

在<>中指定的dynamic_cast的目的類型必須是一個指針或引用。假設T是一個類,那么T*和T&對dynamic_cast來說都是有效的目的類型,而T不是

 

 

看下面代碼:

編譯時會出現一個警告:

     warning C4541: 'dynamic_cast' used on polymorphic type 'class B' with /GR-; unpredictable behavior

執行的時候出現:

如果dynamic_cast成功的話,p將會指向動態創建的B對象,若dynamic_cast失敗p為NULL。本例失敗

 

看一個完整示例:

#include <iostream>
#include <string>
using namespace std;

class Book {
public:
   Book(string t) {title = t;}
   virtual void printTitle() const{
      cout << "Title: " << title << endl; }
private:
   Book();
   string title;
};

class Textbook : public Book {
public:
   Textbook(string t,int l) : Book(t),level(l) {}
   void printTitle() const {
      cout << "Textbook " ;
      Book::printTitle();
   }
   void printLevel() const {
      cout << "Book level: " << level << endl; }
private:
   Textbook();
   int level;
};

class PulpFiction : public Book{
public:
   PulpFiction(string t) : Book(t) {}
   void printTitle() const{
      cout << "Pulp " ;
      Book::printTitle();
   }
private:
   PulpFiction();
};

void printBookInfo(Book *);

int main()
{
   Book * ptr;
   int level;
   string title;
   int ans;
   cout << "Book's titles? (no white space) ";
   cin >> title;
   do{
      cout << "1 == Textbook, 2 == PulpFiction " << endl;
      cin >> ans;
   }while( ans < 1 || ans > 2);
   if(1 == ans){
      cout << "Level ?";
      cin >> level;
      ptr = new Textbook(title,level);
   }
   else
      ptr = new PulpFiction(title);
   printBookInfo(ptr);
   return 0;
}

void printBookInfo(Book * bookPtr)
{
   bookPtr -> printTitle();
   Textbook * ptr = dynamic_cast<Textbook*>(bookPtr);
   if(ptr)
      ptr -> printLevel();
}

 

dynamic_cast的規則:

  dynamic_cast的規則很復雜,特別是在多重繼承或進行與void*類型相關的轉型操作時。在此主要討論最基本的情況,簡單起見,我們用指針而不是引用來說明這些規則。

在單繼承的類層次中,假定基類B具有多態性,而類D是直接或間接從類B派生而來的。通過繼承,類D也因此具有多態性,在這種情況下:

1,從派生類D*到基類B*的dynamic_cast可以進行,這稱為向上轉型(upcast)

2,從基類B*到派生類D*的dynamic_cast不能進行,這稱為向下轉型(downcast)

假定類A和類Z都具有多態性,但它們之間不存在繼承關系,這種情況下:

1,從A*到Z*的dynamic_cast不能進行

2,從Z*到A*的dynamic_cast不能進行

 

通常來說,向上轉型可以成功,而向下轉型不能成功。除了void *之外,無關類型之間的dynamic_cast也不會成功。

 

dynamic_cast與static_cast小結:

C++提供了不同的轉換機制。一個static_cast可施加於任何類型,不管該類型是否具有多態性,dynamic_cast只能施加於具有多態性的類型,而且轉換的目的類型必須是指針或引用。基於這個原因,static_cast比dynamic_cast的應用更廣。但由於只有dynamic_cast才能實施運行期類型安全檢查,因此從這個角度來說,dynamic_cast的功能更強大

 

操作符typeid可用來確定某個表達式的類型,要使用這個操作符,必須包含頭文件typeinfo

操作符typeid返回一個type_info類對象的引用,type_info是一個系統類,用來描述類型,這個操作符可施加於類型名(包括類名)或C++表達式。

 

下面仔細分析下面 bookPtr為Book*,但是typeid(*bootPtr)返回值代表對象類型Textbook

 

語法正確但運行出錯的轉型動作不是類型安全的

 

看下面一個例子

 

typeid不能用於多態!!!

看下面一個例子!!!!

編譯時:

 

運行時:

 

有人將多態分強多態和弱多態兩種。強多態是指對覆蓋的虛函數進行運行期綁定處理。弱多態行則有兩種形式:頂層函數火成員函數的重載和類層次中非虛函數的函數名共享

我們則用多態這個術語來描述覆蓋的虛函數在運行期的綁定,而對C++結構中看似是運行期綁定而實際是編譯器綁定的情況使用重載、函數名共享等術語

 

 

常見的編程錯誤:::

1,只有成員函數才可以聲明為虛函數,頂層函數不能為虛函數

2,靜態成員函數不能為虛函數

3,如果在類聲明之外定義一個虛函數,只需在聲明時使用關鍵字virtual,而在定義時不需要使用virtual

4,聲明任何構造函數為虛函數都是錯誤的,但析構函數可以是虛函數

5,如果一個成員函數遮蔽了繼承而來的成員函數,不指定其全名來調用繼承的成員函數會導致錯誤

6,不能對非虛函數進行運行期綁定

7,如果派生類虛函數與基類虛函數簽名不同,不能覆蓋

8,不能創建一個抽象基類的對象

9,如果抽象基類的純虛函數沒有全部實現,則該派生類也是抽象類,不能創建對象

10,dynamic_cast只能作用於多態性類型(至少有一個虛函數的類)

11,dynamic_cast的目的類型必須是指針或引用

12,typeid操作符不能針對非多態類型進行運行期類型檢查

 

 

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM