注意? ?virtual關鍵字:
? ? 1、可以修飾原函數,為了完成虛函數的重寫,滿足多態的條件之一
? ?2、可以菱形繼承中,去完成虛繼承,解決數據冗余和二義性
兩個地方使用了同一個關鍵字,但是它們互相一點關系都沒有
虛函數重寫:
?
多態的條件:
1、虛函數的重寫
2、父類對象的指針或者引用去調用虛函數
必須是父類指針或者引用
不可以是子類因為父類不可以傳給子類
class Person
{
public:virtual void BuyTicket() { cout << "Person全票" << endl; }
};
class Student : public Person
{
public:virtual void BuyTicket() { cout << "Student半票" << endl; }
};
void func(Person& p1)
{p1.BuyTicket();
}
int main()
{Person p1;Student s1;func(p1);func(s1);return 0;
}
協變(是多態的一種特殊情況):
多態:
1、虛函數的重寫(必須要函數名、返回值、參數要相同)
2、父類對象的指針或者引用去調用虛函數
但是協變可以返回值可以不同
但是返回值必須是基類的指針或引用和子類的指針或引用
//class A
//{
//};
//class B :public A
//{
//}
//其他類的基類和派生類也可以
//class Person
//{
//public:
// virtual A* BuyTicket() { cout << "Person全票" << endl; return nullptr; }
//};
//class Student : public Person
//{
//public:
// virtual B* BuyTicket() { cout << "Student半票" << endl; return nullptr; }
//};
//void func(Person& p1)
//{
// p1.BuyTicket();
//}
//class Person
{
public:virtual Person* BuyTicket() { cout << "Person全票" << endl; return nullptr; }
};
class Student : public Person
{
public:virtual Student* BuyTicket() { cout << "Student半票" << endl; return nullptr;}
};
void func(Person& p1)
{p1.BuyTicket();
}
int main()
{Person p1;Student s1;func(p1);func(s1);return 0;
}
析構函數:
面試題:析構函數需不需要加vitrual?
class Person
{
public:~Person() { cout << "~Person()" << endl; }};
class Student : public Person
{
public:~Student() { cout << "~Student()" << endl;}
};int main()
{Person* p1= new Student;delete p1;return 0;
}
這種情況下父類的指針指向了new Student 但是使用完會造成內存泄漏,父類的指針只會調用父類的析構函數去清理該指向部分的空間,但是我們需要清理子類的空間就要調用子類的析構函數,所以需要加virtual 構成虛函數的重寫,讓父類的指針調用構成多態,就可以調用子類的析構函數。
?
看下一道面試題:
在做面試題之前先看下面代碼
在繼承關系中,
如何理解上述話呢?
看下面代碼
在滿足多態的條件下,虛函數的繼承是繼承了接口,所以缺省值繼承了,但是子類要自己重寫實現
所以當父類中的有虛函數,子類的就可以不用加virtual,但是不規范
答案:是B
為什么多態就要繼承父類的接口?突然感悟
比喻:子類中的函數 drive(Banz* const this),父類也有(Car* const this)
??//子類這個this是接收不了父類的指針,只有父類的指針或引用才可以指向子類
???//所以這個繼承接口才需要繼承父類的接口----突然感悟
============下面代碼===============?
//作者:螞蟻捉蟲蟲
//鏈接:https ://www.zhihu.com/question/517444641/answer/2390138862
//來源:知乎
//著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
#include <iostream> // std::cout
class Base {public:Base() {};virtual void func_a(int a = 0) {}; //這個是虛函數,子類只繼承接口,具體的實現,由子類去實現void func_b(int b) { std::cout << b + 10 << "\n"; }; //這個是實函數,其接口和實現,都會被子類繼承
};class Base_A : public Base {
public:void func_a(int a=15) { std::cout << a << "\n"; };
};class Base_B : public Base {
public:void func_a(int a) { std::cout << a + 15 << "\n"; };
};int main()
{Base_A a;Base_B b;a.func_a(); //僅僅繼承了基類的接口,但沒有繼承實現a.func_b(10); //繼承了基類的接口及實現std::cout << std::endl;b.func_a(10); //僅僅繼承了基類的接口,但沒有繼承實現b.func_b(10); //繼承了基類的接口及實現return 0;
}
?
只有在滿足多態的情況下,虛函數的繼承才是父類的虛函數繼承對于子類來說繼承的是父類的接口(包括缺省值),子類函數的實現需要子類來寫
上述代碼只是完成了重寫,并沒有滿足多態,所以并沒有繼承接口
關鍵字final和override
1、final修飾虛函數,表示該虛函數不能再被繼承
也可以修飾class叫最終類不能被繼承
override關鍵字:檢查子類的虛函數是否完成重寫
構成虛函數重寫嗎?
沒有,認真看,但是不會報錯,所以,加上override就可以自動檢測檢查子類的虛函數是否完成重寫
重載、重寫、重定義
抽象類
可以看下列代碼:
//作者:螞蟻捉蟲蟲
//鏈接:https://www.zhihu.com/question/517444641/answer/2390138862
//來源:知乎
//著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。class Base {public:Base(){};virtual void func_a(int a) = 0; //這個是純虛函數,子類只繼承接口,具體的實現,由子類去實現void func_b(int b) {std::cout << b+10 << "\n";}; //這個是實函數,其接口和實現,都會被子類繼承
};class Base_A: public Base{
public:void func_a(int a){std::cout << a << "\n";};
};class Base_B: public Base{
public:void func_a(int a){std::cout << a + 15 << "\n";};
};int main ()
{Base_A a;Base_B b;a.func_a(10); //僅僅繼承了基類的接口,但沒有繼承實現a.func_b(10); //繼承了基類的接口及實現std::cout << std::endl;b.func_a(10); //僅僅繼承了基類的接口,但沒有繼承實現b.func_b(10); //繼承了基類的接口及實現return 0;
}
上述代碼里,定一個基類,里面有兩個成員函數,一個是虛函數,一個是實際函數;然后又定義了兩個子類,Base_A和Base_B,兩個子類對基類中的func_b函數有不一樣的實現
純虛函數的作用強制子類完成重寫
表示抽象的類型。抽象就是在現實中沒有對應的實體的
接口繼承和實現繼承
多態的原理:
測試我們發現b對象是8個字節,除了_b成員,還多了一個指針_vfptr放在對象對面,我們叫做虛函數指針我們叫做虛函數表指針。一個含有虛函數表的類中至少都有一個虛函數表指針,因為虛函數的地址要被放到虛函數表中,虛函數表稱虛表
注意:虛函數存放在哪里? 虛表存在哪里
虛表存的是虛函數指針,不是虛函數,虛函數也是函數所以也是存在代碼區,只是它的地址被存進虛函數指針中,這個指針被虛表記錄著
重寫:接口繼承,實現重寫,在原理上是覆蓋將父類繼承下來的vfptr的父類虛函數的地址覆蓋成子類的虛函數地址
從反匯編看原理:
普通類函數:
在編譯的過程中就已經確定了調用函數的地址
現在我們加上virtual虛函數
進入匯編,當形成多態時是如何調用的?
00B021E1 8B 45 08 mov eax,dword ptr [A] //將A指向空間地址給eax
00B021E4 8B 10 mov edx,dword ptr [eax] //將eax空間中的前四個字節地址給edx就是虛函數表指針
00B021E6 8B F4 mov esi,esp//這個是維護函數棧幀的寄存器,不用管
00B021E8 8B 4D 08 mov ecx,dword ptr [A] //將A指向空間地址給ecx
00B021EB 8B 42 04 mov eax,dword ptr [edx+4] //因為edx保存的是前四個字節空間的地址就是虛函數表指針+4就是run()的地址,將run()地址給eax,前4個是speak()的地址
00B021EE FF D0 call eax //調用run()
00B021F0 3B F4 cmp esi,esp
00B021F2 E8 1A F1 FF FF call __RTC_CheckEsp (0B01311h)
?多態就是有virtual函數是用虛函數表指針去存放虛函數的地址,在由虛函數表指針調用對應的函數
面試題:
虛函數存在哪里?代碼段,虛函數和普通函數一樣都是函數所以都是編譯成指令存進代碼段中
虛函數表存在哪里?
存在代碼段中,不是存在棧區,因為棧區是由一個個棧幀堆建的所以每調用創建一個對象就要建立一個虛表是很消耗內存的
證明一下:
虛表存放在代碼區中的代碼段最合適,堆區是動態開辟的,數據區分為bss區(存放未初始化的static和未初始化的全局變量)和數據區存放(存放初始化的static和初始化的全局變量),所以代碼段是最合適的
反向驗證:
發現很接近代碼區
總結:
多態的本質原理,符合多態的兩個條件。那么在父類的指針或引用調用時,會到指向對象的虛表找到對應的虛函數地址,進行調用
多態(程序運行時去指向對象的虛表中找到函數地址,進行調用,所以p指向誰就調用誰的虛函數)
普通函數的調用,編譯鏈接時確定函數的地址,運行時直接調用。類型時誰就是誰調用
動態綁定和靜態綁定:
編譯:就是代碼和語法檢查其實就是預處理、編譯、匯編、鏈接
運行:就是將可執行文件加載到內存中進行對數據區的數據替換
靜態綁定:更具調的類型就確定了調用的函數
動態綁定:運行時具體拿到類型確定程序的具體行為,就是在編譯時無法確定函數的行為
運行時根據寄存器去拿到函數的地址
單繼承和多繼承的虛表(不是虛基表)
單繼承:
void(*p)();? //函數指針
補充:
函數名就是函數的地址
那我們手動打印虛函數表
class base
{
public:virtual void func1() { cout << "base::func1()" << endl; }virtual void func2() { cout << "base::func2()" << endl; }};
class derive :public base
{
public:virtual void func1() { cout << "derive::func1()" << endl; }virtual void func3() { cout << "derive::func3()" << endl; }virtual void func4() { cout << "derive::func4()" << endl; }};
//void(*)()
typedef void(*VF_PTR)();//重命名函數指針void PrintVFTable(VF_PTR* pTable)//VF_PTR pTable[] 函數指針數組==虛函數表指針
{for (size_t i = 0; pTable[i] != 0; i++){printf("pTable[%d]=%p->", i, pTable[i]);VF_PTR f = pTable[i];//得到函數的地址==函數名f();}cout << endl;
}int main()
{base b1;derive d2;PrintVFTable((VF_PTR*)(*(int*)&b1));//取b1的地址因為要取到虛函數表指針,它在對象的前四個字節//所以轉換成int*在解引用就是取空間b1的前四個字節,因為此時是int*//所以要轉成VF_PTR*PrintVFTable((VF_PTR*)(*(int*)&d2));return 0;
}
多繼承的虛表:
計算一下test 對象等于多少?
class base
{
public:virtual void func1() { cout << "base::func1()" << endl; }virtual void func2() { cout << "base::func2()" << endl; }int i = 0;
};
class derive
{
public:virtual void func1() { cout << "derive::func1()" << endl; }virtual void func3() { cout << "derive::func3()" << endl; }virtual void func4() { cout << "derive::func4()" << endl; }int i = 0;
};
class test:public base,public derive
{
public:virtual void func3() { cout << "test::func1()" << endl; }virtual void func2() { cout << "test::func3()" << endl; }virtual void func7() { cout << "test::func4()" << endl; }
public:int i = 0;
};//void(*)()
typedef void(*VF_PTR)();//重命名函數指針void PrintVFTable(VF_PTR* pTable)//VF_PTR pTable[] 函數指針數組==虛函數表指針
{for (size_t i = 0; pTable[i] != 0; i++){printf("pTable[%d]=%p->", i, pTable[i]);VF_PTR f = pTable[i];//得到函數的地址==函數名f();}cout << endl;
}int main()
{test i;cout << sizeof(i) << endl;return 0;
}
等于20? ?
編譯器又沒顯示!!!那我們手動去看看
繼承的子類和其父類的表不是同一張表,只有同一類才是用一張表哦