? ? ? ?在上篇文章中,簡單的介紹了多態中的概念以及其相關原理。本文將針對多態中其他的概念進一步進行介紹,并且更加深入的介紹關于多態的相關原理。
目錄
1. 抽象類:
2. 再談虛表:
3. 多繼承中的虛函數表:
1. 抽象類:
? ? ? ?在上篇文章中提到了,如果使用關鍵字修飾一個成員函數,則這個成員函數被稱為虛函數。此處,針對虛函數進行擴展,如果在虛函數的聲明后面加上
,則這個函數被稱為純虛函數。包含純虛函數的類又叫抽象類,其特點是不能初始化出對象。即使是子類繼承這個類,同樣也不能初始化出對象。只有認為對純虛函數進行重寫,才能初始化出一個對象。
? ? 給定一個抽象類及其子類如下:
//抽象類
class Person
{
public:virtual void func() = 0{cout << "Person-func()";}
};class Teacher : public Person
{
public:};class Student : public Person
{
public:};
如果向初始化出這三個類的對象,即:
?
int main()
{Person p;Student s;Teacher t;
}
此時編譯器報錯如下:
如果對子類中繼承父類中的純虛函數進行重寫,即:
class Teacher : public Person
{
public:virtual void func(){cout << "Teacher-func()" << endl;}
};class Student : public Person
{
public:virtual void func(){cout << "Student-func()" << endl;}
};
此時再去分別初始化兩個子類的對象,即:
int main()
{Student s;s.func();Teacher t;t.func();
}
代碼可以正常運行,且運行結果如下:
2. 再談虛表:
在之前基礎的文章中提到了,在構造函數中,存在初始化列表,初始化列表初始化成員變量的順序并不是根據初始化列表的順序,而是根據成員變量聲明的順序。對于虛函數,其也符合這個特性。具體可以用下面的代碼進行證明:
class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}
通過監視窗口,查看對象中虛表:
? ? ? ?可以看到,虛函數在虛表中存放的順序,正是虛函數在類中聲明的順序。對于這一點,也同樣可以在內存窗口中進行查看。
? ? ? ?
?從圖中不難發現,對象中的第一個地址,恰好對應了虛表指針的地址。此時再查看虛表中的內容,即:
不難看出,再內存窗口中,第二,第三條地址分別對應了虛表中兩個虛函數的地址。
而對于子類,其生成的對象中的內容如下:
對于子類對象的內容,可以分為兩個部分,一是從父類中繼承的內容,二是子類中自己的成員變量以及函數。在監視窗口中,可以看到子類繼承了父類的虛表,并且對其中進行重寫的虛函數的地址進行了覆蓋。但是需要注意,在子類中,并不存在自己的虛表?。對于子類虛表中的函數指針如下:在上面給出的圖片中可以看出,藍線連接的兩個地址分別是父類、子類中的虛函數
,但是因為這個函數在子類中發生了重寫,因此,父類,子類中這兩個虛函數的地址并不相同。
而對于紫線連接的兩個虛函數,由于虛函數并未在子類中發生虛函數的重寫,因此,父類,子類中倆個虛函數的地址相同。
如果對于子類,再添加一個虛函數,例如:
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}virtual void Func(){;}
private:int _d = 2;
};
?此時,在監視窗口中進行查看,子類對象的虛表中并沒有出現新的虛函數的函數指針,但是在內存窗口中,卻出現了一條新的地址,對于這個新的地址,一般認為就是子類中新加入的虛函數。至于具體的驗證,將在文章后面給出。?
?
(注:為了方便演示,下面的代碼在,即
位環境下運行)
在之前基礎關于內存管理的文章中(C++(9)——內存管理-CSDN博客?)提到了系統根據不同的需求,將內存劃分為不同的部分,具體如下:
1.棧:用于存儲非全局、非靜態的局部變量,函數參數,返回值等等
2.堆:用于程序運行時的內存的動態開辟
3.數據段(靜態區):用于存儲全局變量和靜態變量
4.代碼段(常量區):可執行代碼\只讀常量
在給出了上述概念后,文章將探討一個 問題,即:虛表指針是存儲在什么地方的。
為了方便測試,首先給出上面四個類型變量的地址,即:
int i = 1;//棧int* p = new int;//堆static int j = 0;//數據段(靜態區)const char* p2 = "xxxxxxx";//代碼段(常量區)printf("棧=%p\n", &i);printf("堆=%p\n", p);printf("靜態區=%p\n", &j);printf("常量區=%p\n",p2);
打印結果如下:
對于如何獲取虛表指針,本文提供一種方法:由于虛表指針存儲在一個類的前四個字節,因此,只需要初始化出一個該類的對象,首先獲取這個對象的指針,在將這個指針強轉成類型,即可獲取虛表指針,具體代碼如下:
Base* B = &b;Derive* D = &d;printf("B=%p\n", *(int*)B);printf("D=%p\n", *(int*)D);
打印結果如下:
從上述區段以及兩個虛表指針的指針對比來看,虛表指針應該存儲在常量區,也就是代碼段。
上面給出了如何獲取虛表指針的存儲地址,下面給出虛表中,如何獲取虛表中存儲各個虛函數的指針,具體方法如下:
typedef void(*VF_PTR)();
void PrintVF(VF_PTR* vf)
{for (size_t i = 0; vf[i] != nullptr; i++){printf("[%d] :%p", i, vf[i]);}
}
PrintVF((VF_PTR*) * (int*)&d);
打印結果如下:
如果在獲取了上述指針后,直接調用這些函數指針,便可知道上述 獲取的地址是否是類中的虛函數,即:
?
typedef void(*VF_PTR)();
void PrintVF(VF_PTR* vf)
{for (size_t i = 0; vf[i] != nullptr; i++){printf("[%d] :%p", i, vf[i]);VF_PTR f = vf[i];f();}
}
打印結果如下:
通過這個例子可以看出,雖然在上面添加新的虛函數時,在子類的虛表中并沒有看到這個函數的地址,但是在次數,照樣可以通過函數指針調用這個函數,這也間接證明了
其實添加到了子類中,只是在監視窗口不可見。
3. 多繼承中的虛函數表:
給定代碼如下:
class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};
class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};int main()
{Derive d;Base1* p1 = &d;Base2* p2 = &d;return 0;
}
在上面給出的代碼中,存在三個類,其中被集成到了
中,由于
最先被繼承到子類中,因此,可以認為,父類成員在子類的空間中的位置是最靠前的。對于&
,表示取對象
的首地址,由于父類成員在空間中位置是最靠前的,因此,理論上
&
。而對于
,由于其在
后繼承,因此
相對于
是靠后的,因此,在子類中,存在著兩張虛表,這兩個虛表分別有著自己獨立的地址。在監視窗口中,同樣可以證明這一點:
而對于中的虛函數
,為了驗證
是存儲在哪個虛表中的,可以用下面的代碼進行檢驗:
PrintVF((VF_PTR*)*(int*)p1);
對于中虛表中存儲的函數指針打印結果如下:
下面打印中虛表中的函數指針:
由此證明,子類中的虛函數是存儲在子類繼承并且進行覆蓋的
中的虛表。