明明白白——虛函數(shù),虛指針,虛表,虛繼承

多態(tài)性

多態(tài)性指相同對象收到不同消息或不同對象收到相同消息時產(chǎn)生不同的實現(xiàn)動作。C++支持兩種多態(tài)性:編譯時多態(tài)性,運行時多態(tài)性。

  • 編譯時多態(tài)性:通過重載函數(shù)實現(xiàn)。
  • 運行時多態(tài)性:通過虛函數(shù)實現(xiàn)。

虛函數(shù)、虛表、虛指針

虛函數(shù)
虛函數(shù)是C++中用于實現(xiàn)多態(tài)(polymorphism)的機制。核心理念就是通過基類訪問派生類定義的函數(shù)。
虛函數(shù)是在基類中被聲明為virtual,并在派生類中重新定義的成員函數(shù),可實現(xiàn)成員函數(shù)的動態(tài)覆蓋(Override)

虛函數(shù)表
編譯器會為每個有虛函數(shù)的類創(chuàng)建一個虛函數(shù)表,該虛函數(shù)表將被該類的所有對象共享。類的每個虛成員占據(jù)虛函數(shù)表中的一行。如果類中有N個虛函數(shù),那么其虛函數(shù)表將有N4(x64下是N8)的大小。
派生類的虛函數(shù)表存放重寫的虛函數(shù),當(dāng)基類的指針指向派生類的對象時,調(diào)用虛函數(shù)時都會根據(jù)vptr(虛表指針)來選擇虛函數(shù),而基類的虛函數(shù)在派生類里已經(jīng)被改寫或者說已經(jīng)不存在了,所以也就只能調(diào)用派生類的虛函數(shù)版本了.

虛表指針
虛表指針在類對象中,每個同類對象中都有個一個vptr,指向內(nèi)存中的vtable,所有同類對象,共享一個vtable,但是每個對象都自帶一個vptr指向這個vtable,否則調(diào)用虛函數(shù)的時候會找不到正確的函數(shù)入口,(后面將會講明)虛表指針是對象的第一個數(shù)據(jù)成員。

class Base 
{
    public:
        virtual void func() {}
}

class Derive : public Base
{
    public:
        void func() {}
}

void main()
{
    Derive d;
    Base *pb = &d;
    b->func();
}

編譯器在編譯的時候,發(fā)現(xiàn)Base類中有虛函數(shù),此時編譯器會為每個包含虛函數(shù)的類創(chuàng)建一個虛表(即vtable),該表是一個一維數(shù)組,在這個數(shù)組中存放每個虛函數(shù)的地址。由于Base類和Derive類都包含了一個虛函數(shù)func(),編譯器會為這兩個類都建立一個虛表,(即使子類里面沒有virtual函數(shù),但是其父類里面有,所以子類中對應(yīng)的函數(shù)也是虛函數(shù))
那么如何定位虛表呢?編譯器另外還為每個類的對象提供了一個虛表指針(即vptr),這個指針指向了對象所屬類的虛表。在程序運行時,根據(jù)對象的類型去初始化vptr,從而讓vptr正確的指向所屬類的虛表。所以在調(diào)用虛函數(shù)時,就能夠找到正確的函數(shù)。

對于上述程序,由于pb實際指向的對象類型是Derive,因此vptr指向的Derive類的vtable,當(dāng)調(diào)用pb->func()時,根據(jù)虛表中的函數(shù)地址找到的就是Derive類的func()函數(shù)。

*正是由于每個對象調(diào)用的虛函數(shù)都是通過虛表指針來索引的,也就決定了虛表指針的正確初始化是非常重要的。換句話說,在虛表指針沒有正確初始化之前,我們不能夠去調(diào)用虛函數(shù)。那么虛表是怎么實現(xiàn)的?虛表存放在哪里?虛表中的數(shù)據(jù)是在什么時候確定的?對象中的虛表指針又在什么時候賦值的?

答案是在構(gòu)造函數(shù)中進(jìn)行虛表的創(chuàng)建和虛表指針的初始化。虛表和靜態(tài)變量一樣存在全局?jǐn)?shù)據(jù)區(qū),虛表可以理解成類的靜態(tài)成員,虛表指針和構(gòu)造函數(shù)的執(zhí)行初始化列表是初始化。

