2020年5月25日 星期一

C++ virtual function 簡易理解 [Early/Static binding v.s. Late/Dynamic binding]

這篇主要是想紀錄一下自己對於 Virtual function 的理解。
首先要了解 virtual function 必須先了解 C++ 在編譯過程中的 early binding (又稱 static binding) 與 late binding (又稱 dynamic binding)是甚摸東西,所謂 binding  指的就是 "鏈結", 那是要鏈結甚摸呢? 就是 "定義"。對於每個函數呼叫(function call)編譯器必須知道這個函數是對應到哪個函數定義的,才有辦法知道具體要執行的內容是甚摸,而 binding 做的事簡單來說就是把函數呼叫跟函數定義對應起來,讓編譯器知道要執行的內容。

那位甚摸又分為 early binding 與 late binding 呢? 顧名思義就是鏈結的時間不同,我們都知道要執行一個 C++ 程式必須先經過編譯 (compile time),得到執行檔之後才能開始執行 (run time),early binding 的 early 指的就是編譯的這個時間點(因為相較於執行發生較早)發生的binding,late 指的就是 run time 時發生的 binding。一般而言在編譯過程中 compiler 會去查找函數定義來進行鏈結,因此基本上預設所有 function call 都是透過 early binding完成的。

那為啥還會需要 late binding 呢? 試想以下的例子:

#include <iostream>

using namespace std;

class Homo
{
    public:
        void height()
        {
            cout << "Homo height is unlimited" << endl;
        }
};

class neanderthalensis : public Homo
{
    public:
        void height()
        {
            cout << "neanderthalensis is about 1.5 meters tall" << endl;
}
};

int main()
{
Homo *h;
        Homo *p;
        Homo b;
        neanderthalensis n;
h = &n;
        // early binding
        h -> height();
    
        if (cin.get() == 'n') {
            p = &b;
        }
        else {
            p = &n;
        }
        
        return 0;
}

我們定義了一個人屬(Homo)的父類別以及一個尼安得塔人(neanderthalensis)的子類別繼承 Homo,然後宣告了一個 Homo 類的 pointer h 指向一個 neanderthalensis 類的object,此時我們會發現到當我們執行這個程式並呼叫member function height()時,會輸出 "Homo height is unlimited" 也就是說會執行父類別的 height(),這是因為雖然 h 指向的是 neanderthalensis 類的 object,但是 h 本身是 Homo 類的 pointer,因此在compile時編譯器會根據 h 是 Homo 類別的 pointer 而把 h->height() 這個 function call 的定義鏈結到 Homo 中 height() 的定義,這就是所謂的"early binding"。

為啥編譯器沒辦法判斷出來要用鏈結到 neanderthalensis 類的 height() 呢? 這是因為在編譯時期 n 還沒有被具體的實現出來,編譯器並無法確定 h 指向的是甚摸類別的物件,因此編譯器只能從 h 宣告的類別判斷它是屬於 Homo 類之後來做 binding。
更明顯的的例子如 Homo 類的 pointer p也是,因為必須等到 run time的時候才知道使用者輸入是啥,才知道 p 指向的物件類別,因此編譯器只能根據 p 的類別來做 "early binding"。
然而如果是拖到 run time 時再來做 binding,這時就可以明確的知道 h 所指向的物件類別,從而鏈結到 neanderthalensis 類的 height()。

但如上所述,預設中所有的 function call 都是採用 "early binding" 的方式進行鏈結,顯然我們必須要有某個語法來告訴編譯器先不要做binding,再拖一下,這個keyword就是 "virtual" 拉!

Virtual Function:
virtual function 是一種在 base class 出現的 member function,語法就是在 member function 的宣告前加上關鍵字 "virtual",編譯器看到這個關鍵字便會知道這個 function之後會在 derived class 也就是繼承這個 base class 的 class 中被 overridden,因此遇到這個 function 的 function call 時便會採用 "late binding"。

#include <iostream>

using namespace std;

class Homo
{
    public:
        virtual void height()
        {
            cout << "Homo height is unlimited" << endl;
        }
};

class neanderthalensis : public Homo
{
    public:
        void height()
        {
            cout << "neanderthalensis is about 1.5 meters tall" << endl;
}
};

int main()
{
Homo *h;
        neanderthalensis n;
h = &n;
        // early binding
        h -> height();
        return 0;
}

另外 virtual function 還有幾個特性:

1. 一個 class 中的 virtual function 在所有繼承它的子類別中自動預設此 function 為 virtual。
2. 一個沒有函數定義的 virtual function 稱做 "pure virtual function",ex: virtual void height() = 0;
3. 包含至少一個 pure virtual function 的 class 稱為 "Abstract class",Abstract class 是不能生成 object 的,只能拿來當作父類別被繼承。
4. 繼承 Abstract class 的子類別必須 override pure virtual function,否則也會變成一個 Abstract class。 

總結一下:
    簡而言之 virtual function 就是在 class member function 宣告前加上關鍵字 "virtual" 告訴編譯器這個 function 之後會被 overridden 所以要採用 "late binding",而 virtual function 可以實現讓同一個 function 根據不同種類的 object 而有不同的行為(函數定義),因此可以視為一種多型(Polymorphism)的方式。

越打越長,有關virtual function的實現方式還是之後在紀錄吧 ˊˋ

沒有留言:

張貼留言