六、多態(tài)與虛函數(shù)

多態(tài)的基本概念

多態(tài)

  • 多態(tài)分為編譯時多態(tài)和運行時多態(tài)
  • 編譯時多態(tài)主要是指函數(shù)的重載(包括運算符的重載)。對重載函數(shù)的調(diào)用,在編譯時就可以根據(jù)實參確定應(yīng)該調(diào)用哪個函數(shù),因此稱為編譯時多態(tài)。
  • 運行時多態(tài)則和繼承、虛函數(shù)等概念有關(guān)。本章中提及的多態(tài)主要是指運行時多態(tài)。
  • 程序編譯階段都早于程序運行階段,所以靜態(tài)綁定稱為早綁定,動態(tài)綁定稱為晚綁定。靜態(tài)多態(tài)和動態(tài)多態(tài)的區(qū)別,只在于在什么時候?qū)⒑瘮?shù)實現(xiàn)和函數(shù)調(diào)用關(guān)聯(lián)起來,是在編譯階段還是在運行階段,即函數(shù)地址是早綁定的還是晚綁定的。
  • 在類之間滿足賦值兼容的前提下,實現(xiàn)動態(tài)綁定必須滿足以下兩個條件:
    1. 必須聲明虛函數(shù)
    2. 通過基類類型的引用或者指針調(diào)用虛函數(shù)

虛函數(shù)

  • 所謂“虛函數(shù)”,就是在函數(shù)聲明時前面加了virtual關(guān)鍵字的成員函數(shù)。virtual關(guān)鍵字只在類定義中的成員函數(shù)聲明處使用,不能在類外部寫成員函數(shù)體時使用。靜態(tài)成員函數(shù)不能是虛函數(shù)。包含虛函數(shù)的類稱為“多態(tài)類”。

  • 聲明虛函數(shù)成員的一般格式如下:

    virtual 函數(shù)返回值類型 函數(shù)名(行參表);
    
  • 在類的定義中使用virtual關(guān)鍵字來限定的成員函數(shù)即稱為虛函數(shù)。再次強調(diào)一下,虛函數(shù)的聲明只能出現(xiàn)在類定義中的函數(shù)原型聲明時,不能在類外成員函數(shù)實現(xiàn)的時候。

  • 派生類可以繼承基類的同名函數(shù),并且可以在派生類中重寫這個函數(shù)。如果不使用虛函數(shù),當(dāng)使用派生類對象調(diào)用這個函數(shù),且派生類中重寫了這個函數(shù)時,則調(diào)用派生類中的同名函數(shù),即“隱藏”了基類中的函數(shù)。

  • 當(dāng)然,如果還想調(diào)用基類的函數(shù),只需在調(diào)用函數(shù)時,在前面加上基類名及作用域限定符即可。

關(guān)于虛函數(shù),有以下幾點需要注意

  1. 雖然將虛函數(shù)聲明為內(nèi)聯(lián)函數(shù)不會引起錯誤,但因為內(nèi)聯(lián)函數(shù)是在編譯階段進行靜態(tài)處理的,而對虛函數(shù)的調(diào)用是動態(tài)綁定的,所以虛函數(shù)一般不聲明為內(nèi)聯(lián)函數(shù)。
  2. 派生類重寫基類的虛函數(shù)實現(xiàn)多態(tài),要求函數(shù)名。參數(shù)列表及返回值類型要完全相同。
  3. 基類中定義了虛函數(shù),在派生類中該函數(shù)始終保持虛函數(shù)的特性。
  4. 只有類的非靜態(tài)成員函數(shù)才能定義為虛函數(shù),靜態(tài)成員函數(shù)和友元函數(shù)不能定義為虛函數(shù)。
  5. 如果虛函數(shù)的定義是在類體外,則只需在聲明函數(shù)時添加virtual關(guān)鍵字,定義時不加virtual關(guān)鍵字。
  6. 構(gòu)造函數(shù)不能定義為虛函數(shù)。最好也不要將operator=定義為虛函數(shù),因為使用時容易混淆。
  7. 不要在構(gòu)造函數(shù)和析構(gòu)函數(shù)中調(diào)用虛函數(shù)。在構(gòu)造函數(shù)和析構(gòu)函數(shù)中,對象是不完整的,可能會出現(xiàn)未定義的行為。
  8. 最好將基類的析構(gòu)函數(shù)聲明為虛函數(shù)。

通過基類指針實現(xiàn)多態(tài)

