1. 程式人生 > >【c++學習筆記】多型

【c++學習筆記】多型

多型到底是什麼呢?

字面意思就是同種事物在不同的場景下所表現出不同的形態。
在c++當中,多型分類如下:
這裡寫圖片描述

在學習多型之前,我們必須得先了解虛擬函式的概念。

  • 虛擬函式就是在類的成員函式(除建構函式、拷貝建構函式、靜態成員函式)前加virtual關鍵字。
class B
{
public:
    virtual void TestFunc()
    {
        cout << "Base::TestFunc()" << endl;
    }
    int _b;
};

int main()
{
    B b;
    cout << sizeof
(B) << endl; return 0; }

這裡寫圖片描述
這裡列印結果為什麼是8不是4呢?
這裡寫圖片描述
在記憶體視窗上&B之後,發現它的前4個位元組放的類似地址的東西,那麼這個地址指向的又是什麼呢?
這裡寫圖片描述
將前四個位元組的類似地址的東西放到記憶體視窗上檢視後,發現這裡放的還是地址,那麼這裡的地址是什麼呢?
其實這裡的地址就是虛擬函式的地址,在帶有虛擬函式的類中,會多開闢四個位元組用來存放指向一張虛表的指標,虛表裡放的都是虛擬函式的地址。要注意的是,虛擬函式只有在繼承體系中才有意義,因為在非繼承體系用不到,還多開闢了4位元組的空間。帶有虛擬函式的類的物件模型如下:
這裡寫圖片描述
靜態多型在這裡不過多介紹,主要學習動態多型

動態多型的條件:

  • 基類中必須包含虛擬函式,並且派生類一定要對基類中的虛擬函式進行重寫
    • 重寫:
    • 要和基類中的虛擬函式原型相同(返回值、引數列表、函式名均相同)(協變和虛擬析構除外)
    • 協變:返回值可以不同,但是基類的虛擬函式必須返回基類物件的指標(引用);派生類的虛擬函式必須返回派生類物件的指標(引用)–這裡不符合返回值相同,但是也是重寫。
    • 解構函式:解構函式也可以作為虛擬函式,並且在繼承體系中建議作為虛擬函式(為什麼建議稍後解釋)
  • 通過基類的指標(引用)呼叫虛擬函式。

多型的含義:

  • 如果基類的指標/引用指向/引用基類的物件,那麼在呼叫虛擬函式時呼叫屬於基類的虛擬函式。
  • 如果基類的指標/引用指向/引用派生類的物件(賦值相容規則),那麼在呼叫虛擬函式時呼叫屬於派生類的虛擬函式。
class B
{
public:
    virtual void TestFunc()//加virtual關鍵字,成員函式將作為虛擬函式
    {
        cout << "Base::TestFunc()" << endl;
    }

};

class D : public B
{
public:
    virtual void TestFunc()//派生類中對虛擬函式進行重寫,函式名、返回值、引數列表必須一致,
                           //在重寫時,派生類中的訪問限定符不會對虛擬函式有什麼影響,且可以不加virtual關鍵字
    {
        cout << "Derived::TestFunc()" << endl;
    }
};

void Test(B& b)//通過基類的引用呼叫虛擬函式
{
    b.TestFunc();
}

int main()
{
    D d;
    B b;

    Test(d);
    Test(b);

    return 0;
}

那麼,單繼承中派生類的物件模型是怎樣的呢?

帶有虛擬函式單繼承物件模型

class B
{
public:
    virtual void TestFunc()//加virtual關鍵字,成員函式將作為虛擬函式
    {
        cout << "Base::TestFunc()" << endl;
    }

    int _b;
};

class D : public B
{
public:
    virtual void TestFunc()
    {
        cout << "Derived::TestFunc()" << endl;
    }

    int _d;
};

int main()
{
    D d;
    d._b = 1;
    d._d = 2;

    cout << sizeof(d) << endl;

    return 0;
}

這裡寫圖片描述
在除錯的時候調出記憶體視窗,&d可以很容易得出帶有虛擬函式的單繼承物件模型:
這裡寫圖片描述

普通成員函式和虛擬函式的區別:
最大的區別就是呼叫方式不同,普通成員函式直接呼叫,而虛擬函式的呼叫分為如下幾步:

  • 從物件前4個位元組中取虛表的地址
  • 傳遞this指標
  • 從虛表中獲取虛擬函式的地址(虛表地址+虛擬函式在虛表中的偏移量)
  • 呼叫虛擬函式

