C++多型

範中豪發表於2021-05-17

本章內容旨在解決以下幾個問題:

  1. 什麼是 C++ 多型, C++ 多型的實現原理是什麼
  2. 什麼是虛擬函式,虛擬函式的實現原理是什麼
  3. 什麼是虛表,虛表的記憶體結構佈局如何,虛表的第一項(或第二項)是什麼
  4. 菱形繼承(類 D 同時繼承 B 和 C,B 和 C 又繼承自 A)體系下,虛表在各個類中的佈局如何,如果類 B 和類 C 同時有一個成員變了 m,m 如何在 D 物件的記憶體地址上分佈的,是否會相互覆蓋
  5. 存在虛擬函式的類物件size計算

什麼是 C++ 多型, C++ 多型的實現原理是什麼

在 C++ 程式設計中,多型性是指具有不同功能的函式可以用同一個函式名,這樣就可以用一個函式名呼叫不同內容的函式。在物件導向方法中,一般是這樣表述多型性的:向不同的物件傳送同一個訊息,不同的物件在接收時會產生不同的行為(即方法);也就是說,每個物件可以用自己的方式去響應共同的訊息所謂訊息,就是呼叫函式,不同的行為就是指不同的實現,即執行不同的函式。換言之,可以用同樣的介面訪問功能不同的函式,從而實現“一個介面,多種方法”。在C++中主要分為靜態多型和動態多型兩種,在程式執行前就完成聯編的稱為靜態多型,主要通過函式過載和模板實現,動態多型在程式執行時才完成聯編,主要通過虛擬函式實現。

函式過載示例:

void print_hello(string name){
    cout << "hello " << name << endl;;
}

void print_hello(string pre_, string name){
    cout << "hello, " << pre_ << " " << name << endl;
}

int main(){
    print_hello("fan");
    print_hello("Mr.", "fan");
    return 0;
}
// out
/*
hello fan
hello, Mr. fan
*/

函式模板示例:

template <class T>
T add_two_num(T a, T b){
    return a + b;
}

int main(){
    cout << add_two_num<int>(1, 2) << endl;
    cout << add_two_num<float>(1.0, 2.0) << endl;
    cout << add_two_num(1.0, 2.0) << endl; // 編譯器自動推斷
    // cout << add_two_num(1, 2.0) << endl; // error
    int va = 10;
    cout << add_two_num<decltype(va)>(va, 2.0) << endl;
    return 0;
}
// out
/*
3
3
3
12
*/

虛擬函式示例:

class A{
public:
    virtual void fun(){
        cout << "hello A" << endl;
    }
};

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

int main(){
    A *a = new A();
    a->fun();
    a = new B();
    a->fun();
    return 0;
}
// out
/*
hello A
hello B
*/

執行期多型實現原理

對於一個繼承體系來說,如果在基類的函式前加上virtual關鍵字,在派生類中重寫該函式,執行時將會根據物件的實際型別來呼叫相應的函式。如果物件型別是派生類,就呼叫派生類的函式;如果物件型別是基類,就呼叫基類的函式。執行期多型就是通過虛擬函式和虛擬函式表實現的。一個含有虛擬函式的類中至少都有一個虛擬函式表指標,且有一個虛表,虛擬函式指標指向虛擬函式表。虛表可以繼承,如果子類沒有重寫虛擬函式,那麼子類虛表中仍然會有該函式的地址,只不過這個地址指向的是基類的虛擬函式實現。如果基類有3個虛擬函式,那麼基類的虛表中就有三項(虛擬函式地址),派生類也會有虛表,至少有三項,如果重寫了相應的虛擬函式,那麼虛表中的地址就會改變,指向自身的虛擬函式實現。如果派生類有自己的虛擬函式,那麼虛表中就會新增該項。派生類的虛表中虛擬函式地址的排列順序和基類的虛表中虛擬函式地址排列順序相同。

什麼是虛擬函式,虛擬函式的實現原理是什麼

