欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解C++中多態(tài)的底層原理

 更新時間:2022年04月27日 10:42:37   作者:賣寂寞的小男孩  
要了解C++多態(tài)的底層原理需要我們對C指針有著深入的了解,這個在打印虛表的時候就可以見功底,所以快來跟隨小編一起學(xué)習(xí)一下吧

前言

要了解C++多態(tài)的底層原理需要我們對C指針有著深入的了解,這個在打印虛表的時候就可以見功底,理解了多態(tài)的本質(zhì)我們才能記憶的更牢,使用起來更加得心應(yīng)手。

1.虛函數(shù)表

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

首先我們在基類Base中定義一個虛函數(shù),然后觀察Base類型對象b的大?。?/p>

class Base
{
public:
    virtual void Func1()
    {
        cout << "Func1" << endl;
    }
    virtual void Func2()
    {
        cout << "Func2" << endl;
    }
    void f()
    {
        cout << "f()" << endl;
    }
protected:
    int b = 1;
    char ch = 1;
};
int main()
{
    Base b;
    cout << sizeof(b);
    return 0;
}

我們發(fā)現(xiàn),如果按照對齊數(shù)原則來計算b的大小時,得到的結(jié)果是8,而我們打印的結(jié)果是:

這說明帶有虛函數(shù)的類所定義的對象中,除了成員變量之外還有其他的東西被加入進(jìn)去了(成員函數(shù)默認(rèn)不在對象內(nèi),在代碼段)。

我們可以通過調(diào)試來觀察b中的內(nèi)容:

我們發(fā)現(xiàn)對象中多了一個__vfptr,即為虛函數(shù)表指針。簡稱為虛表指針。

(2)虛函數(shù)表

仍然看上圖,我們發(fā)現(xiàn)虛函數(shù)表指針下方有兩個地址,這兩個地址分別對應(yīng)的就是Base中兩個虛函數(shù)的地址,構(gòu)成了一個虛函數(shù)表。所以虛函數(shù)表本質(zhì)是一個指針數(shù)組,數(shù)組中每一個元素是一個虛函數(shù)的地址。

VS2019封裝更為嚴(yán)密,在底層的匯編代碼中,虛函數(shù)表中的地址并不一定是虛函數(shù)的地址,可能存放的是跳轉(zhuǎn)到虛函數(shù)的地址的指令的地址。這個在后面會加以演示。

因此當(dāng)我們調(diào)用普通函數(shù)和虛函數(shù)時,它們的本質(zhì)是不同的:

    Base* bb=nullptr;
    bb->f();
    bb->Func1();

其中bb調(diào)用f()的過程沒有發(fā)生解引用操作,非虛函數(shù)在公共代碼段中,直接對其進(jìn)行調(diào)用即可。而bb調(diào)用Func1()的過程中,需要通過虛表指針來找到Func1(),而拿到虛表指針需要對bb進(jìn)行解引用操作,而bb是空,因此程序會崩潰。

我們知道對象中只存儲成員變量,成員函數(shù)存儲在公共代碼段中,其實(shí)虛函數(shù)也是一樣存儲在公共代碼段,只不過尋找虛函數(shù)需要通過虛表來確定位置。普通函數(shù)直接就可以確定位置。

2.虛函數(shù)表的繼承–重寫(覆蓋)的原理

還拿上一節(jié)中買票的例子舉例,其中父類中有兩個虛函數(shù),子類重寫了其中的一個,子類中還有自己的函數(shù)。

class Person
{
public:
    virtual void BuyTicket()
    {
        cout << "全價" << endl;
    }
    virtual void Func1()
    {
        cout << "Func1" << endl;
    }
protected:
    int _a;
};
class Student :public Person
{
public:
    virtual void BuyTicket()
    {
        cout << "半價" << endl;
    }
    virtual void Func2()
    {
        cout << "Func2" << endl;
    }
protected:
    int _b;
};
int main()
{
    Person a;
    Student b;
    return 0;
}

我們可以通過調(diào)試來觀察一下他們的虛表和虛表指針。

顯然父類對象__vfptr[0]中存放的是BuyTicket的地址,__vfptr[1]中存放的是Func1()的地址。子類對象中__vfptr[0]中存放的是繼承并重寫的BuyTicket的地址,__vfptr[1]中存放的是繼承下來但沒有進(jìn)行重寫的Func1()的地址。通過對比我們發(fā)現(xiàn):對于沒有進(jìn)行重寫的Func1()來說,子類中虛表中的地址和父類中的是一樣的,可以說是直接拷貝下來的。而對于進(jìn)行了重寫的BuyTicket來說,子類中虛表的地址與父類中明顯不一樣,其實(shí)是在拷貝了父類的地址后又進(jìn)行了覆蓋的。因此重寫從底層的角度來說又叫做覆蓋。

