MOOC C++筆記(六):多態

多態

虛函數

在類的定義中,前面有virtual關鍵字的成員函數就是虛函數。
virtual關鍵字只用在類定義裏的函數聲明中,寫函數體時不用。
構造函數和靜態成員函數不能是虛函數。

多態的表現形式

基類指針指向派生類

派生類的指針可以賦給基類指針。
通過基類指針調用基類和派生類中的同名虛函數時:
(1)若該指針指向一個基類的對象,那麼被調用的是基類的虛函數。
(2)若該指針指向一個派生類的對象,那麼被調用的是派生類的虛函數。
這種機制叫做"多態"。

    class CBase
    {
    public:
        virtual void SomeVirtualFunction(){}
    };
    class CDerived :public CBase
    {
    public:
        virtual void SomeVirtualFunction() {}
    };
    int main()
    {
        CDerived ODerived;
        CBase*p = &ODerived;
        p->SomeVirtualFunction();//調用哪個函數取決於p指向哪個類的對象。
        return 0;
    }

基類引用派生類對象

派生類的對象可以賦給基類引用
通過基類引用調用基類和派生類中的同名虛函數時:
(1)若該引用引用的是一個基類的對象,那麼被調用的基類的虛函數。
(2)若該引用引用的是一個派生類的對象,那麼被調用的是派生類的虛函數。
這種機制也叫做“多態”。

    class CBase
    {
    public:
        virtual void SomeVirtualFunction(){}
    };
    class CDerived :public CBase
    {
    public:
        virtual void SomeVirtualFunction() {}
    };
    int main()
    {
        CDerived ODerived;
        CBase&r = ODerived;
        r.SomeVirtualFunction();//調用哪個函數取決於r引用哪種類型的對象。
        return 0;
    }

非構造函數和非析構函數中調用虛函數

    class Base
    {
    public:
        void fun1() { this->fun2; }
        virtual void fun2() { cout << "Base::fun2()" << endl; }
    };
    class Derived :public Base
    {
    public:
        virtual void fun2() { cout << "Derived::fun2()" << endl; }
    };
    int main()
    {
        Derived d;
        Base*pBase = &d;
        pBase->fun1();//輸出Derived::fun2(),調用哪一個fun2,取決於指向的對象類型。
        return 0;
    }

在非構造函數和非析構函數中調用虛函數,所調用的虛函數是哪一個取決於其指向的對象類型或者其引用的對象類型。
注意:在析構函數和構造函數中調用虛函數不是多態。編譯時即可確定,調用的函數是自己的類或基類中定義的函數,不會等到運行時才決定調用自己的還是派生類的函數。
另外,多態要保證派生函數的形式參數要和基類的相同,派生類的函數可以virtual不進行聲明,但是基類的函數如果是用來實現多態的話一定要用virtual聲明。

多態的實現原理

動態的關鍵在於通過基類指針或引用調用一個虛函數時,編譯時不確定到底調用的是基類還是派生類的函數,運行時來確定----這叫“動態聯編”。

虛函數表

每一個有虛函數的類(或有虛函數的類的派生類)都有一個虛函數表,該類的任何對象中都放在虛函數表的指針。虛函數表中列出了該類的虛函數地址。
每一個有虛函數的類都會多出來4個字節(具體取決於機器字長),這些多出來的字節就是來放虛函數表的地址的(通常放在類的存儲空間的頭部)。
多態的函數調用語句被編譯成一系列根據基類指針所指向的(或基類引用所引用的)對象中存放的虛函數表的地址,在虛函數表中查找虛函數地址,並調用虛函數的指令。
多態的實現需要額外付出一些時間和空間上的開銷。

通過改變虛函數表地址來改變多態時指向的虛函數(幫助理解多態的實現原理)

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

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

    int main(void)
    {
        A a;
        A*pa = new B;
        pa->Func();
        //64位程序指針爲8字節
        long long *p1 = (long long*)&a;
        long long *p2 = (long long *)pa;
        *p2 = *p1;
        pa->Func();
        system("pause");
    }

輸出結果爲:
B::Func
A::Func
這裏第二個Func的調用的是A中的Func只是因爲我們使用了指針改變了該對象的虛函數表地址。

虛析構函數

通過基類的指針刪除派生類對象時,通常情況下只調用基類的析構函數,但是刪除一個派生類的對象時,應該先調用派生類的析構函數,然後調用基類的析構函數。
解決方法是把基類的析構函數聲明爲virtual:
派生類的析構函數可以virtual不進行聲明
通過基類的指針刪除派生類的析構函數,然後調用基類的析構函數。
一般來說,一個類如果定義了虛函數,則應該將析構函數也定義成虛函數。或者,一個類打算作爲基類使用,也應該將析構函數定義成虛函數。

純虛函數和抽象類

純虛函數

沒有函數體地虛函數,具體寫法爲virtual void Fun()=0

抽象類

包含純虛函數的類叫抽象類。
抽象類還能作爲基類來派生新類使用,不能創建抽象類的對象。
抽象類的指針和引用可以指向由抽象類派生出來的類的對象。(可以定義抽象類的指針和引用,但是不能創建類的實體)。
在抽象類的成員函數內可以調用純虛函數,但是在構造函數或者析構函數內部不能調用純虛函數(這是因爲析構函數和構造函數不屬於多態,而調用自己爲空的純虛函數是沒有意義的)。
如果一個類從抽象派生而來,那麼當且僅當它實現類基類中的所有純虛函數,它才能成爲非抽象類。

多態、重載和覆蓋的區別

詳見博客:多態,重載,覆蓋區別與聯繫

多態的作用

在面向對象的抽象設計中使用多態,能夠增強抽象的可擴充性,即程序需要修改或增加功能的時候,需要改動和增加的代碼較少。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章