構(gòu)造函數(shù)的調(diào)用順序是,在構(gòu)造子類對象時,要先調(diào)用父類的構(gòu)造函數(shù),此時編譯器只“看到了”父類,并不知道后面是否后還有繼承者,它初始化父類對象的虛表指針,該虛表指針指向父類的虛表。當(dāng)執(zhí)行子類的構(gòu)造函數(shù)時,子類對象的虛表指針被初始化,指向自身的虛表。對于以上的例子,當(dāng)Derive類的d對象構(gòu)造完畢后,其內(nèi)部的虛表指針也就被初始化為指向Derive類的虛表。在類型轉(zhuǎn)換后,調(diào)用pb->func(),由于pb實際指向的是Derive類的對象,該對象內(nèi)部的虛表指針指向的是Derive類的虛表,因此最終調(diào)用的是Derive類的func()函數(shù)。

有以下結(jié)論:
  • 擁有虛函數(shù)的類會有一個虛表,而且這個虛表存放在類定義模塊的數(shù)據(jù)段中。模塊的數(shù)據(jù)段通常存放定義在該模塊的全局?jǐn)?shù)據(jù)和靜態(tài)數(shù)據(jù),這樣我們可以把虛表看作是模塊的全局?jǐn)?shù)據(jù)或者靜態(tài)數(shù)據(jù)
  • 類的虛表會被這個類的所有對象所共享。類的對象可以有很多,但是他們的虛表指針都指向同一個虛表,從這個意義上說,我們可以把虛表簡單理解為類的靜態(tài)數(shù)據(jù)成員。值得注意的是,雖然虛表是共享的,但是虛表指針并不是,類的每一個對象有一個屬于它自己的虛表指針
  • 虛表中存放的是虛函數(shù)的地址。
  • 虛表的地址被存放在對象的起始位置,即對象的第一個數(shù)據(jù)成員就是它的虛表指針。 同時我們還可以注意到,虛表指針的初始化確實發(fā)生在構(gòu)造函數(shù)的調(diào)用過程中, 但是在執(zhí)行構(gòu)造函數(shù)體之前,即進(jìn)入到構(gòu)造函數(shù)的"{"和"}"之前。 為了更好的理解這一問題, 我們可以把構(gòu)造函數(shù)的調(diào)用過程細(xì)分為兩個階段,即:
    1.進(jìn)入到構(gòu)造函數(shù)體之間。在這個階段如果存在虛函數(shù)的話,虛表指針被初始化。如果存在構(gòu)造函數(shù)的初始化列表的話,初始化列表也會被執(zhí)行。
    2.進(jìn)入到構(gòu)造函數(shù)體內(nèi)。這一階段是我們通常意義上說的構(gòu)造函數(shù)

構(gòu)造函數(shù)初始化列表
類成員初始化總在構(gòu)造函數(shù)執(zhí)行之前
1)從必要性:
a. 成員是類或結(jié)構(gòu),且構(gòu)造函數(shù)帶參數(shù):成員初始化時無法調(diào)用缺省(無參)構(gòu)造函數(shù)
b. 成員是常量或引用:成員無法賦值,只能被初始化
2)從效率上:
如果在類構(gòu)造函數(shù)里賦值:在成員初始化時會調(diào)用一次其默認(rèn)的構(gòu)造函數(shù),在類構(gòu)造函數(shù)里又會調(diào)用一次成員的構(gòu)造函數(shù)再賦值
如果在類構(gòu)造函數(shù)使用初始化列表:僅在初始化列表里調(diào)用一次成員的構(gòu)造函數(shù)并賦值

如下:

class CExample {
public:
    int a;
    float b;
    //構(gòu)造函數(shù)初始化列表
    CExample(): a(0),b(8.8)
    {}
    //構(gòu)造函數(shù)內(nèi)部賦值
    CExample()
    {
        a=0;
        b=8.8;
    }
};

上面的例子中兩個構(gòu)造函數(shù)的結(jié)果是一樣的。上面的構(gòu)造函數(shù)(使用初始化列表的構(gòu)造函數(shù))顯式的初始化類的成員;而沒使用初始化列表的構(gòu)造函數(shù)是對類的成員賦值,并沒有進(jìn)行顯式的初始化。
初始化和賦值對內(nèi)置類型的成員沒有什么大的區(qū)別,像上面的任一個構(gòu)造函數(shù)都可以。對非內(nèi)置類型成員變量,為了避免兩次構(gòu)造,推薦使用類構(gòu)造函數(shù)初始化列表。

另外有的時候必須用帶有初始化列表的構(gòu)造函數(shù)
1.成員類型是沒有默認(rèn)構(gòu)造函數(shù)的類。若沒有提供顯示初始化式,則編譯器隱式使用成員類型的默認(rèn)構(gòu)造函數(shù),若類沒有默認(rèn)構(gòu)造函數(shù),則編譯器嘗試使用默認(rèn)構(gòu)造函數(shù)將會失敗。
2.const成員或引用類型的成員。因為const對象或引用類型只能初始化,不能對他們賦值。

