深入瞭解C++多態的原理及實現方式

前言

需要深入瞭解C/C++語言的基礎之上再看此文章。

關於多態

具有多種形態,調用同一個方法會隨上下文不同而產生不同的結果,多態有靜態多態與動態多態兩種。

函數承載方式

函數重載是讓同一個函數(函數名字是相同的)可以根據參數不同從而實現不同的處理,之所以稱之爲函數重載,是因爲它有多個同名的函數,而編譯器進行了重載,編譯器根據實參與形參的類型及個數,自動確定調用那一個函數。這是通過函數重載的方式實現多態,這種實現的方式是靜態的多態,因爲在編譯階段就已經知道了的。

比如以下代碼,通過重載Base類中的func方法,達到不同的參數產生不同的結果:

#include <iostream>

class Base {
public:
    Base(void){};
    ~Base(){};

    void func() { 
        std::cout << "Base func()" << std::endl;
    };
    void func(int a) {
        std::cout << "Base func(int a)" << std::endl;
    };
};

int main(void)
{
    Base base;
    base.func();      
    base.func(2);  

    return 0;
}

// 運行輸出

Base func()
Base func(int a)

當然,也可以在子類中重載基類的函數方法,比如在基類Bash中派生出一個子類Subclass,重載func函數:

class Subclass : public Base {
public:
    Subclass(void) {};
    ~Subclass() {};

    using Base::func;       // 將基類的func所有重載實例到包含到Subclass中
    void func(std::string s) { 
        std::cout << "Subclass func(string s)" << std::endl;
    };
};

這樣子在子類中也可以直接通過重載使用基類的函數:

#include <iostream>

class Base {
public:
    Base(void) {};
    ~Base() {};

    void func() { 
        std::cout << "Base func()" << std::endl;
    };
    void func(int a) {
        std::cout << "Base func(int a)" << std::endl;
    };
};

class Subclass : public Base {
public:
    Subclass(void) {};
    ~Subclass() {};

    using Base::func;
    void func(std::string s) { 
        std::cout << "Subclass func(string s)" << std::endl;
    };
};

int main(void)
{
    Base base;
    Subclass sub;
    
    sub.func();
    sub.func(2);
    sub.func("666");

    return 0;
}

// 運行結果
Base func()
Base func(int a)
Subclass func(string & s)

虛函數方式

當類中存在虛函數時,編譯器會在類中自動生成一個虛函數表,虛函數表是一個存儲類成員函數指針的數據結構,編譯器會爲對象自動生成一個指向虛函數表的指針(通常稱之爲 vptr 指針),它由編譯器自動生成和維護,通過C++關鍵字 virtual 修飾的成員函數會被編譯器放入虛函數表中。虛函數表是和類對應的,虛函數表指針是和對象對應的,而對象在運行過程中會被賦值或者是強轉,因此這種實現的方式是動態的,因爲虛函數表指針是可以被修改的,當父類中的虛函數表的指針被子類的虛函數表指針替換掉,那麼在調用這個類的方法(虛函數)就會指向實際子類實現的方法,因此實現了多態。

那麼如何定位虛函數表呢?編譯器另外還爲每個對象提供了一個虛函數表指針(即vptr),這個指針指向了對象所屬類的虛函數表,在程序運行時,根據對象的類型去初始化vptr,從而讓vptr正確的指向了所屬類的虛函數表,從而在調用虛函數的時候,能夠找到正確的函數。

正是由於每個對象調用的虛函數都是通過虛函數表指針來索引的,也就決定了虛函數表指針的正確初始化是非常重要的,換句話說,在虛函數表指針沒有正確初始化之前,我們不能夠去調用虛函數,那麼虛函數表指針是在什麼時候,或者什麼地方初始化呢?這其實是在構造函數中進行虛函數表的創建和虛函數表指針的初始化,在構造子類對象時,要先調用父類的構造函數,此時編譯器只“看到了”父類,並不知道後面是否還有繼承者,它初始化父類對象的虛函數表指針,該虛函數表指針指向父類的虛函數表,當執行子類的構造函數時,子類對象的虛函數表指針被初始化,指向自身的虛函數表

總的來說:虛函數表可以繼承,如果子類沒有重寫虛函數,那麼子類虛函數表中仍然會有該函數的地址,只不過這個地址指向的是基類的虛函數實現,如果基類有3個虛函數,那麼基類的虛函數表中就有3項(虛函數地址),派生類也會有虛函數表,至少有3項,如果重寫了相應的虛函數,那麼虛函數表中的地址就會改變,指向自身的虛函數實現,如果派生類有自己的虛函數,那麼虛函數表中就會添加該項。

比如以下的代碼,創建一個父類,父類中包含一個虛函數Say(),同時再定義一個子類Son,子類中實現了Say()函數方法:

#include <iostream>

class Father {
public:
    Father(void) {};
    ~Father() {};
    
    virtual void Say() {
        std::cout << "father say hello!" << std::endl;
    };
};

class Son : public Father {
public:
    Son(void) {};
    ~Son() {};
    
    void Say(void) {
        std::cout << "son say hello!" << std::endl;
    };
};

int main(void)
{
    using namespace std;

    Father father;
    Son son;
    Father *father1 = &son; // 強制轉換類型

    father.Say();
    son.Say();

    father1->Say();
}

// 運行結果

father say hello!
son say hello!
son say hello!

通過這個結果,我們發現是"son say hello",也就說它其實已經根據對象的類型調用了正確的函數,這就是虛函數方式實現了C++的多態性。

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