聲明虛函數(shù)后,派生類對象的地址可以賦值給基類指針,也就是基類指針可以指向派生類對象。
對于通過基類指針調(diào)用基類和派生類中都有的同名、同參數(shù)表的虛函數(shù)的語句,編譯時系統(tǒng)并不確定要執(zhí)行的是基類還是派生類的虛函數(shù);
而當(dāng)程序運行到該語句時,
如果基類指針指向的是一個基類對象,則調(diào)用基類的虛函數(shù);
如果基類指針指向的是一個派生類對象,則調(diào)用派生類的虛函數(shù)。

#include <iostream>
using namespace std;

class A {
public:
    virtual void Print() {
        cout << "A::Print" << endl;
    }
};

class B : public A {
public:
    virtual void Print() {
        cout << "B::Print" << endl;
    }
};

class D : public A {
public:
    virtual void Print() {
        cout << "D::Print" << endl;
    }
};

class E : public B {
public:
    virtual void Print() {
        cout << "E::Print" << endl;
    }
};

int main() {
    A a;
    B b;
    D d;
    E e;
    
    A *pa = &a;//基類pa指針指向基類對象a
    B *pb = &b;//派生類pb指針指向基類對象b
    
    pa->Print();//多態(tài),目前指向基類對象a,調(diào)用a.Print()
    
    pa = pb;//派生類指針賦值給基類指針,pa指向派生類對象b
    pa->Print();//多態(tài),目前指向派生類對象b,調(diào)用b.Print()
    
    pa = &d;//基類指針pa指向派生類對象d
    pa->Print();//多態(tài),目前指向派生類對象d,調(diào)用d.Print()
    
    pa = &e;//基類指針pa指向派生類對象e
    pa->Print();//多態(tài),目前指向派生類對象e,調(diào)用e.Print()

    return 0;
};

通過基類引用實現(xiàn)多態(tài)

通過基類指針調(diào)用虛函數(shù)時可以實現(xiàn)多態(tài),通過基類的引用調(diào)用虛函數(shù)的語句也是多態(tài)的。
即通過基類的引用調(diào)用基類和派生類中同名、同參數(shù)表的虛函數(shù)時,
若其引用的是一個基類的對象,則調(diào)用的是基類的虛函數(shù);
若其引用的是一個派生類的對象,則調(diào)用的是派生類的虛函數(shù)。

#include <iostream>
using namespace std;

class A {
public:
    virtual void Print() {
        cout << "A::Print" << endl;
    }
};

class B : public A {
public:
    virtual void Print() {
        cout << "B:Print" << endl;
    }
};

void PrintInfo(A &r) {
    //多態(tài),使用基類引用調(diào)用哪個Print(),取決于r引用了哪個類的對象
    r.Print();
}

int main() {
    A a;
    B b;
    
    PrintInfo(a);//使用基類對象,調(diào)用基類中的函數(shù)
    PrintInfo(b);//使用派生類對象,調(diào)用派生類中的函數(shù)
    
    return 0;
}

多態(tài)的實現(xiàn)原理

多態(tài)的關(guān)鍵在于通過基類指針或引用調(diào)用一個虛函數(shù)時,編譯階段不能確定到底調(diào)用的是基類還是派生類的函數(shù),運行時才能確定。

派生類對象占用的存儲空間大小,等于基類成員變量占用的存儲空間大小加上派生類對象自身成員變量占用的存儲空間大小。

多態(tài)的使用

在普通成員函數(shù)(靜態(tài)成員函數(shù)、構(gòu)造函數(shù)和析構(gòu)函數(shù)除外)中調(diào)用其他虛成員函數(shù)也是允許的,并且是多態(tài)的。

#include <iostream>
using namespace std;

class CBase {
public:
    void func1() {
        cout << "CBase::func1()" << endl;
        func2();//在成員函數(shù)中調(diào)用虛函數(shù)
        func3();
    };
    virtual void func2() {
        cout << "CBase::func2()" << endl;
    };
    void func3() {
        cout << "CBase::func3()" << endl;
    };
};

class CDerived : public CBase {
public:
    virtual void func2() {
        cout << "CDerived::func2()" << endl;
    };
    void func3() {
        cout << "CDerived::func3()" << endl;
    };
};

int main() {
    CDerived d;
    d.func1();
    //CBase::func1()
    //CDerived::func2()
    //CBase::func3()

    return 0;
};

不僅能在成員函數(shù)中調(diào)用虛函數(shù),還可以在構(gòu)造函數(shù)和析構(gòu)函數(shù)中調(diào)用虛函數(shù),但這樣調(diào)用的虛函數(shù)不是多態(tài)的。

