C++多態


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

  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++繼承內存布局 - 多繼承(無虛繼承)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM