c++菱形繼承、多型與類記憶體模型

胖白白發表於2024-05-20

目錄
  • 1.菱形繼承
    • 1.1.菱形繼承的問題
    • 1.2.解決辦法
  • 2.虛擬函式與多型
    • 2.1.普通函式不能實現多型
    • 2.2.虛擬函式(子類重寫)+ 父類指向子類——實現多型
    • 2.3.多型原理
  • 3.c++記憶體模型
  • 4.參考

1.菱形繼承

先看下面的例子,SheepTuo同時繼承了SheepTuo,而他們同時繼承Animal
image

#include <iostream>
using namespace std;

class Animal
{
    int mAge;
};

class Sheep : public Animal {};
class Tuo : public Animal {};
class SheepTuo : public Sheep, public Tuo {};

int main()
{
    SheepTuo st;
    ////// 1.報錯,"SheepTuo::mAge" is ambiguous,mAge成員在兩個子類都存在,二義性
    // st.mAge = 18; 

    ////// 2.可以宣告作用域,避免成員的二義性
    st.Sheep::mAge = 18;
    st.Tuo::mAge = 100;

    ////// 3.但是在SheepTuo類中mAge成員會複製兩份,造成記憶體浪費
    return 0;
}

1.1.菱形繼承的問題

  1. 共享成員二義性——增加作用域可以解決
  2. 記憶體複製兩份-浪費——虛繼承解決
    image

1.2.解決辦法

#include <iostream>
using namespace std;

// 虛基類
class Animal
{
public:
    int mAge;
};

// virtual --> 虛繼承
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};

int main()
{
    SheepTuo st;
    ////// 1.不會報錯了
    // st.mAge = 18;

    ////// 2.這樣寫,下面的mAge都會是100,因此虛繼承後,成員不會被複制,只在基類中有一份,子類中維護vbptr(管理不同的偏移量)指向它
    st.Sheep::mAge = 18;
    st.Tuo::mAge = 100;
    
    return 0;
}

SheepTuo繼承了SheepTuo的虛基類指標vbptr,這倆的指標會指向他們的虛基類表vbtable,虛基類表中存著其vbptr的偏移量,透過偏移量可以幫助子類正確找到從虛基類繼承來的資料
image

2.虛擬函式與多型

2.1.普通函式不能實現多型

#include <iostream>
using namespace std;

class Animal
{
public:
    void Speak()
    {
        cout << "動物 在說話" << endl;
    }
};

class Cat : public Animal 
{
    void Speak()
    {
        cout << "小貓 在說話" << endl;
    }
};
class Dog : public Animal 
{
    void Speak()
    {
        cout << "小狗 在說話" << endl;
    }
};

// 常物件只能呼叫常函式
// void DoSpeak(const Animal &animal)
void DoSpeak(Animal &animal)
{
    animal.Speak();
}

int main()
{
    Cat cat;
    DoSpeak(cat);
    
    return 0;
}

輸出:動物 在說話
上面的AnimalSpeak是一個普通成員函式,雖然有繼承的條件,但是編譯器在編譯階段,會DoSpeak函式中把animal.Speak();animal繫結為Animal的地址,無法實現多型

2.2.虛擬函式(子類重寫)+ 父類指向子類——實現多型

#include <iostream>
using namespace std;

class Animal
{
public:
    virtual void Speak()
    {
        cout << "動物 在說話" << endl;
    }
};

class Cat : public Animal 
{
    void Speak()
    {
        cout << "小貓 在說話" << endl;
    }
};
class Dog : public Animal 
{
    void Speak()
    {
        cout << "小狗 在說話" << endl;
    }
};

void DoSpeak(Animal &animal)
{
    animal.Speak();
}

int main()
{
    Cat cat;
    DoSpeak(cat);
    
    return 0;
}

輸出:小貓 在說話
和2.1.節相比,只增加了virtual關鍵字,使得父類普通函式變為虛擬函式,使得DoSpeak函式中animal.Speak()的animal在執行階段才會指向真正呼叫的物件,實現了多型
其執行流程先說明:物件指標->取其虛表指標->取其虛表中函式->call呼叫

2.3.多型原理

  • case1.普通函式的基類所佔記憶體大小, sizeof(Animal) = 1
class Animal
{
public:
    void Speak()
    {
        cout << "動物 在說話" << endl;
    }
};

image

  • case2.虛擬函式的基類所佔記憶體大小, sizeof(Animal) = 4
class Animal
{
public:
    virtual void Speak()
    {
        cout << "動物 在說話" << endl;
    }
};

image

case1和case2說明了增加virtual會在類中多用記憶體,增加的是虛擬函式指標和虛擬函式表

  • case3.子類繼承虛基類,並且子類重寫虛擬函式
class Animal
{
public:
    virtual void Speak()
    {
        cout << "動物 在說話" << endl;
    }
};

class Cat : public Animal 
{
    void Speak()
    {
        cout << "小貓 在說話" << endl;
    }
};
  • Cat中的虛擬函式表發生覆蓋
    image
  • 總體來看看
    image

3.c++記憶體模型

C++類中記憶體儲存情況比較複雜,涉及成員資料、函式、靜態成員、虛擬函式等情況
記錄結論,詳情參考C++類的記憶體佈局
image
image

4.參考

C++類的記憶體佈局
C++多型剖析

相關文章