同時我們又發(fā)現(xiàn)了一個問題,那就是子類對象的虛表中為什么沒有寫它自己的虛函數(shù)地址Func2()呢?

其實(shí)是寫了的,只不過通過VS的監(jiān)視窗口并不能看到,我們可以通過內(nèi)存來進(jìn)行觀察:

3.觀察虛表的方法

(1)內(nèi)存觀察

我們可以通過觀察內(nèi)存來觀察虛函數(shù)表的情況,這里觀察的是父類對象,會發(fā)現(xiàn)在虛函數(shù)指針的地址存放的是父類對象中兩個虛函數(shù)的地址。

我們也可以觀察一下子類對象:

與父類對象中存儲的相同,唯一有區(qū)別的地方就是紫色的部分,存放的其實(shí)是子類虛函數(shù)Func2()的地址。這說明Func2()也在虛表中只不過在監(jiān)視窗口沒有看不到而已。

(2)打印虛表

虛表的地址

通過觀察內(nèi)存,對于單繼承來說,我們只需要打印對象的首元素的地址即可找到虛表,并進(jìn)行打印。

我們發(fā)現(xiàn)對象的前四個字節(jié)存儲的就是虛表的地址??梢酝ㄟ^這一點(diǎn)來打印虛表。

我們關(guān)閉一下調(diào)試來重新寫一下代碼(關(guān)閉調(diào)試后再進(jìn)行運(yùn)行地址會發(fā)生變化,但是規(guī)律是不變的)

typedef void(*vfptr)();
void Printvfptr(vfptr* table)
{
    for (int i = 0; table[i] != nullptr; i++)
    {
        printf("%d:%p\n",i,table[i]);
    }
    cout << endl;
}
int main()
{
    Person a;
    Student b;
    Printvfptr((vfptr*)*(void**)&a);
    Printvfptr((vfptr*)*(void**)&b);
    return 0;
}

下面來解釋一下如何打印的虛表,分為兩部分,一部分是函數(shù),一部分是傳參:

函數(shù)

首先我們明確,虛函數(shù)指針是一個函數(shù)指針,因此為了簡便我們可以將函數(shù)指針重命名為vfptr。

通過接收虛表指針,并依次打印指針數(shù)組中的內(nèi)容(虛函數(shù)的地址)。

傳參

拿父類對象a舉例,我們要找到a的前四個字節(jié)的內(nèi)容,即為虛表指針,然后再傳入函數(shù)中。

首先使用(void**)對a的地址進(jìn)行強(qiáng)制類型轉(zhuǎn)換,這其中發(fā)生了切割。使用(void**)的原因在于,由于不知道是使用的32位還是64位系統(tǒng),但我們可以通過指針的大小來判斷。首先將&a轉(zhuǎn)換成一個指針,再將其轉(zhuǎn)換成一個指針類型,再進(jìn)行解引用就得到了a的前4或者8個字節(jié)。但同時我們需要傳遞的是一個vfptr類型的函數(shù)指針,所以還需要進(jìn)行(vfptr*)類型的強(qiáng)制轉(zhuǎn)換。

有了前面的解釋,我們就可以理解打印虛表的原理了,我們把這段代碼運(yùn)行一下:

發(fā)現(xiàn)分別打印出了a和b的虛函數(shù)表。

如果打印的虛函數(shù)數(shù)量不對,這是VS編譯器的bug,我們可以重新生成解決方案,再重新運(yùn)行代碼。

(3)虛表的位置

我們還可以觀察一下虛表的位置,在哪個區(qū)域:

使用其他區(qū)域的變量進(jìn)行對比:

    Person per;
    Student std;
    int* p = (int*)malloc(4);
    printf("堆:%p\n", p);
    int a = 0;
    printf("棧:%p\n", &a);
    static int b = 1;
    printf("數(shù)據(jù)段:%p\n", &b);
    const char* c = "aaa";
    printf("常量區(qū):%p\n", &c);
    printf("虛表:%p\n", *(void**)&std);

打印的結(jié)果是:

我們發(fā)現(xiàn)虛表的位置在數(shù)據(jù)段和常量區(qū)之間。大致屬于數(shù)據(jù)段。

4.多態(tài)的底層過程

class Person
{
public:
    virtual void BuyTicket()
    {
        cout << "全價" << endl;
    }
    virtual void Func1()
    {
        cout << "Func1" << endl;
    }
protected:
    int _a;
};
class Student :public Person
{
public:
    virtual void BuyTicket()
    {
        cout << "半價" << endl;
    }
    virtual void Func2()
    {
        cout << "Func2" << endl;
    }
protected:
    int _b;
};
void F(Person& p)
{
    p.BuyTicket();
}
int main()
{
    Person per;
    Student std;
    F(per);
    F(std);
    return 0;
}

我們還使用這一段代碼來舉例,首先復(fù)習(xí)一下多態(tài):使用父類的指針或者引用去接收子類或者父類的對象,使用該指針或者引用調(diào)用虛函數(shù),調(diào)用的是父類或子類中不同的虛函數(shù)。