#include <iostream>
using namespace std;

class A {
public:
    virtual void hello() {
        cout << "A::hello" << endl;
    };
    virtual void bye() {
        cout << "A::bye" << endl;
    };
};

class B : public A {
public:
    virtual void hello() {
        cout << "B::hello" << endl;
    };
    B() {
        hello();//調(diào)用虛函數(shù),但不是多態(tài)
    };
    ~B() {
        bye();//調(diào)用虛函數(shù),但不是多態(tài)
    };
};

class C : public B {
public:
    virtual void hello() {
        cout << "C::hello" << endl;
    };
};

int main() {
    C c;
    //B::hello
    //A::bye

    return 0;
};
  • 在構(gòu)造函數(shù)中調(diào)用的,編譯系統(tǒng)可以據(jù)此決定調(diào)用哪個類中的版本,所以它不是多態(tài)的;
  • 在析構(gòu)函數(shù)中調(diào)用的,所以也不是多態(tài);
  • 實現(xiàn)多態(tài)時,必須滿足的條件是:使用基類指針或引用來調(diào)用基類中聲明的虛函數(shù)。
  • 派生類中繼承自基類的虛函數(shù),可以寫virtual關(guān)鍵字,也可以省略這個關(guān)鍵字,這不影響派生類中的函數(shù)也是虛函數(shù)。
#include <iostream>
using namespace std;

class A {
public:
    void func1() {
        cout << "A::func1" << endl;
    };
    virtual void func2() {//虛函數(shù)
        cout << "A::func2" << endl;
    };
};

class B : public A {
public:
    virtual void func1() {
        cout << "B::func1" << endl;
    };
    void func2() {//自動成為虛函數(shù)
        cout << "B::func2" << endl;
    };
};

class C : public B {
public:
    void func1() {//自動成為虛函數(shù)
        cout << "C::func1" << endl;
    };
    void func2() {//自動成為虛函數(shù)
        cout << "C::func2" << endl;
    };
};

int main() {
    C c;
    A *pa = &c;
    B *pb = &c;
    
    pa->func2();//多態(tài) C::func2
    pa->func1();//因為基類的func1不是虛函數(shù),這也的調(diào)用也不是多態(tài) A::func1
    pb->func1();//多態(tài) C::func1

    return 0;
};

虛析構(gòu)函數(shù)

  • 如果一個基類指針指向的對象是用new運算符動態(tài)生成的派生類對象,那么釋放該對象所占用的空間時,如果僅調(diào)用基類的析構(gòu)函數(shù),則只會完成該析構(gòu)函數(shù)內(nèi)的空間釋放,不會涉及派生類析構(gòu)函數(shù)內(nèi)的空間釋放,容易造成內(nèi)存泄露。

  • 聲明虛析構(gòu)函數(shù)的一般格式如下:

    virtual ~類名();
    
  • 虛析構(gòu)函數(shù)沒有返回值類型,沒有參數(shù),所以它的格式非常簡單。

  • 如果一個類的虛構(gòu)函數(shù)是虛函數(shù),則由他派生的所有子類的析構(gòu)函數(shù)也是虛析構(gòu)函數(shù)。使用虛析構(gòu)函數(shù)的目的是為了在對象消亡時實現(xiàn)多態(tài)。

#include <iostream>
using namespace std;

class ABase {
public:
    ABase() {
        cout << "ABase構(gòu)造函數(shù)" << endl;
    };
    virtual ~ABase() {
        cout << "ABase::析構(gòu)函數(shù)" << endl;
    };
};

class Derived : public ABase {
public:
    Derived() {
        cout << "Derived構(gòu)造函數(shù)" << endl;
    };
    ~Derived() {
        cout << "Derived::析構(gòu)函數(shù)" << endl;
    };
};

int main() {
    ABase *a = new Derived();
    delete a;
    //ABase構(gòu)造函數(shù)
    //Derived構(gòu)造函數(shù)
    //Derived::析構(gòu)函數(shù)
    //ABase::析構(gòu)函數(shù)

    return 0;
};
  • 可以看出,不僅調(diào)用了基類的析構(gòu)函數(shù),也調(diào)用了派生類的析構(gòu)函數(shù)
  • 只要基類的析構(gòu)函數(shù)是虛函數(shù),那么派生類的析構(gòu)函數(shù)不論是否用virtual關(guān)鍵字聲明,都自動成為虛析構(gòu)函數(shù)
  • 一般來說,一個類如果定了虛函數(shù),則最好將析構(gòu)函數(shù)也定義成虛函數(shù)。不過切記,構(gòu)造函數(shù)不能是虛函數(shù)

