C++八股之函式過載與重寫-靜態多型與動態多型

tstars發表於2024-04-22

過載:是指在同一作用域中允許存在多個同名函式,⽽這些函式的參數列不同(或許引數個數不同,或許引數型別不同,或許兩者都不同)。過載與類無關,過載實現編譯時多型,屬於靜態繫結。
重寫:指⼦類新定義⽗類的函式的做法。如果重寫的函式在父類中是虛擬函式,那麼能夠實現動態多型。
如果在父類中沒有將函式宣告為虛擬函式,子類仍然可以重寫(覆蓋)父類的函式,但是在執行時將無法實現多型性,即無法透過父類指標或引用呼叫子類的函式。

什麼叫執行時多型?父類指標或引用可以指向子類物件,並且如果父類中定義的虛擬函式被派生類重寫,那麼透過該指標或引用使用該函式時呼叫的是子類中重寫後的函式。注意,必須透過虛擬函式才能實現動態多型。虛擬函式定義使用virtual關鍵字

點選檢視程式碼
class Base {
public:
    virtual void print() {
        cout << "Base class" << endl;
    }
};

class Derived : public Base {
public:
    void print() override {
        cout << "Derived class" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->print();  // 輸出 "Derived class"
    delete basePtr;
    return 0;
}

上述程式碼中使用virtual關鍵字是定義基類中的函式為虛擬函式,在子類中進行重寫。重寫虛擬函式時override關鍵字不是必須的,但是建議寫上,這是一個良好的習慣。override關鍵字提供編譯器級別的檢查,若過載的不是虛擬函式同時又使用了override關鍵字,那麼會丟擲錯誤異常。

純虛擬函式和虛擬函式的區別
純虛擬函式(Pure Virtual Functions)是在基類中宣告的沒有實際實現的虛擬函式,它只是為了讓派生類去實現。基類中包含純虛擬函式的類被稱為抽象類(Abstract Class),抽象類不能被例項化,只能作為介面使用。為了將函式宣告為純虛擬函式,需要在基類中使用關鍵字virtual,並將函式定義為純虛擬函式,即在函式宣告的末尾加上= 0。

點選檢視程式碼
class Shape {
public:
    virtual void draw() = 0;  // 純虛擬函式
};

class Circle : public Shape {
public:
    void draw() {
        cout << "Drawing a circle." << endl;
    }
};

int main() {
    // Shape shape;  // 錯誤,抽象類不能被例項化
    Shape* shapePtr = new Circle();
    shapePtr->draw();  // 輸出 "Drawing a circle."
    delete shapePtr;
    return 0;
}

在一個類中可以同時定義純虛擬函式和虛擬函式,被稱為混合類。在包含純虛擬函式的類中,該類仍然是一個抽象類,無法直接例項化。必須在派生類中對純虛擬函式進行實現,才能例項化派生類物件。

點選檢視程式碼
class Base {
public:
    virtual void virtualFunction() {
        cout << "Virtual function" << endl;
    }
    
    virtual void pureVirtualFunction() = 0;  // 純虛擬函式
};

class Derived : public Base {
public:
    void virtualFunction() {
        cout << "Derived class's virtual function" << endl;
    }
    
    void pureVirtualFunction() {
        cout << "Derived class's implementation of pure virtual function" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->virtualFunction();       // 輸出 "Derived class's virtual function"
    basePtr->pureVirtualFunction();   // 輸出 "Derived class's implementation of pure virtual function"
    delete basePtr;
    return 0;
}

虛擬函式實現動態多型在編譯器上如何實現
在編譯器上,虛擬函式的實現主要涉及兩個關鍵概念:虛擬函式表(vtable)和虛擬函式指標(vptr)。

虛擬函式表(vtable):

對於包含虛擬函式的類,在編譯過程中,編譯器會為該類生成一個虛擬函式表(vtable)。虛擬函式表是一個儲存函式指標的資料結構,每個虛擬函式在表中都有一個對應的函式指標。虛擬函式表位於類的記憶體佈局中,通常是以靜態資料成員的形式存在於類的頭部。虛擬函式表是針對類層次結構中的每個類生成的,每個類都有自己的虛擬函式表。

虛擬函式指標(vptr):

對於每個包含虛擬函式的類的物件,在記憶體中都會被分配一個額外的隱藏成員,稱為虛擬函式指標(vptr)。如果子類繼承的父類中包含虛擬函式,那麼子類從邏輯上也包含虛擬函式,因此子類的物件也會有一個虛擬函式指標
虛擬函式指標是一個指向虛擬函式表的指標,它指向與物件所屬的類對應的虛擬函式表。虛擬函式指標位於物件的記憶體佈局中,通常是作為物件的第一個或前幾個位元組。透過虛擬函式指標,程式能夠在執行時動態地確定呼叫哪個虛擬函式。

實現動態多型的過程如下:

  • 當使用基類指標或引用指向派生類物件時,編譯器會根據指標或引用的靜態型別(即基類型別)來訪問虛擬函式。
  • 在執行時,透過虛擬函式指標(vptr)找到物件所屬的虛擬函式表。
  • 透過虛擬函式表中的函式指標,確定要呼叫的虛擬函式的地址。
    最終,執行時會呼叫正確的派生類函式,實現多型性。

派生類如何繼承和修改基類的虛擬函式表
派生類會繼承並修改基類的虛擬函式表。
當一個派生類繼承自一個基類時,它會繼承基類的虛擬函式表。繼承的過程中,派生類會保留基類的虛擬函式表,並在其後新增自己新增的虛擬函式的地址。如果派生類重寫(覆蓋)了基類的虛擬函式,它會將自己的虛擬函式的地址替換掉基類虛擬函式表中相應位置的地址。這樣,透過派生類物件的虛擬函式指標,就可以呼叫派生類自己的虛擬函式。如果派生類新增了虛擬函式,它會將新增虛擬函式的地址新增到自己的虛擬函式表的末尾。這樣,透過派生類物件的虛擬函式指標,就可以呼叫新增的虛擬函式。需要注意的是,派生類的虛擬函式表與基類的虛擬函式表是相互獨立的,它們有不同的記憶體空間。派生類的虛擬函式表會繼承基類的虛擬函式表,並在其基礎上進行修改或擴充套件。

理解多型的關鍵在於virtual關鍵字,還是有很多深入討論的內容,後續繼續補充

相關文章