再谈c++中的多态
何為多態(tài)
多態(tài)的概念:通俗來說,就是多種形態(tài),具體點就是去完成某個行為,當(dāng)不同的對象去完成時會產(chǎn)生出不同的狀態(tài)。
多態(tài)的實現(xiàn)
在繼承的體系下
體現(xiàn)多態(tài)性
在代碼運行時,基類指針指向哪個類的對象,就調(diào)用哪個類的虛函數(shù)
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; }
并沒有體現(xiàn)出多態(tài)性,如果要讓不同的人買到各自的票,我們可以寫三個重載函數(shù)來實現(xiàn),但是這樣的話,代碼的重復(fù)性太高,所以這里就要使用多態(tài)。
什么是虛函數(shù)
虛函數(shù):就是在類的成員函數(shù)的前面加virtual關(guān)鍵字
virtual void BuyTicket(){cout << "全價票" << endl;}什么虛函數(shù)的重寫?
虛函數(shù)的重寫:**派生類中有一個跟基類的完全相同虛函數(shù),我們就稱子類的虛函數(shù)重寫了基類的虛函數(shù),完全相同是指:函數(shù)名、參數(shù)、返回值都相同。**另外虛函數(shù)的重寫也叫作虛函數(shù)的覆蓋。
也就是說派生類重寫基類中的某個虛函數(shù)—>派生類函數(shù)必須要與基類中的虛函數(shù)原型完全一致
注意事項
我們定義一個函數(shù),形參為基類的指針,然后在主函數(shù)中分別創(chuàng)建基類對象,子類對象,然后將他們傳進去,第一個傳的是基類對象,所以是基類的指針指向了基類的對象,所以調(diào)的全部都是基類的函數(shù)。第二個傳的是子類的對象,就是基類指針指向子類對象,但是因為Func2函數(shù)與基類中的原型不一致,Func3()函數(shù)基類中沒有加virtual關(guān)鍵字,所以這兩個函數(shù)并沒有實現(xiàn)多態(tài),都調(diào)用的是基類的函數(shù),而1和4調(diào)用的是子類的函數(shù),實現(xiàn)了多態(tài)。
虛函數(shù)重寫的例外:協(xié)變
基類中虛函數(shù)**返回基類(基類1)**的引用(指針),子類的虛函數(shù)返回子類1(只要繼承于基類1)的引用(指針)基類和子類虛函數(shù)的返回值類型不同
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的 };虛函數(shù)重寫的例外:虛函數(shù)
函數(shù)名字不同,但是可以構(gòu)成重寫。編譯器對析構(gòu)函數(shù)的名稱做了特殊處理,編譯后
析構(gòu)函數(shù)的名稱統(tǒng)一處理成destructor,這也說明的基類的析構(gòu)函數(shù)最好寫成虛函數(shù)。
針對于上面的代碼,如果基類和子類都不寫成虛函數(shù)。
基類不是虛函數(shù),但是子類是。程序會出內(nèi)存泄露
正常的結(jié)果
接口繼承和實現(xiàn)繼承
普通函數(shù)的繼承是一種實現(xiàn)繼承,派生類繼承了基類函數(shù),可以使用函數(shù),繼承的是函數(shù)的實現(xiàn)。虛函數(shù)的繼承是一種接口繼承,派生類繼承的是基類虛函數(shù)的接口,目的是為了重寫,達成多態(tài),繼承的是接口。所以如果不實現(xiàn)多態(tài),不要把函數(shù)定義成虛函數(shù)。
重載,重寫(覆蓋),重定義(隱藏)
| 兩個函數(shù)在同一作用域 | 兩個函數(shù)分別在基類和派生類的作用域 | 兩個函數(shù)分別在基類和派生類的作用域 |
| 函數(shù)名/參數(shù)相同 | 函數(shù)名/參數(shù)/返回值都必須是相同的(協(xié)變例外) | 函數(shù)名相同 |
| 兩個函數(shù)必須是虛函數(shù) | 兩個基類和派生類的同名函數(shù)不構(gòu)成重寫就是重定義 |
按值傳參與按指針/引用傳參的區(qū)別
void TestBuyTicket(Person *p) {p->~Person(); }- 在編譯階段,編譯器無法確認(rèn)基類的指針到底指向哪個類的對象,因為函數(shù)在執(zhí)行期間才會傳參,因此在編譯期間無法確認(rèn)虛函數(shù)的行為。只能在代碼運行時,才可以確定該基類指針指向哪個類的對象。
- 編譯期間,因為該函數(shù)按照值的方式傳參,參數(shù)已經(jīng)確認(rèn)。因此在編譯階段,就會生成基類的臨時對象,因此該函數(shù)在編譯期間可以確定虛函數(shù)行為,已經(jīng)確定調(diào)用哪個類的函數(shù)。
C++11 override 和 final
override:只能修飾派生類的虛函數(shù)
作用:檢測派生類中的某個虛函數(shù)是否重寫了哪個虛函數(shù),防止函數(shù)名有時候?qū)戝e,沒有構(gòu)成重寫。
final:可以修飾類—表示該類不能被繼承,修飾虛函數(shù)—虛函數(shù)不能被繼承
抽象類
在虛函數(shù)的后面寫上 =0 ,則這個函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類叫做抽象類(也叫接口類),抽象類不能實例化出對象。派生類繼承后也不能實例化出對象,只有重寫純虛函數(shù),派生類才能實例化出對象。純虛函數(shù)規(guī)范了派生類必須重寫,另外純虛函數(shù)更體現(xiàn)出了接口繼承。
class WC { public:void GoToManRoom(){cout << "go to left"<<endl;}void GoToWoManRoom() {cout << "go to right"<<endl;} }; //作用:規(guī)范后續(xù)的接口 class Person {//不能實例化對象,但可以創(chuàng)建該類的指針 public://純虛函數(shù)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 也是抽象類,因為該類沒有重寫基類中的純虛函數(shù) 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; }多態(tài)的原理
無繼承
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; };- 一個類中包含有虛函數(shù),會給該類的對象多增加四個字節(jié),該4字節(jié)中的內(nèi)容是在構(gòu)造函數(shù)中填充的。
- 如果類沒有顯示定義構(gòu)造函數(shù),編譯器會給該類生成一個默認(rèn)的構(gòu)造函數(shù),作用:給對象的前4個字節(jié)賦值
- 如果類顯示定義了自己的構(gòu)造函數(shù),編譯器將會對構(gòu)造函數(shù)進行修改,多增加一條語句,給對象的前4個字節(jié)賦值
出現(xiàn)繼承
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 {};虛表指針不一樣,派生類和基類用的不是同一張?zhí)摫?br />
基類虛表構(gòu)建過程
將虛函數(shù)按照其在類中的聲明次序依次放到虛表中。
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; };派生類虛表的構(gòu)建過程
調(diào)用原理
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; }; //虛函數(shù)的調(diào)用:通過基類的指針或者引用調(diào)用虛函數(shù) 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個字節(jié)取虛表的地址 01104BC8 mov esi,esp 01104BCA mov ecx,dword ptr [pb] //傳遞this指針 01104BCD mov eax,dword ptr [edx+4] //從虛表中找對應(yīng)的虛函數(shù) 01104BD0 call eax //調(diào)用虛函數(shù) //重新解析d對象的內(nèi)存空間, //將d對象的內(nèi)存空間按照基類對象方式進行解析 //這個過程并沒有創(chuàng)建新的對象,所以還是調(diào)用派生類的函數(shù) Base* pb = (Base*)&d; pb->TestFunc1(); return 0;打印虛函數(shù)表
單繼承
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[]) {// 依次取虛表中的虛函數(shù)指針打印并調(diào)用。調(diào)用就可以看出存的是哪個函數(shù)cout << " 虛表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d個虛函數(shù)地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();//調(diào)用虛函數(shù),f=vTable[i] vTable[i]等于一個虛函數(shù)入口地址}cout << endl; } int main() {Base b;Derive d; // 思路:取出b、d對象的頭4比特,就是虛表的指針,虛函數(shù)表本質(zhì)是一個存虛函數(shù)指針的指針數(shù)組,這個數(shù)組最后面放了一個nullptr // 1.先取b的地址,強轉(zhuǎn)成一個int*的指針 // 2.再解引用取值,就取到了b對象頭4比特的值,這個值就是指向虛表的指針 // 3.再強轉(zhuǎn)成VFPTR*,因為虛表就是一個存VFPTR類型(虛函數(shù)指針類型)的數(shù)組。 // 4.虛表指針傳遞給PrintVTable進行打印虛表 // 5.需要說明的是這個打印虛表的代碼經(jīng)常會崩潰,因為編譯器有時對虛表的處理不干凈,虛表最后面沒有 //放nullptr,導(dǎo)致越界,這是編譯器的問題。我們只需要點目錄欄的 - 生成 - 清理解決方案,再編譯就好了。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個虛函數(shù)地址 :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; }
多繼承派生類虛表的構(gòu)建過程
總結(jié)
問題
什么是多態(tài)?
概念
同一個事物在不同場景下可以表現(xiàn)出的多種形態(tài),例如迪迦奧特曼的三種形態(tài)。
多態(tài)的分類
| 在編譯時,可確定具體哪個函數(shù) | 在編譯階段無法確定函數(shù)具體調(diào)用哪個函數(shù),必須在代碼運行時才能確定,無法確定基類指針或者引用到底指向哪個類的對象 |
| 函數(shù)重載/模板 | 虛函數(shù)+繼承 |
多態(tài)的實現(xiàn)條件
inline函數(shù)可以是虛函數(shù)嗎?
不能,因為inline函數(shù)沒有地址,無法把地址放到虛函數(shù)表中。inline在編譯時期展開,多態(tài)發(fā)生在運行時。
靜態(tài)成員可以是虛函數(shù)嗎?
不能,因為靜態(tài)成員函數(shù)沒有this指針,使用類型::成員函數(shù)的調(diào)用方式無法訪問虛函數(shù)表,所以靜態(tài)成員函數(shù)無法放進虛函數(shù)表。
構(gòu)造函數(shù)可以是虛函數(shù)嗎?
不能,因為對象中的虛函數(shù)表指針是在構(gòu)造函數(shù)初始化列表階段才初始化的。
構(gòu)造函數(shù)的作用:
析構(gòu)函數(shù)可以是虛函數(shù)嗎?什么場景下析構(gòu)函數(shù)是虛函數(shù)?
可以,并且最好把基類的析構(gòu)函數(shù)定義成虛函數(shù)。析構(gòu)函數(shù)可以為虛函數(shù)---->重寫的一種特例,因為派生類重寫基類中的虛析構(gòu)函數(shù),名字不一樣。
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() {//靜態(tài)類型:聲明變量時的類型----在編譯期間起作用//動態(tài)類型:實際引用(指向)的類型----在運行時確定調(diào)用哪個類的虛函數(shù)Base* pb = new Derived(10);delete pb;//看析構(gòu)函數(shù)是不是虛函數(shù),如果不是用靜態(tài)類型,//delete:1.調(diào)用析構(gòu)函數(shù)釋放對象中的資源// 2.調(diào)用operator delete()釋放對象的空間system("pause");return 0; }如果派生類中涉及到動態(tài)資源的管理(比如:子類從堆上申請空間),建議:基類中的析構(gòu)函數(shù)最好設(shè)置為虛函數(shù),否則可能存在內(nèi)存泄露
對象訪問普通函數(shù)快還是虛函數(shù)更快?
首先如果是普通對象,是一樣快的。如果是指針對象或者是引用對象,則調(diào)用的普通函數(shù)快,因為構(gòu)成多態(tài),運行時調(diào)用虛函數(shù)需要到虛函數(shù)表中去查找。
| 傳參(如果有參數(shù)) | 跟普通函數(shù)調(diào)用一樣 | 從對象的前4個字節(jié)中取虛表地址 |
| 通過指令call調(diào)用該函數(shù)(call 函數(shù)入口地址) | 傳參(包括this指針) | |
| 從虛表中獲取函數(shù)地址 | ||
| 調(diào)用函數(shù) |
多態(tài)的缺陷:降低程序運行的速度
虛函數(shù)表是在什么階段生成的,存在哪的?
虛函數(shù)是在編譯階段就生成的,一般情況下存在代碼段(常量區(qū))的。
總結(jié)
- 上一篇: 性生活出血会导致不孕不育吗
- 下一篇: 皇子归来之欢喜知府剧情介绍