初始化列表的成員初始化順序:
C++初始化類成員時,是按照聲明的順序初始化的,而不是按照出現(xiàn)在初始化列表中的順序。

Example:

class CMyClass {
    CMyClass(int x, int y);
    int m_x;
    int m_y;
};
 
CMyClass::CMyClass(int x, int y) : m_y(1), m_x(m_y)
{
}

可能以為上面的代碼將會首先做m_y=1,然后做m_x=m_y,最后它們有相同的值。
但是編譯器先初始化m_x,然后是m_y,,因為它們是按這樣的順序聲明的。結(jié)果是m_x將有一個不可預(yù)測的值。

虛繼承與虛基類

虛繼承:在繼承定義中包含了virtual關(guān)鍵字的繼承關(guān)系;
虛基類:在虛繼承體系中的通過virtual繼承而來的基類;需要注意的是:
class CSubClass : public virtual CBase {};其中CBase稱之為CSubClass 的虛基類,而不是CBase就是個虛基類,因為CBase還可以不不是虛繼承體系 中的基類。
虛繼承是為了解決菱形繼承帶來的二義性問題

菱形繼承.png

類 A 派生出類 B 和類 C,類 D 繼承自類 B 和類 C,這個時候類 A 中的成員變量和成員函數(shù)繼承到類 D 中變成了兩份,一份來自 A-->B-->D 這條路徑,另一份來自 A-->C-->D 這條路徑。
在一個派生類中保留間接基類的多份同名成員,雖然可以在不同的成員變量中分別存放不同的數(shù)據(jù),但大多數(shù)情況下這是多余的:因為保留多份成員變量不僅占用較多的存儲空間,還容易產(chǎn)生命名沖突。假如類 A 有一個成員變量 a,那么在類 D 中直接訪問 a 就會產(chǎn)生歧義,編譯器不知道它究竟來自 A -->B-->D 這條路徑,還是來自 A-->C-->D 這條路徑。

虛繼承的目的是讓某個類做出聲明,承諾愿意共享它的基類。其中,這個被共享的基類就稱為虛基類(Virtual Base Class),本例中的 A 就是一個虛基類。在這種機制下,不論虛基類在繼承體系中出現(xiàn)了多少次,在派生類中都只包含一份虛基類的成員。
實際上為了實現(xiàn)虛繼承引入了類似虛函數(shù)表指針的vbptr,vbptr 指的是虛基類表指針(virtual base table pointer),該指針指向了一個虛基類表(virtual table),虛表中記錄了虛基類與本類的偏移地址;通過偏移地址,這樣就找到了虛基類成員,而虛繼承也不用像普通多繼承那樣維持著公共基類(虛基類)的兩份同樣的拷貝,節(jié)省了存儲空間。

純虛函數(shù)

a. 純虛函數(shù)是一個在基類中只有聲明的虛函數(shù),在基類中無定義。要求在任何派生類中都定義自己的版本;
b. 純虛函數(shù)為各派生類提供一個公共界面(接口的封裝和設(shè)計,軟件的模塊功能劃分);
c. 純虛函數(shù)聲明形式:

virtual void func()=0;  //純虛函數(shù)

抽象類

抽象類就是指含有純虛函數(shù)的類,該類不能創(chuàng)建對象,但是可以聲明指針和引用。
一個具有純虛函數(shù)的類稱為抽象類。

//抽象類
class A
{

public:
    virtual void func() = 0;
};

class B:public A
{
public:
    virtual void func()  
    {
        cout << "實現(xiàn)父類的純虛函數(shù)" << endl;
    }
};

int main()
{
    B b;
    b.func();
    return 0;
}

結(jié)論:
(1). 抽象類對象不能做函數(shù)參數(shù),不能創(chuàng)建對象,不能作為函數(shù)返回類型;

A a;(×);            //不能創(chuàng)建抽象類的對象
void func(A a);(×)  //不能做函數(shù)參數(shù)
A func(); (×)       //不能作為函數(shù)的返回值

(2). 可以聲明抽象類指針,可以聲明抽象類的引用;

A * a; (√)         //可以聲明抽象類的指針
A & a; (√)         //可以聲明抽象類的引用

(3). 子類必須繼承父類的純虛函數(shù)才能創(chuàng)建對象。

參考:

  1. 虛函數(shù)、虛指針和虛表

  2. 虛表和虛指針

  3. 虛表指針初始化

  4. 參考4:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。