下面來分析原理:

父類對象原理:

首先用父類引用p來接收父類對象per,此時p中的虛表和per中的虛表一模一樣,只需要訪問__vfptr中的BuyTicket()的地址即可調(diào)用該函數(shù)。

子類對象的原理:

用p來接收子類對象std,發(fā)生切片處理,會將子類中的虛表內(nèi)容拷貝到父類引用p中,然后再調(diào)用其中的__vfptr中的BuyTicket地址。此時的p不是新創(chuàng)建了一個父類對象,而是子類對象std切片后構(gòu)成的,其中就將重寫之后的BuyTicket()的地址也隨之切入了p??梢园裵看成原std的包含__vfptr的一部分。

總結(jié):基類的指針或者引用,指向誰就去誰的虛函數(shù)表中找到對應(yīng)位置的虛函數(shù)進(jìn)行調(diào)用。

5.幾個原理性問題

了解了多態(tài)原理之后,就可以分析出在上一節(jié)中出現(xiàn)的一些現(xiàn)象規(guī)律。

(1)虛表中函數(shù)是公用的嗎?

虛表中的函數(shù)和類中的普通函數(shù)一樣是放在代碼段的,只是虛函數(shù)還需要將地址存一份到虛表,方便實(shí)現(xiàn)多態(tài)。這也就說明同一類型的不同對象的虛表指針是相同的,我們還可以通過調(diào)試觀察:

    Person per;
    Person pper;

(2)為什么必須傳入指針或引用而不能使用對象?

當(dāng)我們使用父類對象去接收時,父類對象本身就具有一個虛表了,當(dāng)子類對象傳給父類對象的時候,其他內(nèi)容會發(fā)生拷貝,但是虛表不會,C++這樣處理的原因在于,如果虛表也會發(fā)生拷貝的話,那么該父類對象的虛表就存了子類對象的虛表,這是不合理的。

我們同樣可以通過調(diào)試來進(jìn)行觀察:

void F(Person p)
{
    p.BuyTicket();
}
int main()
{
    Person per;
    Student std;
    F(std);
}

這是std中的虛表內(nèi)容。

這是p中的虛表內(nèi)容,而且在調(diào)試過程中,程序是進(jìn)入父類中進(jìn)行調(diào)用函數(shù)的。

(3)為什么私有虛函數(shù)也能實(shí)現(xiàn)多態(tài)?

這是因?yàn)榫幾g器調(diào)用了父類的public接口,由于是父類的引用或者指針,因此編譯器發(fā)現(xiàn)是public之后就不再進(jìn)行檢查了,只要在虛表中可以找到就能調(diào)用函數(shù)。

(4)VS中的虛表中存的是指令地址?

在VS2019中,為了封裝嚴(yán)密,其實(shí)虛表中存入的是跳轉(zhuǎn)指令,我們可以通過反匯編進(jìn)行觀察:

我們將虛表中的地址輸入反匯編,看到的是這樣的一條語句:

這是一條跳轉(zhuǎn)指令,會跳轉(zhuǎn)到BuyTicket()的實(shí)際地址處。

6.多繼承中的虛表

談到多繼承就要談到菱形虛擬繼承,這是一個龐大而復(fù)雜的問題,需要更大的大佬來解釋。

這里只介紹多繼承中虛表的內(nèi)容:

class Base1
{
public:
    virtual void Func1()
    {
        cout << "Func1" << endl;
    }
    virtual void Func2()
    {
        cout << "Func2" << endl;
    }
protected:
    int _a;
};
class Base2
{
public:
    virtual void Func3()
    {
        cout << "Func3" << endl;
    }
    virtual void Func4()
    {
        cout << "Func4" << endl;
    }
};
class Derive :public Base1, Base2
{
public:
    virtual void Func5()
    {
        cout << "Func5" << endl;
    }
};
int main()
{
    Derive a;
}

我們可以使用調(diào)試來觀察a中的虛表內(nèi)容:

通過調(diào)試我們可以看到a中有兩個虛表指針分別存放的是Base1中虛函數(shù)的地址和Base2中虛函數(shù)的地址,那么a中特有的類Func5()存在哪個虛表呢?這需要通過內(nèi)存進(jìn)行觀察:

我們發(fā)現(xiàn)它被存放在了第一個虛表指針指向的虛表中。

我們知道打印第一個虛表指針指向虛表的方法,那么第二個虛表指針的該怎樣進(jìn)行處理呢:

Printvfptr((vfptr*)*(void**)((char*)&a+sizeof(Base1));

注意需要先將&a轉(zhuǎn)換成char*類型,這樣對其加一,才代表加一個字節(jié)。

以上就是詳解C++中多態(tài)的底層原理的詳細(xì)內(nèi)容,更多關(guān)于C++多態(tài)原理的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論