直觀上來說,虛擬函式就是類中使用 virtual 關鍵字描述的函式。虛擬函式的作用主要是實現了多型的機制,基類定義虛擬函式,子類可以重寫該函式;在派生類中對基類定義的虛擬函式進行重寫時,需要在派生類中宣告該方法為虛方法,否則將會形成覆蓋。虛擬函式的底層實現機制基於虛擬函式表+虛表指標。
編譯器處理虛擬函式的方法是:為每個類物件新增一個隱藏成員,隱藏成員中儲存了一個指向函式地址陣列的指標,稱為虛表指標(vptr),這種陣列成為虛擬函式表(virtual function table, vtbl),即,每個類使用一個虛擬函式表,每個類物件用一個虛表指標。如果派生類重寫了基類的虛方法,該派生類虛擬函式表將儲存重寫的虛擬函式的地址,而不是基類的虛擬函式地址。如果基類中的虛方法沒有在派生類中重寫,那麼派生類將繼承基類中的虛方法,而且派生類中虛擬函式表將儲存基類中未被重寫的虛擬函式的地址。注意,如果派生類中定義了新的虛方法,則該虛擬函式的地址也將被新增到派生類虛擬函式表中,虛擬函式無論多少個都只需要在物件中新增一個虛擬函式表的地址。呼叫虛擬函式時,程式將檢視儲存在物件中的虛擬函式表地址,轉向相應的虛擬函式表,使用類宣告中定義的第幾個虛擬函式,程式就使用陣列的第幾個函式地址,並執行該函式。

詳細請參考

什麼是虛表,虛表的記憶體結構佈局如何,虛表的第一項(或第二項)是什麼

對於每個存在虛擬函式的類來說,其都含有一個虛擬函式表與至少一個虛指標。虛擬函式表指標(vfptr)指向虛擬函式表(vftbl)的某一項,虛擬函式表中按照物件繼承的順序排列物件的虛擬函式地址,虛基類表中按照物件繼承的順序排列物件的直接虛繼承類到虛基類的偏移。

對於虛基類來說,虛表中按宣告順序依次儲存所有虛擬函式地址。

class Base {
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g() { cout << "Base::g" << endl; }
    virtual void h() { cout << "Base::h" << endl; }
};
int main(){
    Base *b = new Base();
}

虛表示例:

表中最後的一個點表示虛擬函式結束標誌

一般繼承的情況下,虛擬函式按照其宣告順序放於表中,且父類的虛擬函式在子類的虛擬函式前面。

class Derive: public Base{
public:
    virtual void f1() { cout << "Base::f" << endl; }
    virtual void g1() { cout << "Base::g" << endl; }
    virtual void h1() { cout << "Base::h" << endl; }
};

一般繼承且存在虛擬函式覆蓋的情況,覆蓋的虛擬函式將被放到虛表中原來父類虛擬函式的位置,沒有被覆蓋的函式按之前的順序儲存,最後在表中新增子類新加的虛擬函式地址。

class Derive: public Base{
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g1() { cout << "Base::g" << endl; }
    virtual void h1() { cout << "Base::h" << endl; }
};

多重繼承(無虛擬函式覆蓋)時,每個父類都有自己的虛表,且子類的成員函式被放到了第一個父類的表中。

class Base1 {
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g() { cout << "Base::g" << endl; }
    virtual void h() { cout << "Base::h" << endl; }
};
class Base2 {
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g() { cout << "Base::g" << endl; }
    virtual void h() { cout << "Base::h" << endl; }
};
class Base3 {
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g() { cout << "Base::g" << endl; }
    virtual void h() { cout << "Base::h" << endl; }
};
class Derive: public Base1, public Base2, public Base3{
public:
    virtual void f1() { cout << "Base::f" << endl; }
    virtual void g1() { cout << "Base::g" << endl; }
};

多重繼承(有虛擬函式覆蓋)時,父類虛表中對應的虛擬函式地址將被子類的虛擬函式地址覆蓋,子類新加的虛擬函式地址將被新增到第一個父類的虛擬函式表之後。

class Derive: public Base1, public Base2, public Base3{
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g1() { cout << "Base::g" << endl; }
};

綜上,虛表的第一項(或第二項)是父類或者子類宣告的第一(二)個虛擬函式地址。

菱形繼承(類 D 同時繼承 B 和 C,B 和 C 又繼承自 A)體系下,虛表在各個類中的佈局如何,如果類 B 和類 C 同時有一個成員變了 m,m 如何在 D 物件的記憶體地址上分佈的,是否會相互覆蓋