純虛函數(shù)和抽象類

純虛函數(shù)

  • 純虛函數(shù)的作用相當(dāng)于一個統(tǒng)一的接口形式,表明在基類的各派生類中應(yīng)該有這樣的一個操作,然后在各派生類中具體實現(xiàn)與本派生類相關(guān)的操作。

  • 純虛函數(shù)是聲明在基類中的虛函數(shù),沒有具體的定義,而由個派生類根據(jù)實際需要給出各自的定義。

  • 聲明純虛函數(shù)的一般格式如下:

    virtual 函數(shù)類型 函數(shù)名(參數(shù)表) = 0;
    
  • 純虛函數(shù)沒有函數(shù)體,參數(shù)標(biāo)后要寫= 0。派生類中必須重寫這個函數(shù)。按照純虛函數(shù)名調(diào)用時,執(zhí)行的是派生類中重寫的語句,即調(diào)用的是派生類中的版本。

純虛函數(shù)不同于函數(shù)體為空的虛函數(shù),
它們的不同之處如下:

  1. 純虛函數(shù)沒有函數(shù)體,而空的虛函數(shù)的函數(shù)體為空
  2. 純虛函數(shù)所在的類是抽象類,不能直接進行實例化;而空的虛函數(shù)所在的類是可以實例化的。

它們的共同特點是:
純虛函數(shù)與函數(shù)體為空的虛函數(shù)都可以派生出新的類,然后在新類中給出虛函數(shù)的實現(xiàn),而且這種新的實現(xiàn)具有多態(tài)特征。

抽象類

包含純虛函數(shù)的類稱為抽象類。因為抽象類中有尚未完成的函數(shù)定義,所以它不能實例化一個對象。抽象類的派生類中,如果沒有給出全部純虛函數(shù)的定義,則派生類繼續(xù)是抽象類。直到派生類中給出全部純虛函數(shù)定義后,它才不再是抽象類,也才能實例化一個對象。****雖然不能創(chuàng)建抽象類的對象,但可以定義抽象類的指針和引用。這樣的指針和引用可以指向并訪問派生類的成員,這種訪問具有多態(tài)性。

#include <iostream>
using namespace std;

class A {
public:
    virtual void Print() = 0;//純虛函數(shù)
    void func1() {
        cout << "A_func1" << endl;
    };
};

class B : public A {
public:
    void Print();
    void func1() {
        cout << "B_func1" << endl;
    };
};
void B::Print() {
    cout << "B_print" << endl;
};

int main() {
    //A a;           //?,抽象類不能實例化
    //A *pa = new A; //?,不能創(chuàng)建抽象類類A的示例
    //B b[2];        //?,不能聲明抽象類的數(shù)組

    A *pa;         //?,可以聲明抽象類的指針
    A *pb = new B; //使用基類指針指向派生類對象
    pb->Print();   //多態(tài),調(diào)用的是類B中的函數(shù),B_print
    
    B b;
    A *pb1 = &b;
    pb1->func1();//不是虛函數(shù),調(diào)用的是類A中的函數(shù),A_func1
    
    return 0;
};

虛基類

定義虛基類的一般格式如下:

class 派生類名 : virtual 派生方式 基類名 {
    派生類體
};

多重繼承的模型結(jié)構(gòu)圖如下:

多重繼承

為了避免產(chǎn)生二義性,C++提供虛基類機制,使得在派生類中,繼承同一個間接基類的成員僅保留一個版本。

#include <iostream>
using namespace std;

class A {
public:
    int a;
    void showa() {
        cout << "a = " << a << endl;
    };
};

class B : virtual public A {//對類A進行了虛繼承
public:
    int b;
};

class C : virtual public A {//對類A進行了虛繼承
public:
    int c;
};

class D : public B, public C {
//派生類D的兩個基類B、C具有共同的基類A
//采用了虛繼承,從而使類D的對象中只包含著類A的一個示例
public:
    int d;
};

int main() {
    D dObj;     //聲明派生類D的對象
    dObj.a = 11;//若不是虛繼承,這里會報錯!因為“D::a”具有二義性
    dObj.b = 22;
    
    dObj.showa();//a = 11
    //若不是虛繼承,這里會報錯!因為“D::showa”具有二義性
    
    cout << "dObj.b = " << dObj.b << endl;//dObj.b = 22

    return 0;
};
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,953評論 2 373