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

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

多態(tài)

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

虛函數(shù)

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

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

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

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

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

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

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

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

聲明虛函數(shù)后,派生類對(duì)象的地址可以賦值給基類指針,也就是基類指針可以指向派生類對(duì)象。
對(duì)于通過(guò)基類指針調(diào)用基類和派生類中都有的同名、同參數(shù)表的虛函數(shù)的語(yǔ)句,編譯時(shí)系統(tǒng)并不確定要執(zhí)行的是基類還是派生類的虛函數(shù);
而當(dāng)程序運(yùn)行到該語(yǔ)句時(shí),
如果基類指針指向的是一個(gè)基類對(duì)象,則調(diào)用基類的虛函數(shù);
如果基類指針指向的是一個(gè)派生類對(duì)象,則調(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指針指向基類對(duì)象a
    B *pb = &b;//派生類pb指針指向基類對(duì)象b
    
    pa->Print();//多態(tài),目前指向基類對(duì)象a,調(diào)用a.Print()
    
    pa = pb;//派生類指針賦值給基類指針,pa指向派生類對(duì)象b
    pa->Print();//多態(tài),目前指向派生類對(duì)象b,調(diào)用b.Print()
    
    pa = &d;//基類指針pa指向派生類對(duì)象d
    pa->Print();//多態(tài),目前指向派生類對(duì)象d,調(diào)用d.Print()
    
    pa = &e;//基類指針pa指向派生類對(duì)象e
    pa->Print();//多態(tài),目前指向派生類對(duì)象e,調(diào)用e.Print()

    return 0;
};

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

通過(guò)基類指針調(diào)用虛函數(shù)時(shí)可以實(shí)現(xiàn)多態(tài),通過(guò)基類的引用調(diào)用虛函數(shù)的語(yǔ)句也是多態(tài)的。
即通過(guò)基類的引用調(diào)用基類和派生類中同名、同參數(shù)表的虛函數(shù)時(shí),
若其引用的是一個(gè)基類的對(duì)象,則調(diào)用的是基類的虛函數(shù);
若其引用的是一個(gè)派生類的對(duì)象,則調(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)用哪個(gè)Print(),取決于r引用了哪個(gè)類的對(duì)象
    r.Print();
}

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

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

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

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

多態(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)用哪個(gè)類中的版本,所以它不是多態(tài)的;
  • 在析構(gòu)函數(shù)中調(diào)用的,所以也不是多態(tài);
  • 實(shí)現(xiàn)多態(tài)時(shí),必須滿足的條件是:使用基類指針或引用來(lái)調(diào)用基類中聲明的虛函數(shù)。
  • 派生類中繼承自基類的虛函數(shù),可以寫(xiě)virtual關(guān)鍵字,也可以省略這個(gè)關(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() {//自動(dòng)成為虛函數(shù)
        cout << "B::func2" << endl;
    };
};

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

int main() {
    C c;
    A *pa = &c;
    B *pb = &c;
    
    pa->func2();//多態(tài) C::func2
    pa->func1();//因?yàn)榛惖膄unc1不是虛函數(shù),這也的調(diào)用也不是多態(tài) A::func1
    pb->func1();//多態(tài) C::func1

    return 0;
};

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

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

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

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

  • 如果一個(gè)類的虛構(gòu)函數(shù)是虛函數(shù),則由他派生的所有子類的析構(gòu)函數(shù)也是虛析構(gòu)函數(shù)。使用虛析構(gòu)函數(shù)的目的是為了在對(duì)象消亡時(shí)實(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)鍵字聲明,都自動(dòng)成為虛析構(gòu)函數(shù)
  • 一般來(lái)說(shuō),一個(gè)類如果定了虛函數(shù),則最好將析構(gòu)函數(shù)也定義成虛函數(shù)。不過(guò)切記,構(gòu)造函數(shù)不能是虛函數(shù)

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

純虛函數(shù)

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

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

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

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

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

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

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

抽象類

包含純虛函數(shù)的類稱為抽象類。因?yàn)槌橄箢愔杏猩形赐瓿傻暮瘮?shù)定義,所以它不能實(shí)例化一個(gè)對(duì)象。抽象類的派生類中,如果沒(méi)有給出全部純虛函數(shù)的定義,則派生類繼續(xù)是抽象類。直到派生類中給出全部純虛函數(shù)定義后,它才不再是抽象類,也才能實(shí)例化一個(gè)對(duì)象。****雖然不能創(chuàng)建抽象類的對(duì)象,但可以定義抽象類的指針和引用。這樣的指針和引用可以指向并訪問(wèn)派生類的成員,這種訪問(wèn)具有多態(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;           //?,抽象類不能實(shí)例化
    //A *pa = new A; //?,不能創(chuàng)建抽象類類A的示例
    //B b[2];        //?,不能聲明抽象類的數(shù)組

    A *pa;         //?,可以聲明抽象類的指針
    A *pb = new B; //使用基類指針指向派生類對(duì)象
    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++提供虛基類機(jī)制,使得在派生類中,繼承同一個(gè)間接基類的成員僅保留一個(gè)版本。

#include <iostream>
using namespace std;

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

class B : virtual public A {//對(duì)類A進(jìn)行了虛繼承
public:
    int b;
};

class C : virtual public A {//對(duì)類A進(jìn)行了虛繼承
public:
    int c;
};

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

int main() {
    D dObj;     //聲明派生類D的對(duì)象
    dObj.a = 11;//若不是虛繼承,這里會(huì)報(bào)錯(cuò)!因?yàn)椤癉::a”具有二義性
    dObj.b = 22;
    
    dObj.showa();//a = 11
    //若不是虛繼承,這里會(huì)報(bào)錯(cuò)!因?yàn)椤癉::showa”具有二義性
    
    cout << "dObj.b = " << dObj.b << endl;//dObj.b = 22

    return 0;
};
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容