C++基礎-多態

本文爲 C++ 學習筆記,參考《Sams Teach Yourself C++ in One Hour a Day》第 8 版、《C++ Primer》第 5 版、《代碼大全》第 2 版。ios

多態(Polymorphism)是面嚮對象語言的一種特徵,可能使用類似的方式(基類中的接口)處理不一樣類型的對象。在編碼時,咱們將不一樣類型(具備繼承層次關係的基類和派生類)的對象視爲基類對象進行統一處理,沒必要關注各派生類的細節,在運行時,將會經過相應機制執行各對象所屬的類中的方法。多態是一種很是強大的機制,咱們考慮這種狀況,基類早已寫好並定義了良好的接口,基類的使用者編寫代碼時,將能經過基類的接口來調用派生類中的方法,也就是說,後寫的代碼能被先寫的代碼調用,這使程序具備很強的複用性和擴展性。程序員

1. 使用虛函數實現多態

看以下例程:編程

#include <iostream>
using namespace std;

class Fish
{
public:
    /* virtual */ void Swim() { cout << "Fish swims!" << endl; }
};

class Tuna : public Fish
{
public:
    void Swim() { cout << "Tuna swims!" << endl; }
};

class Carp : public Fish
{
public:
    void Swim() { cout << "Carp swims!" << endl; }
};

void FishSwim(Fish &fish)
{
    fish.Swim();
}

int main() 
{
    // 引用形式
    Fish myFish;
    Tuna myTuna;
    Carp myCarp;
    FishSwim(myFish);
    FishSwim(myTuna);
    FishSwim(myCarp);

    // 引用形式
    Fish &rFish1 = myFish;
    Fish &rFish2 = myTuna;
    Fish &rFish3 = myCarp;
    rFish1.Swim();
    rFish2.Swim();
    rFish3.Swim();

    // 指針形式
    Fish *pFish1 = new Fish();
    Fish *pFish2 = new Tuna();
    Fish *pFish3 = new Carp();
    pFish1->Swim();
    pFish2->Swim();
    pFish3->Swim();

    return 0;
}

直接編譯運行代碼,獲得以下結果:數組

Fish swims!
Fish swims!
Fish swims!
Fish swims!
Fish swims!
Fish swims!
Fish swims!
Fish swims!
Fish swims!

將第 7 行的 virtual 關鍵字取消註釋,再次編譯運行代碼,獲得以下結果:ide

Fish swims!
Tuna swims!
Carp swims!
Fish swims!
Tuna swims!
Carp swims!
Fish swims!
Tuna swims!
Carp swims!

分析上述例程:函數

  1. 派生類對象能夠賦值給基類對象(這裏對象是廣義稱法,代指對象、指針、引用),例程中使用基類引用或指針指向派生類對象
  2. 若是基類中的 Swim() 不是虛函數,那麼不管基類引用(或指針)指向何種類型的對象,運行時都調用基類中的方法。這種狀況未啓用多態機制
  3. 若是基類中的 Swim() 是虛函數,那麼運行時會根據基類引用(或指針)指向的具體對象,調用對象所屬的類中的方法。若指向派生類對象則調用派生類方法,若指向基類對象則調用基類方法。這種狀況使用了多態機制

使用基類指針或引用指向基類或派生類對象,運行時調用對象所屬的類(具備繼承層次關係的基類或派生類)中的方法,這就是多態。在編寫代碼時,可將派生類對象視爲基類對象進行統一處理,據此咱們能夠先實現一個通用接口,如第 29 行 FishSwim() 函數所示,運行時具體調用哪一個方法由傳入的參數決定。學習

編程實踐:對於將被派生類覆蓋的基類方法,務必將其聲明爲虛函數,以使其支持多態。this

2. 虛析構函數

虛析構函數與普通虛函數機制並沒有不一樣。編碼

