再谈c++中的多态
何為多態
多態的概念:通俗來說,就是多種形態,具體點就是去完成某個行為,當不同的對象去完成時會產生出不同的狀態。
多態的實現
在繼承的體系下
體現多態性
在代碼運行時,基類指針指向哪個類的對象,就調用哪個類的虛函數
class Person { public:Person(const string& name,const string& gender,const int age):_name(name), _gender(gender), _age(age){}void BuyTicket(){cout << "全價票" << endl;} protected:string _name;string _gender;int _age; };class Studnet :public Person { public:Studnet(const string& name, const string& gender, const int age,const int _stuId):Person(name,gender,age), _stuId(_stuId){}void BuyTicket(){cout << "半價票" << endl;} protected:int _stuId;};class Soldier:public Person { public:Soldier(const string& name, const string& gender, const int age, const string& rank):Person(name,gender,age), _rank(rank){}void BuyTicket(){cout << "免費" << endl;} protected:string _rank; };void TestBuyTicket(Person& p) {p.BuyTicket(); }int main() {Person p("Tom", "男", 18);Studnet st("小帥", "女", 19,1000);Soldier so("威武", "男", 23, "班長");TestBuyTicket(p);TestBuyTicket(st);TestBuyTicket(so);system("pause");return 0; }
并沒有體現出多態性,如果要讓不同的人買到各自的票,我們可以寫三個重載函數來實現,但是這樣的話,代碼的重復性太高,所以這里就要使用多態。
什么是虛函數
虛函數:就是在類的成員函數的前面加virtual關鍵字
virtual void BuyTicket(){cout << "全價票" << endl;}什么虛函數的重寫?
虛函數的重寫:**派生類中有一個跟基類的完全相同虛函數,我們就稱子類的虛函數重寫了基類的虛函數,完全相同是指:函數名、參數、返回值都相同。**另外虛函數的重寫也叫作虛函數的覆蓋。
也就是說派生類重寫基類中的某個虛函數—>派生類函數必須要與基類中的虛函數原型完全一致
注意事項
我們定義一個函數,形參為基類的指針,然后在主函數中分別創建基類對象,子類對象,然后將他們傳進去,第一個傳的是基類對象,所以是基類的指針指向了基類的對象,所以調的全部都是基類的函數。第二個傳的是子類的對象,就是基類指針指向子類對象,但是因為Func2函數與基類中的原型不一致,Func3()函數基類中沒有加virtual關鍵字,所以這兩個函數并沒有實現多態,都調用的是基類的函數,而1和4調用的是子類的函數,實現了多態。
虛函數重寫的例外:協變
基類中虛函數**返回基類(基類1)**的引用(指針),子類的虛函數返回子類1(只要繼承于基類1)的引用(指針)基類和子類虛函數的返回值類型不同
class A{};//基類 class B : public A {}; //子類 class Person {//基類1 public:virtual A* f() {return new A;}//返回值的是基類,不是基類1 }; class Student : public Person {//子類1繼承于基類1 public:virtual B* f() {return new B;}//返回值是子類,不是子類1的 };虛函數重寫的例外:虛函數
函數名字不同,但是可以構成重寫。編譯器對析構函數的名稱做了特殊處理,編譯后
析構函數的名稱統一處理成destructor,這也說明的基類的析構函數最好寫成虛函數。
針對于上面的代碼,如果基類和子類都不寫成虛函數。
基類不是虛函數,但是子類是。程序會出內存泄露
正常的結果
接口繼承和實現繼承
普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實現。虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是為了重寫,達成多態,繼承的是接口。所以如果不實現多態,不要把函數定義成虛函數。
重載,重寫(覆蓋),重定義(隱藏)
| 兩個函數在同一作用域 | 兩個函數分別在基類和派生類的作用域 | 兩個函數分別在基類和派生類的作用域 |
| 函數名/參數相同 | 函數名/參數/返回值都必須是相同的(協變例外) | 函數名相同 |
| 兩個函數必須是虛函數 | 兩個基類和派生類的同名函數不構成重寫就是重定義 |
按值傳參與按指針/引用傳參的區別
void TestBuyTicket(Person *p) {p->~Person(); }- 在編譯階段,編譯器無法確認基類的指針到底指向哪個類的對象,因為函數在執行期間才會傳參,因此在編譯期間無法確認虛函數的行為。只能在代碼運行時,才可以確定該基類指針指向哪個類的對象。
- 編譯期間,因為該函數按照值的方式傳參,參數已經確認。因此在編譯階段,就會生成基類的臨時對象,因此該函數在編譯期間可以確定虛函數行為,已經確定調用哪個類的函數。
C++11 override 和 final
override:只能修飾派生類的虛函數
作用:檢測派生類中的某個虛函數是否重寫了哪個虛函數,防止函數名有時候寫錯,沒有構成重寫。
final:可以修飾類—表示該類不能被繼承,修飾虛函數—虛函數不能被繼承
抽象類
在虛函數的后面寫上 =0 ,則這個函數為純虛函數。包含純虛函數的類叫做抽象類(也叫接口類),抽象類不能實例化出對象。派生類繼承后也不能實例化出對象,只有重寫純虛函數,派生類才能實例化出對象。純虛函數規范了派生類必須重寫,另外純虛函數更體現出了接口繼承。
class WC { public:void GoToManRoom(){cout << "go to left"<<endl;}void GoToWoManRoom() {cout << "go to right"<<endl;} }; //作用:規范后續的接口 class Person {//不能實例化對象,但可以創建該類的指針 public://純虛函數virtual void GoToWc(WC& wc) = 0;string _name;int _age; };class Man :public Person { public:void GoToWc( WC& wc){wc.GoToManRoom();} }; class WoMan :public Person { public:void GoToWc(WC& wc){wc.GoToWoManRoom();} }; #include<Windows.h> #include<time.h> //Monster 也是抽象類,因為該類沒有重寫基類中的純虛函數 class Monster :public Person {}; void TestWC(int n) {WC wc;srand(time(nullptr));for (int i = 0; i < n; ++i){Person* pGuard;if (rand() % 2 == 0)pGuard = new Man;elsepGuard = new WoMan;pGuard->GoToWc(wc);delete pGuard;Sleep(1000);} } int main() {//Person* p;TestWC(10);system("pause");return 0; }多態的原理
無繼承
class Base { public:Base(){}virtual void TestFunc3(){cout << "Base::TestFunc2()" << endl;}virtual void TestFunc1(){cout << "Base::TestFunc1()" << endl;}virtual void TestFunc2(){cout << "Base::TestFunc2()" << endl;}int _b; };- 一個類中包含有虛函數,會給該類的對象多增加四個字節,該4字節中的內容是在構造函數中填充的。
- 如果類沒有顯示定義構造函數,編譯器會給該類生成一個默認的構造函數,作用:給對象的前4個字節賦值
- 如果類顯示定義了自己的構造函數,編譯器將會對構造函數進行修改,多增加一條語句,給對象的前4個字節賦值
出現繼承
class Base { public:Base(){}virtual void TestFunc3(){cout << "Base::TestFunc2()" << endl;}virtual void TestFunc1(){cout << "Base::TestFunc1()" << endl;}virtual void TestFunc2(){cout << "Base::TestFunc2()" << endl;}int _b; }; class Derived :public Base {};虛表指針不一樣,派生類和基類用的不是同一張虛表
基類虛表構建過程
將虛函數按照其在類中的聲明次序依次放到虛表中。
class Base { public:Base(){}virtual void TestFunc3(){cout << "Base::TestFunc2()" << endl;}virtual void TestFunc1(){cout << "Base::TestFunc1()" << endl;}virtual void TestFunc2(){cout << "Base::TestFunc2()" << endl;}int _b; }; class Derived :public Base { public:virtual void TestFunc1(){cout << "Derived::TestFunc1()" << endl;}virtual void TestFunc3(){cout << "Derived::TestFunc2()" << endl;}int _d; };派生類虛表的構建過程
調用原理
class Base { public:Base(){}virtual void TestFunc1(){cout << "Base::TestFunc1()" << endl;}virtual void TestFunc2(){cout << "Base::TestFunc2()" << endl;}virtual void TestFunc3(){cout << "Base::TestFunc3()" << endl;}void TestFunc4(){cout << "Base::TestFunc4()" << endl;}int _b; }; class Derived :public Base { public:virtual void TestFunc1(){cout << "Derived::TestFunc1()" << endl;}virtual void TestFunc2(){cout << "Base::TestFunc2()" << endl;}virtual void TestFunc3(){cout << "Derived::TestFunc3()" << endl;}int _d; }; //虛函數的調用:通過基類的指針或者引用調用虛函數 void TestVirtual(Base *pb) {pb->TestFunc1();pb->TestFunc2();pb->TestFunc3();pb->TestFunc4(); } 01104BC3 mov eax,dword ptr [pb] 01104BC6 mov edx,dword ptr [eax] //前兩步,從對象前4個字節取虛表的地址 01104BC8 mov esi,esp 01104BCA mov ecx,dword ptr [pb] //傳遞this指針 01104BCD mov eax,dword ptr [edx+4] //從虛表中找對應的虛函數 01104BD0 call eax //調用虛函數 //重新解析d對象的內存空間, //將d對象的內存空間按照基類對象方式進行解析 //這個過程并沒有創建新的對象,所以還是調用派生類的函數 Base* pb = (Base*)&d; pb->TestFunc1(); return 0;打印虛函數表
單繼承
class Base { public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; } private:int a; }; 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; } private:int b; }; typedef void(*VFPTR) (); void PrintVTable(VFPTR vTable[]) {// 依次取虛表中的虛函數指針打印并調用。調用就可以看出存的是哪個函數cout << " 虛表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d個虛函數地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();//調用虛函數,f=vTable[i] vTable[i]等于一個虛函數入口地址}cout << endl; } int main() {Base b;Derive d; // 思路:取出b、d對象的頭4比特,就是虛表的指針,虛函數表本質是一個存虛函數指針的指針數組,這個數組最后面放了一個nullptr // 1.先取b的地址,強轉成一個int*的指針 // 2.再解引用取值,就取到了b對象頭4比特的值,這個值就是指向虛表的指針 // 3.再強轉成VFPTR*,因為虛表就是一個存VFPTR類型(虛函數指針類型)的數組。 // 4.虛表指針傳遞給PrintVTable進行打印虛表 // 5.需要說明的是這個打印虛表的代碼經常會崩潰,因為編譯器有時對虛表的處理不干凈,虛表最后面沒有 //放nullptr,導致越界,這是編譯器的問題。我們只需要點目錄欄的 - 生成 - 清理解決方案,再編譯就好了。VFPTR* vTableb = (VFPTR*)(*(int*)&b);PrintVTable(vTableb);VFPTR* vTabled = (VFPTR*)(*(int*)&d);PrintVTable(vTabled);system("pause");return 0; }多繼承
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; }; typedef void(*VFPTR) (); void PrintVTable(VFPTR vTable[]) {cout << " 虛表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d個虛函數地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl; } int main() {Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb2);system("pause");return 0; }
多繼承派生類虛表的構建過程
總結
問題
什么是多態?
概念
同一個事物在不同場景下可以表現出的多種形態,例如迪迦奧特曼的三種形態。
多態的分類
| 在編譯時,可確定具體哪個函數 | 在編譯階段無法確定函數具體調用哪個函數,必須在代碼運行時才能確定,無法確定基類指針或者引用到底指向哪個類的對象 |
| 函數重載/模板 | 虛函數+繼承 |
多態的實現條件
inline函數可以是虛函數嗎?
不能,因為inline函數沒有地址,無法把地址放到虛函數表中。inline在編譯時期展開,多態發生在運行時。
靜態成員可以是虛函數嗎?
不能,因為靜態成員函數沒有this指針,使用類型::成員函數的調用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。
構造函數可以是虛函數嗎?
不能,因為對象中的虛函數表指針是在構造函數初始化列表階段才初始化的。
構造函數的作用:
析構函數可以是虛函數嗎?什么場景下析構函數是虛函數?
可以,并且最好把基類的析構函數定義成虛函數。析構函數可以為虛函數---->重寫的一種特例,因為派生類重寫基類中的虛析構函數,名字不一樣。
class Base { public:Base(int b):_b(b){cout << "Base::Base()" << endl;}virtual ~Base(){cout << "Base::~Base()" << endl;}int _b; }; class Derived :public Base { public:Derived(int b):Base(b){_p = new int[10];}~Derived(){delete[]_p;} private:int *_p; }; int main() {//靜態類型:聲明變量時的類型----在編譯期間起作用//動態類型:實際引用(指向)的類型----在運行時確定調用哪個類的虛函數Base* pb = new Derived(10);delete pb;//看析構函數是不是虛函數,如果不是用靜態類型,//delete:1.調用析構函數釋放對象中的資源// 2.調用operator delete()釋放對象的空間system("pause");return 0; }如果派生類中涉及到動態資源的管理(比如:子類從堆上申請空間),建議:基類中的析構函數最好設置為虛函數,否則可能存在內存泄露
對象訪問普通函數快還是虛函數更快?
首先如果是普通對象,是一樣快的。如果是指針對象或者是引用對象,則調用的普通函數快,因為構成多態,運行時調用虛函數需要到虛函數表中去查找。
| 傳參(如果有參數) | 跟普通函數調用一樣 | 從對象的前4個字節中取虛表地址 |
| 通過指令call調用該函數(call 函數入口地址) | 傳參(包括this指針) | |
| 從虛表中獲取函數地址 | ||
| 調用函數 |
多態的缺陷:降低程序運行的速度
虛函數表是在什么階段生成的,存在哪的?
虛函數是在編譯階段就生成的,一般情況下存在代碼段(常量區)的。
總結
- 上一篇: 性生活出血会导致不孕不育吗
- 下一篇: 再谈c++中的继承