C++多態內幕

c++ 多態,就是利用了一個二級指針(指針數組),數組裏的每個元素都指向了,用virtual修飾的成員函數。

既然提到了指針,那就讓我們用內存地址來證明一下吧。

爲了證明,我們必須要取到成員函數的首地址。利用下面的函數取得成員函數的地址

template<typename dst_type,typename src_type>
dst_type pointer_cast(src_type src)
{
    return *static_cast<dst_type*>(static_cast<void*>(&src));
}

調用上面函數的方法:

void* p1 = pointer_cast<void*>(&類名::成員方法名);

1,首先我們先看看非多態的成員方法的內存佈局,通過下面的證明代碼:

#include "stdio.h"
#include "string.h"
#include <iostream>
template<typename dst_type,typename src_type>
dst_type pointer_cast(src_type src)
{
    return *static_cast<dst_type*>(static_cast<void*>(&src));
}
class A
{
    private:
        int a;
        char b;
        short c;
    public:
        A(int m,char n,short t)
        {
            a = m;b = n;c = t;
        }
   void funca(){
     std::cout << "A::func()" << std::endl;
   }
};
class B:public A
{
    private:
        char d;
    public:
        B(int m,char n,short t,char q):A(m,n,t)
        {
            d = q;
        }
  void funcb(){
     std::cout << "B::func()" << std::endl;
   }
};
int main()
{
    A x(1,2,3);
    B y(1,2,3,4);
   A* m = new B(1,2,3,4);
    void* p1 = pointer_cast<void*>(&A::funca);
    void* p2 = pointer_cast<void*>(&B::funcb);
    void(*a)() = (void(*)())p1;
    a();
    return 0;
}

實驗環境:gcc version 7.3.0。ubuntu18.04

下圖是GDB的調試截圖

gdb裏的操作步驟1,先用【info line 22】命令去得到A類的成員函數funca的地址,22是成員函數funca所在的行號。

gdb裏的操作步驟2,因爲p1是指向A類的成員函數funca的指針,所以【p p1】的結果就是A類的成員函數funca的地址。

結論:通過步驟1和步驟2的結果來看,得出的兩個地址是相同的。

說明了什麼呢?說明了非多態的成員函數的地址偏移量,是在編譯階段,就固定好了的。

地址偏移量是啥?

當運行程序時,先從硬盤把程序載入到內存中,這個程序就成爲了一個運行中的進程,操作系統會給進程分配虛擬內存空間,爲了能夠調用到函數,必須知道函數在的虛擬內存空間的地址。這個地址調用側怎麼知道的呢,在編譯階段,編譯器自動把被調用函數相對於進程首地址的偏移量算出來了,告訴了調用測,所以調用側才能找到被調用函數的地址。

2,我們再來看看多態的成員方法的內存佈局,通過下面的證明代碼:

#include "stdio.h"
#include "string.h"
#include <iostream>
template<typename dst_type,typename src_type>
dst_type pointer_cast(src_type src)
{
    return *static_cast<dst_type*>(static_cast<void*>(&src));
}
class A
{
    private:
        int a;
        char b;
        short c;
    public:
        A(int m,char n,short t)
        {
            a = m;b = n;c = t;
        }
   virtual void func(){
     std::cout << "A::func()" << std::endl;
   }
   virtual void func1(){
     std::cout << "A::func1()" << std::endl;
   }
};
class B:public A
{
    private:
        char d;
    public:
        B(int m,char n,short t,char q):A(m,n,t)
        {
            d = q;
        }
  virtual void func(){
     std::cout << "B::func()" << std::endl;
   }
   virtual void func1(){
     std::cout << "B::func1()" << std::endl;
   }
};
int main()
{
    A x(1,2,3);
    B y(1,2,3,4);
    A* m = new B(1,2,3,4);
    void* p1 = pointer_cast<void*>(&A::func);
    void* p2 = pointer_cast<void*>(&B::func);
    m->func();//調用的是B的func
    m->func1();//調用的是B的func1
    //void(*a)() = (void(*)())p1;
    //a();
    return 0;
}

下圖是GDB的調試截圖

指針p1指向的類A的func成員函數;

指針p2指向的類B的func成員函數;

但是從gdb的結果來看,他們指向的地址都是0x1,也就說明他們沒有正確的指向類的成員函數。

那麼類的成員函數的地址在哪裏呢?看下面的gdb截圖

通過【p x】查看對象x,發現x裏面多了個_vptr的東西。這個東西就是最開始說的二級指針(指針數組)。

步驟1:先用【info line 22】命令去得到A類的成員函數func的地址.

步驟2:【p *(void**)0x555555755d48】,先把_vptr指針轉成void型的二級指針,然後再用【*】取得地址裏的內容,發現地址類存放的就是類A的成員函數func的地址0x555555554cc4。

結論:_vptr指向的就是所有虛函數中的第一個虛函數的地址。

問題來了,如何得到第二個呢?因爲64位系統指針所佔8個字節,所以(_vptr + 8)就是第二個虛函數的地址。

下面就讓多態的真相浮出水面

當有如下代碼:用父類的指針指向子類的對象(多態的最終目的:面對抽象類編程),然後調用子類和父類完全相同的函數(必須是虛函數)。讓人迷惑,到底調用的是哪個。

A* m = new B(1,2,3,4);//B是A的子類
m->func();
m->func1();

用父類的指針調用虛函數時,先去它指向的內存中(子類所佔用的內存)找_vptr,然後從_vptr裏找函數的地址。非虛函數的地址不在_vptr裏。

步驟1:【p *m】,發現m是類A的對象

步驟2:【set print object on】,含義是顯示對象真正的類型

步驟3:【p *m】,發現m原來不是類A的對象,而是類B的對象。

步驟4:查看_vptr裏第一個指針,發現指向的是B的func;加8後得到第二個指針,發現發現指向的是B的func1.

讓虛函數們的地址存入_vptr裏面的代碼肯定是由編譯器給我們加上的,這些代碼加到哪裏了呢?

加到了構造函數裏

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章