虛表會同時存在兩個A,記憶體分佈與多繼承一致,即存在兩個虛指標指向兩個父類虛表(B, C),B和C的虛表中又同時存在A的虛表。虛表記憶體模型如下:

class Base
{
public:
    Base (int a = 1):base(a){}
    virtual void fun0(){cout << base << endl;}
    int base;
};
class Base1:public Base
{
public:
    Base1 (int a = 2):base1(a){}
    virtual void fun1(){cout << base1 << endl;}
    int base1;
};
class Base2:public Base
{
public:
    Base2 (int a = 3):base2(a){}
    virtual void fun2(){cout << base2 << endl;}
    int base2;
};
class Derive: public Base1, public Base2
{
public:
    Derive (int value = 4):derive (value){}
    virtual void fun3(){cout << derive << endl;}
    int derive;
};

首先給出結論,不會互相覆蓋,不過直接使用 m 的話將會造成二義性問題,這是可以使用類名+引用的方式進行呼叫

class A{
};

class B:public A{
public:
    int n;
    B(){
        A();
        n = 2;
    }
};

class C: public A{
public:
    int n;
    C(){
        A();
        n = 3;
    }
};

class D: public B, public C{
public:
    void fun(){
        cout << B::n << endl;
        cout << C::n << endl;
        cout << sizeof(D);
    }
};

int main(){
    D d;
    d.fun();
    return 0;
}
// out
/*
2
3
8
*/

記憶體分佈為

D
B::n
C::n

存在虛擬函式的類物件size計算

空類的大小為1,因為在C++中任何物件都需要有一個地址,最小為1。對於存在虛擬函式的類來說,至少存在一個虛擬函式指標,指標大小與機器相關(int),在64位的機器上,應為8位元組,在32位的機器上為4位元組。在進行計算的時候還要注意1. 不同的資料型別會進行對齊 2.對於多重繼承,多重繼承幾個基類就有幾個虛指標。

class A{
    int n;
};

class B{
    int n;
    double m;
};

class C{
    int n;
    int l;
    double m;
};

class D {
    int n;
    double m;
    int l;
};

int main(){
    A a;
    B b;
    C c;
    D d;
    cout << sizeof(a) << " " << sizeof(b) << " " << sizeof(c) << " " << sizeof(d);
    return 0;
}
// out
/*
4
16 // int和double對齊 (4->8)
16
24 // n向m對齊,然後l和m對齊
*/
class A {
    virtual void fun() {

    }
};

class B {
    virtual void fun() {

    }
};

class C : A, B {
    virtual void fun() {

    }
};

class D : A {
    virtual void fun() {

    }
};

class E : C {
    virtual void fun() {

    }
};

int main() {
    A a;
    B b;
    C c;
    D d;
    E e;
    cout << sizeof(a) << " " << sizeof(b) << " " << sizeof(c) << " " << sizeof(d) << " " << sizeof(e) << endl;
}
// out
/*
4
4
8
4
8
*/

一個含有虛擬函式的類中含有的虛擬函式表指標個數

一個含有虛擬函式的類中至少都有一個虛擬函式表指標,因為虛擬函式的地址要被放到虛擬函式表(虛表)中。當存在多重繼承時,多重繼承了幾個基類,子類將含有幾個虛指標,並且此指標具有傳遞性。

class A {
    virtual void fun() {

    }
};

class B {
    virtual void fun() {

    }
};

class C : A, B {
    virtual void fun() {

    }
};

class D : A {
    virtual void fun() {

    }
};

class E : C {
    virtual void fun() {

    }
};

int main() {
    A a;
    B b;
    C c;
    D d;
    E e;
    cout << sizeof(a) << " " << sizeof(b) << " " << sizeof(c) << " " << sizeof(d) << " " << sizeof(e) << endl;
}
// out
/*
4
4
8
4
8
*/

參考連結

後臺開發:核心技術與應用實踐 -- C++

C++之多型性

C++函式模板

C++虛擬函式和虛擬函式表原理

C++ | 虛擬函式表記憶體佈局

虛擬函式實現原理

c++中虛基類表和虛擬函式表的佈局

c++繼承彙總(單繼承、多繼承、虛繼承、菱形繼承)

C++繼承記憶體佈局 - 多繼承(無虛繼承)

相關文章