若是不將析構函數聲明爲虛函數,那麼若是一個函數的形參是基類指針,實參是指向堆內存的派生類指針時,函數返回時做爲實參的派生類指針將被看成基類指針進行析構,這會致使資源釋放不徹底和內存泄漏;要避免這一問題,可將基類的析構函數聲明爲虛函數,那麼函數返回時,做爲實參的派生類指針就會被看成派生類指針進行析構。spa

換句話說,對於使用 new 在堆內存中實例化的派生類對象,若是將其賦給基類指針,並經過基類指針調用 delete,若是基類析構函數不是虛函數,delete 將按基類析構的方式來析構此指針,若是基類析構函數是虛函數,delete 將按派生類析構的方式來析構此指針(調用派生類析構函數和基類析構函數)。

編程實踐:務必要將基類的析構函數聲明爲虛函數,以免派生類實例未被妥善銷燬的狀況發生。

3. 多態機制的工做原理-虛函數表

爲方便說明,將第一節代碼加以修改,以下:

#include <iostream>
using namespace std;

class Fish
{
public:
   virtual void Swim() { cout << "Fish swims!" << endl; }
};

class Tuna : public Fish
{
public:
   void Swim() { cout << "Tuna swims!" << endl; }
};

class Carp:public Fish
{
public:
   void Swim() { cout << "Carp swims!" << endl; }
};

int main() 
{
   Fish *pFish = NULL;

   pFish = new Fish();
   pFish->Swim();

   pFish = new Tuna();
   pFish->Swim();

   pFish = new Carp();
   pFish->Swim();

   return 0;
}

編譯運行的輸出結果爲:

Fish swims!
Tuna swims!
Carp swims!

例程中使用統一類型(基類)的指針 pFish 指向不一樣類型(基類或派生類)的對象,指針的賦值是在運行階段執行的,在編譯階段,編譯器把 pFish 認做 Fish 類型的指針,而並不知道 pFish 指向的是哪一種類型的對象,沒法肯定將執行哪一個類中的 Swim() 方法。調用哪一個類中的 Swim() 方法顯然是在運行階段決定的,這是使用實現多態的邏輯完成的,而這種邏輯由編譯器在編譯階段提供。

3.1 虛函數表機制

以下 Base 類聲明瞭 N 個虛函數:

class Base
{
public:
    virtual void Func1() { // Func1 implementation }
    virtual void Func2() { // Func2 implementation }
    // .. so on and so forth
    virtual void FuncN() { // FuncN implementation }
};

以下 Derived 類繼承自 Base 類,並覆蓋了除 Base::Func2() 外的其餘全部虛函數。

class Derived: public Base
{
public:
    virtual void Func1() { // Func2 overrides Base::Func2() }
    // no implementation for Func2()
    // .. so on and so forth
    virtual void FuncN() { // FuncN overrides Base::FuncN() }
};

編譯器見到這種繼承層次結構後,知道 Base 定義了一些虛函數,並在 Derived 中覆蓋了它們。在這種狀況下,編譯器將爲實現了虛函數的基類和覆蓋了虛函數的派生類分別建立一個虛函數表(Virtual Function Table, VFT)。換句話說,Base 和 Derived 類都將有本身的虛函數表。實例化這些類的對象時,會爲每一個對象建立一個隱藏的指針(咱們稱之爲 VFT*),它指向相應的 VFT。可將 VFT 視爲一個包含函數指針的靜態數組,其中每一個指針都指向相應的虛函數,以下圖所示:

基類和派生類的虛函數表

每一個虛函數表都由函數指針組成,其中每一個指針都指向相應虛函數的實現。在類 Derived 的虛函數表中,除一個函數指針外,其餘全部函數指針都指向 Derived 本地的虛函數實現。Derived 沒有覆蓋 Base::Func2(),所以相應的函數指針指向 Base 類的 Func2() 實現。

下述代碼調用未覆蓋的虛函數,編譯器將查找 Derived 類的 VFT,最終調用的是 Base::Func2() 的實現:

Derived objDerived;
objDerived.Func2();

調用被覆蓋的虛函數時,也是相似的機制:

void DoSomething(Base& objBase)
{
    objBase.Func1(); // invoke Derived::Func1
}
int main()
{
    Derived objDerived;
    DoSomething(objDerived);
};

在這種狀況下,雖然將 objDerived 傳遞給了 objBase,進而被解讀爲一個 Base 實例,但該實例的 VFT 指針仍指向 Derived 類的虛函數表,所以經過該 VTF 執行的是 Derived::Func1()。

C++ 就是經過虛函數表實現多態的。

3.2 類實例中的 VFT 指針

#include <iostream>
using namespace std;

class Class1
{
private:
    int a, b;

public:
    void DoSomething() {}
};

class Class2
{
private:
    int a, b;

public:
    virtual void DoSomething() {}
};

int main() 
{
    cout << "sizeof(Class1) = " << sizeof(Class1) << endl;
    cout << "sizeof(Class2) = " << sizeof(Class2) << endl;

    return 0;
}

在 64 位系統下編譯並運行,結果爲:

sizeof(Class1) = 8
sizeof(Class2) = 16

Class2 中將函數聲明爲虛函數,所以類的成員多了一個 VFT 指針,64 位系統中,指針變量佔用 8 字節空間,所以 Class2 比 Class1 多佔用了 8 個字節。

3.3 繼承關係中的 VFT 指針

#include <iostream>
using namespace std;

class Base
{
private:
    int x = 1;
    int y = 2;
    const static int z = 3;

/* 註釋1
public:
    virtual void test() {};
*/
};

class Derived : public Base
{
private:
    int u = 11;
    int v = 22;
    const static int w = 33;

/* 註釋2
public:
    virtual void test() {};
*/
};

int main()
{
    Base base;
    Derived derived;

    cout << "sizeof(Base) = " << sizeof(Base) << endl;
    cout << "sizeof(Derived) = " << sizeof(Derived) << endl;

    return 0;
}

上述代碼運行結果爲:

sizeof(Base) = 8
sizeof(Derived) = 16

使用 gdb 查看變量值:

(gdb) p base
$1 = {x = 1, y = 2, static z = 3}
(gdb) p derived
$2 = {<Base> = {x = 1, y = 2, static z = 3}, u = 11, v = 22, static w = 33}

取消「註釋1」處的註釋,運行結果爲:

sizeof(Base) = 16
sizeof(Derived) = 24

使用 gdb 查看變量值:

(gdb) p base
$1 = {_vptr.Base = 0x400b10 <vtable for Base+16>, x = 1, y = 2, static z = 3}
(gdb) p derived
$2 = {<Base> = {_vptr.Base = 0x400af0 <vtable for Derived+16>, x = 1, y = 2, static z = 3}, u = 11, v = 22, static w = 33}

取消「註釋1」和「註釋2」處的註釋,運行結果爲:

sizeof(Base) = 16
sizeof(Derived) = 24

使用 gdb 查看變量值:

(gdb) p base
$1 = {_vptr.Base = 0x400b10 <vtable for Base+16>, x = 1, y = 2, static z = 3}
(gdb) p derived
$2 = {<Base> = {_vptr.Base = 0x400af0 <vtable for Derived+16>, x = 1, y = 2, static z = 3}, u = 11, v = 22, static w = 33}

根據上述實驗結果,給出結論:

  1. 只要基類含有虛函數,基類和派生類對象都會含有各自的 VFT 指針,即便派生類沒有虛函數。若是派生類沒有虛函數,那麼派生類虛函數表中的每一個元素都指向基類的的虛函數。
  2. 派生類對象只含一份 VFT 指針,基類的私有成員都會在派生類對象中佔用內存,但基類的 VFT 指針不會在派生類中佔用內存。從打印能夠看出,VFT 指針爲 _vptr.Base,派生類的 VFT 指針存在在派生類的 Base 部分,也能夠認爲派生類的 VFT 指針覆蓋了基類的 VFT 指針,指向本身的虛函數表。

4. 純虛函數和抽象基類

在 C++ 中,包含純虛函數的類是抽象基類。抽象基類用於定義接口,在派生類中實現接口,這樣能夠實現接口與實現的分離。抽象基類不能被實例化。抽象基類提供了一種很是好的機制,可在基類聲明全部派生類都必須實現的函數接口,將這些派生類中必須實現的接口聲明爲純虛函數便可。

純虛函數寫法以下:

class AbstractBase
{
public:
    virtual void DoSomething() = 0; // pure virtual method
};

其派生類中必須實現此函數。

分析下列代碼:

#include <iostream>
#include <stdio.h>
using namespace std;

class B
{
public:
    virtual void func1() = 0;   // 純虛函數不能在基類中實現,必定要在派生類中實現
    virtual void func2() = 0;   // 純虛函數不能在基類中實現,必定要在派生類中實現
    virtual void func3() { cout << "B::func3" << endl; }    // 此虛函數被派生類中函數覆蓋
    virtual void func4() { cout << "B::func4" << endl; }    // 此虛函數在派生類中無覆蓋
            void func5() { cout << "B::func5" << endl; }    // 此函數被派生類中函數覆蓋
            void func6() { cout << "B::func6" << endl; }    // 此函數在派生類中無覆蓋

private:
    int x = 1;
    int y = 2;
    static int z;
};

class D : public B
{
public:
    virtual void func1() override { cout << "D::func1" << endl; }
    virtual void func2() override { cout << "D::func2" << endl; }
    virtual void func3() override { cout << "D::func3" << endl; }
            void func5()          { cout << "D::func5" << endl; }   // 不能帶 override

private:
    int u = 11;
    int v = 22;
    static int w;
};

int main()
{
    // B b;  // 編譯錯誤,抽象基類不能被實例化
    D d;

    cout << "sizeof(d) = " << sizeof(d) << endl;
    d.func1();      // 訪問派生類中的覆蓋函數(覆蓋純虛函數)
    d.func2();      // 訪問派生類中的覆蓋函數(覆蓋純虛函數)
    d.func3();      // 訪問派生類中的覆蓋函數(覆蓋虛函數)
    d.func5();      // 訪問派生類中的覆蓋函數(覆蓋普通函數)
    d.B::func3();   // 訪問基類中的虛函數
    d.B::func4();   // 訪問基類中的虛函數
    d.B::func5();   // 訪問基類中的普通函數

    return 0;
}

上述代碼運行結果:

sizeof(d) = 24
D::func1
D::func2
D::func3
D::func5
B::func3
B::func4
B::func5

結論以下:

  1. 類中只要有一個純虛函數,這個類就是抽象基類,不能被實例化
  2. 基類中的純虛函數,基類不能給出實現,必須在派生類中實現,即必定要有派生類中覆蓋基類的純虛函數
  3. 基類中的虛函數,基類中要給出實現,派生類可實現也可不實現,即派生類須要覆蓋基類中的虛函數
  4. 基類中的普通函數,基類中要給出實現,派生類可實現也可不實現。普通函數不支持多態,因此須要繼承的函數應聲明爲虛函數,不該使用普通函數

5. 使用虛繼承解決菱形問題

一個類繼承多個父類,而這多個父類又繼承一個更高層次的父類時,會引起菱形問題。例如,鴨嘴獸具有哺乳動物、鳥類和爬行動物的特徵,這意味着 Platypus 類須要繼承 Mammal、 Bird 和 Reptile 三個類。然而,這些類都從同一個 Animal 類派生而來,以下圖所示:

多繼承的菱形問題

例程以下:

#include <iostream>
using namespace std;

class Animal
{
public:
    Animal() { cout << "Animal constructor" << endl; }
    int age;
};

class Mammal : public /* virtual */ Animal
{
};

class Bird : public /* virtual */ Animal
{
};

class Reptile : public /* virtual */ Animal
{
};

class Platypus : public Mammal, public Bird, public Reptile
{
public:
    Platypus() { cout << "Platypus constructor" << endl; }
};

int main()
{
    Platypus duckBilledP;

    // uncomment next line to see compile failure
    // age is ambiguous as there are three instances of base Animal 
    // duckBilledP.age = 25;

    duckBilledP.Mammal::age = 25;
    duckBilledP.Bird::age = 25;
    duckBilledP.Reptile::age = 25;

    return 0;
}

編譯並運行,輸出結果以下:

Animal constructor
Animal constructor
Animal constructor
Platypus constructor

可見,Platypus 有三個 Animal 實例。若是取消第 35 行的註釋,編譯沒法經過,由於沒法肯定是要設置哪一個 Animal 實例中的 age 成員。

若是取消第 十一、1五、19 行對關鍵字 virtual 的註釋,再次編譯運行,可看到以下輸出結果:

Animal constructor
Platypus constructor

此時,Platypus 只有一個 Animal 實例。可見使用虛繼承能夠解決多繼承時的菱形問題,確保

在繼承層次結構中,繼承多個從同一個類派生而來的基類時,若是這些基類沒有采用虛繼承,將致使二義性。這種二義性被稱爲菱形問題(Diamond Problem)。

C++關鍵字 virtual 被用於實現兩個不一樣的概念,其含義隨上下文而異,以下:

  1. 在函數聲明中, virtual 意味着當基類指針指向派生對象時,經過它可調用派生類的相應函數。
  2. 從 Base 類派生出 Derived1 和 Derived2 類時,若是使用了關鍵字 virtual,則意味着再從 Derived1 和 Derived2 派生出 Derived3 時,每一個 Derived3 實例只包含一個 Base 實例。

6. 使用 override 明確代表覆蓋意圖

從 C++11 起,程序員可以使用限定符 override 來覈實被覆蓋的函數在基類中是否被聲明爲虛函數。形式以下:

class Fish
{
public:
    virtual void Swim()
    {
        cout << "Fish swims!" << endl;
    }
};

class Tuna:public Fish
{
public:
    void Swim() const override  // Error: no virtual fn with this sig in Fish
    {
        cout << "Tuna swims!" << endl;
    }
    void Swim() override        // Right: has virtual fn with this sig in Fish
    {
        cout << "Tuna swims!" << endl;
    }
};

換而言之, override 提供了一種強大的途徑,讓程序員可以明確地表達對基類的虛函數進行覆蓋的意圖,進而讓編譯器作以下檢查:
• 基類函數是不是虛函數?
• 派生類中被聲明爲 override 的函數是不是基類中對應虛函數的覆蓋?確保沒有有手誤寫錯。

編程實踐:在派生類中聲明要覆蓋基類函數的函數時,務必使用關鍵字 override

7. 使用 final 禁止覆蓋

被聲明爲 final 的類禁止繼承,不能用做基類。而被聲明爲 final 的虛函數,不能在派生類中進行覆蓋。

所以,要在 Tuna 類中禁止進一步定製虛函數 Swim(),可像下面這樣作:

class Tuna:public Fish
{
public:
    // override Fish::Swim and make this final
    void Swim() override final
    {
        cout << "Tuna swims!" << endl;
    }
};

Tuna 類能夠被繼承,但 Swim() 函數不能派生類中的實現覆蓋。

8. 可將複製構造函數聲明爲虛函數嗎

答案是不能夠。不可能實現虛複製構造函數,由於在基類方法聲明中使用關鍵字 virtual 時,表示它將被派生類的實現覆蓋,這種多態行爲是在運行階段實現的。而構造函數只能建立固定類型的對象,不具有多態性,所以 C++不容許使用虛複製構造函數。

雖然如此,但存在一種不錯的解決方案,就是定義本身的克隆函數來實現上述目的。這部份內容有些複雜,待用到時再做補充。

相關文章
相關標籤/搜索