帶有虛擬函式的多繼承物件模型

class B1
{
public:
    virtual void TestFunc1()
    {
        cout << "Base1::TestFunc1()" << endl;
    }

    int _b1;
};

class B2
{
public:
    virtual void TestFunc2()
    {
        cout << "Base2::TestFunc2()" << endl;
    }

    int _b2;
};

class D: public B1, public B2
{
public:
    virtual void TestFunc1()//對B1中的虛擬函式TestFunc1重寫
    {
        cout << "Derived::TestFunc1()" << endl;
    }

    virtual void TestFunc2()
    {
        cout << "Derived::TestFunc2()" << endl;//對B2中的虛擬函式TestFunc2重寫
    }

    virtual void TestFunc3()//派生類自己特有的虛擬函式
    {
        cout << "Derived::TestFunc3()" << endl;
    }


    int _d;
};


int main()
{
    D d;
    d._b1 = 1;
    d._b2 = 2;
    d._d = 3;

    cout << sizeof(D) << endl;

    return 0;
}

這裡寫圖片描述
對&d得:
這裡寫圖片描述
發現這裡有兩個類似地址得東西,檢視得
這裡寫圖片描述
這裡寫圖片描述
不難發現,這兩個地址分別指向兩張虛表,一張為繼承B1的、另一張為繼承B2的,第一張虛表還會存放派生類中特有的虛擬函式。所以,物件模型如下:
這裡寫圖片描述

帶有虛擬函式的菱形繼承

#include <iostream>
using namespace std;

class B
{
public:
    virtual void TestFunc1()
    {
        cout << "B::TestFunc1()" << endl;
    }
    virtual void TestFunc2()
    {
        cout << "B::TestFunc2()" << endl;
    }

    virtual void TestFunc3()
    {
        cout << "B::TestFunc3()" << endl;
    }

    int _b;
};

class C1 : public B
{
public:
    virtual void TestFunc1()
    {
        cout << "C1::TestFunc1()" << endl;
    }
    int _c1;
};

class C2 : public B
{
public:
    virtual void TestFunc2()
    {
        cout << "B::TestFunc2()" << endl;
    }

    int _c2;
};

class D : public C1, public C2
{
public:
    virtual void TestFunc1()
    {
        cout << "D::TestFunc()" << endl;
    }

    int _d;
};

int main()
{
    D d;
    cout << sizeof(D) << endl;

    return 0;
}

這裡寫圖片描述

物件模型:
其實從派生類D的大小很容易就能推斷出物件的模型如下:
這裡寫圖片描述
由於菱形繼承存在資料二意性的問題,所以就引出了帶有虛擬函式的菱形虛擬繼承

帶有虛擬函式的菱形虛擬繼承

首先,先看看在單繼承中,帶有虛擬函式的虛擬繼承的物件模型,這裡是為了方便理解帶有虛擬函式的菱形虛擬繼承的物件模型,因為在單繼承中,虛擬繼承是沒有什麼實際意義的。

class B
{
public:
    virtual void TestFunc()
    {
        cout << "B::TestFunc()" << endl;
    }

    int _b;
};

class D : virtual public B
{
public:
    virtual void TestFunc()
    {
        cout << "D::TestFunc()" << endl;
    }

    int _d;
};

int main()
{
    D d;

    cout << sizeof(D) << endl;
    d._b = 1;
    d._d = 2;

    return 0;
}

這裡寫圖片描述
在物件d中,除了有一張拷貝B的虛表外,因為是虛擬繼承,還有一張儲存偏移量的表格,調出記憶體視窗,取地址,如下:
這裡寫圖片描述
發現,確實是有兩個指標,那麼,哪一個是指向虛表的,哪一個又是指向儲存偏移量的呢,我們可以再呼叫一個記憶體視窗進行檢視,因為虛表中儲存的是地址,而另一個儲存的是偏移量,是整數。
這裡寫圖片描述
這裡寫圖片描述
可以發現,上面的地址指向存放偏移量的表格,下面的指向虛表,所以再單繼承中,帶有虛擬函式的虛擬繼承物件模型如下:
這裡寫圖片描述
上面的是派生類沒有新增自己特有的虛擬函式的模型,接下來我們用同樣的方法看看在派生類中新增虛擬函式後,物件模型又是什麼?

