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.