class B
{
public:
    virtual void TestFunc1()
    {
        cout << "B::TestFunc1()" << endl;
    }

    int _b;
};

class D : virtual public B
{
public:
    virtual void TestFunc1()
    {
        cout << "D::TestFunc1()" << endl;
    }
    virtual void TestFunc2()
    {
        cout << "D::TestFunc2()" << endl;
    }

    int _d;
};

int main()
{
    D d;

    cout << sizeof(D) << endl;
    d._b = 1;
    d._d = 2;

    return 0;
}

這裡寫圖片描述
算下來大小比上面的多了4位元組。其實就是如果派生類自己新增虛擬函式,就會多開闢四個位元組,指向另一張虛表,這張虛表中存放派生類自己特有的虛擬函式地址。
這裡寫圖片描述

接下來,再來看看帶有虛擬函式的菱形虛擬繼承,這裡為了簡單起見,沒有在派生類中新增特有的虛擬函式。

class B
{
public:
    virtual void TestFunc1()
    {
        cout << "B::TestFunc1()" << endl;
    }

    virtual void TestFunc2()
    {
        cout << "B::TestFunc2()" << endl;
    }

    virtual void TestFunc3()
    {
        cout << "B::TestFunc3()" << endl;
    }

    int _b;
};

class C1 : virtual public B
{
public:
    virtual void TestFunc1()
    {
        cout << "C1::TestFunc1()" << endl;
    }
    int _c1;
};

class C2 : virtual public B
{
public:
    virtual void TestFunc2()
    {
        cout << "C2::TestFunc2()" << endl;
    }
    int _c2;
};

class D : public C1, public C2
{
public:
    virtual void TestFunc3()
    {
        cout << "D::TestFunc3()" << endl;
    }

    int _d;
};


int main()
{
    D d;

    cout << sizeof(C1) << endl;
    cout << sizeof(D) << endl;
    d._b = 1;
    d._c1 = 2;
    d._c2 = 3;
    d._d = 4;

    return 0;
}

這裡寫圖片描述
物件模型如下:
這裡寫圖片描述
講到這裡,現在我們來想想動態多型的實現原理

動態多型實現原理

  • 編譯器在帶有虛擬函式的類的背後維護了一張虛表(虛擬函式的入口地址)
  • 虛擬函式的呼叫原理(通過基類的指標/引用呼叫虛擬函式)
    • 從指標所指物件(基類/派生類)前4個位元組中取虛表的地址
    • 傳遞引數(this+當前虛擬函式的引數)
    • 根據從物件4個位元組取到的虛表的地址取物件的虛擬函式地址
    • 呼叫虛擬函式

現在我們再來談談為什麼靜態函式,建構函式,拷貝建構函式為什麼不能作為虛擬函式?
- 最主要的原因就是呼叫虛擬函式需要this指標,但是這幾個函式當中並沒有虛擬函式(或者還沒構造好物件)
那麼為什麼建議將解構函式作為虛擬函式呢?
答案是因為如果不作為虛擬函式,有可能會造成記憶體洩露的問題,如下:

class B
{
public:
    B()
        :_b(1)
    {
        cout << "B::B()" << endl;
    }

    ~B()
    {
        cout << "B::~B()" << endl;
    }
    int _b;
};

class D: public B
{
public:
    D()
        :_ptr(new char[10])
    {
        cout << "D::D()" << endl;
    }

    ~D()
    {
        if (_ptr)
        {
            delete[] _ptr;
        }

        cout << "D::~D()" << endl;
    }

    char* _ptr;
};

void TestFunc()
{
    B* pb;

    pb = new D;//由於賦值相容規則,基類指標可以指向派生類物件,這裡就會呼叫D的建構函式申請空間
    delete pb;//由於是基類型別的指標,所以delete只會呼叫基類的析構,所以這裡就會造成記憶體洩漏
}

int main()
{
    TestFunc();

    return 0;
}

這裡寫圖片描述
只要我們將解構函式作為虛擬函式,就不會出現上面的問題了,列印結果如下:
這裡寫圖片描述
那麼,為什麼不直接將解構函式預設作為虛擬函式呢?

  • C++不 把虛解構函式直接作為預設值的原因是虛擬函式表的開銷以及和C語言的型別的相容性。