C++ 超全面講解多態(tài)
多態(tài)的概念
概念:通俗的來(lái)說(shuō)就是多種形態(tài),具體就是去完成某個(gè)行為,當(dāng)不同類(lèi)型的對(duì)象去完成同一件事時(shí),產(chǎn)生的動(dòng)作是不一樣的,結(jié)果也是不一樣的。
舉一個(gè)現(xiàn)實(shí)中的例子:買(mǎi)票這個(gè)行為,當(dāng)普通人買(mǎi)票時(shí)是全價(jià);學(xué)生是半價(jià);軍人是不需要排隊(duì)。
多態(tài)也分為兩種:
- 靜態(tài)的多態(tài):函數(shù)調(diào)用
- 動(dòng)態(tài)的多態(tài):父類(lèi)指針或引用調(diào)用重寫(xiě)虛函數(shù)。
這里的靜態(tài)是指在編譯時(shí)實(shí)現(xiàn)多態(tài)的,而動(dòng)態(tài)是在運(yùn)行時(shí)完成的。
多態(tài)的定義及實(shí)現(xiàn)
構(gòu)成條件
多態(tài)一定是建立在繼承上的,那么除了繼承還要兩個(gè)條件:
- 必須通過(guò)基類(lèi)(父類(lèi))的指針或引用調(diào)用函數(shù)
- 被調(diào)用的函數(shù)必須是虛函數(shù),且派生類(lèi)(子類(lèi))必須對(duì)積累的虛函數(shù)進(jìn)行重寫(xiě)。
虛函數(shù)
概念:被virtual修飾的類(lèi)成員函數(shù)稱為虛函數(shù)
class Person { public: virtual void BuyTicket() { cout<<"全價(jià)票"<<endl; } };
注意:
- 只有類(lèi)的非靜態(tài)成員函數(shù)可以是虛函數(shù)
- 虛函數(shù)這里virtual和虛繼承中用的是同一個(gè)關(guān)鍵字,但是他們之間沒(méi)有關(guān)系;虛函數(shù)這里是為了實(shí)現(xiàn)多態(tài);虛繼承是為了解決菱形繼承的數(shù)據(jù)冗余和二義性,它們沒(méi)有關(guān)聯(lián)
虛函數(shù)的重寫(xiě)
概念:派生類(lèi)(子類(lèi))中有一個(gè)跟基類(lèi)(父類(lèi))完全相同的虛函數(shù)(即派生類(lèi)虛函數(shù)與基類(lèi)虛函數(shù)的返回值類(lèi)型,函數(shù)名字,參數(shù)列表完全相同),稱子類(lèi)的虛函數(shù)重寫(xiě)了基類(lèi)的虛函數(shù)。
例:
class Person { public: virtual void BuyTicket() { cout<<"全價(jià)票"<<endl; } }; ? class Student :public Person { public: //子類(lèi)的虛函數(shù)重寫(xiě)了父類(lèi)的虛函數(shù) virtual void BuyTicket() { cout<<"半價(jià)票"<<endl; } }; ? class Soldier : public Person { public: //子類(lèi)的虛函數(shù)重寫(xiě)了父類(lèi)的虛函數(shù) virtual void BuyTicket() { cout<<"優(yōu)先買(mǎi)票"<<endl; } }; //多態(tài)的實(shí)現(xiàn) void f(Person& p)//這塊的參數(shù)必須是引用或者指針 { p.BuyTicket(); } ? int main() { Person p; Student st; Soldier so; f(p); f(st); f(so); return 0; }
注意:這里子函數(shù)的虛函數(shù)可以不加virtual,也算完成了重寫(xiě),但是父類(lèi)的虛函數(shù)必須要加,因?yàn)樽宇?lèi)是先繼承父類(lèi)的虛函數(shù),繼承下來(lái)后就有了virtual屬性了,子類(lèi)只是重寫(xiě)這個(gè)virtual函數(shù);除了這個(gè)原因之外,還有一個(gè)原因,如果父類(lèi)的析構(gòu)函數(shù)加了virtual,子類(lèi)加不加都一定完成了重寫(xiě),就保證了delete時(shí)一定能實(shí)現(xiàn)多態(tài)的正確調(diào)用析構(gòu)函數(shù)。
虛函數(shù)重寫(xiě)的兩個(gè)例外
1、協(xié)變
概念:派生類(lèi)重寫(xiě)基類(lèi)虛函數(shù)時(shí),與基類(lèi)虛函數(shù)返回值類(lèi)型不同。即基類(lèi)虛函數(shù)返回基類(lèi)對(duì)象的指針或者引用,派生類(lèi)虛函數(shù)返回派生類(lèi)對(duì)象的指針或者引用時(shí),稱為協(xié)變
例:
class A{}; class B : public A{}; ? class Person { public: virtual A* f() { return new A; } }; ? class Student : public Person { public: virtual B* f() //返回值不同但是構(gòu)成虛函數(shù)重寫(xiě) { return new B; } };
2、析構(gòu)函數(shù)的重寫(xiě)
如果基類(lèi)的析構(gòu)函數(shù)為虛函數(shù),此時(shí)派生類(lèi)析構(gòu)函數(shù)只要定義,無(wú)論是否加virtual關(guān)鍵字,都與基類(lèi)的析構(gòu)函數(shù)構(gòu)成重寫(xiě),雖然基類(lèi)與派生類(lèi)析構(gòu)函數(shù)名字不同。雖然函數(shù)名不相同,看起來(lái)違背了重寫(xiě)的規(guī)則,其實(shí)不然,這里可以理解為編譯器對(duì)析構(gòu)函數(shù)的名稱做了特殊處理,編譯后析構(gòu)函數(shù)的名稱統(tǒng)一處理成destructor
例:
class Person { public: //建議把父類(lèi)析構(gòu)函數(shù)定義為虛函數(shù),這樣方便子類(lèi)的虛函數(shù)重寫(xiě)父類(lèi)的虛函數(shù) virtual ~Person() {cout << "~Person()" << endl;} }; ? class Student : public Person { public: virtual ~Student() { cout << "~Student()" << endl; } }; // 只有派生類(lèi)Student的析構(gòu)函數(shù)重寫(xiě)了Person的析構(gòu)函數(shù),下面的delete對(duì)象調(diào)用析構(gòu)函數(shù),才能構(gòu)成多態(tài),才能保證p1和p2指向的對(duì)象正確的調(diào)用析構(gòu)函數(shù)。 int main() { Person* p1 = new Person; //這里p2指向的子類(lèi)對(duì)象,應(yīng)該調(diào)用子類(lèi)析構(gòu)函數(shù),如果沒(méi)有調(diào)用的話,就可能內(nèi)存泄漏 Person* p2 = new Student; //多態(tài)行為 delete p1; delete p2; //只有析構(gòu)函數(shù)重寫(xiě)了那么這里delete父類(lèi)指針調(diào)用析構(gòu)函數(shù)才能實(shí)現(xiàn)多態(tài)。 return 0; }
C++11 override和finel
從上面可以看出,C++對(duì)函數(shù)重寫(xiě)的要求比較嚴(yán)格,但是有些情況下由于疏忽,可能會(huì)導(dǎo)致函數(shù)名字母次序?qū)懛炊鵁o(wú)法構(gòu)成重載,而這種錯(cuò)誤在編譯期間是不會(huì)報(bào)出的,只有在程序運(yùn)行時(shí)沒(méi)有得到預(yù)期結(jié)果才來(lái)debug會(huì)得不償失,因此:C++11提供了override和final兩個(gè)關(guān)鍵字,可以幫助用戶檢測(cè)是否重寫(xiě)
final:修飾虛函數(shù),表示該虛函數(shù)不能再被重寫(xiě)
class Car { public: virtual void Drive() final {} }; class Benz :public Car { public: //會(huì)在這塊報(bào)錯(cuò),因?yàn)榛?lèi)的虛函數(shù)已經(jīng)被final修飾,不能被重寫(xiě)了 virtual void Drive() {cout << "Benz-舒適" << endl;} };
override: 檢查派生類(lèi)虛函數(shù)是否重寫(xiě)了基類(lèi)某個(gè)虛函數(shù),如果沒(méi)有重寫(xiě)編譯報(bào)錯(cuò)
class Car{ public: virtual void Drive(){} }; class Benz :public Car { public: virtual void Drive() override {cout << "Benz-舒適" << endl;} };
重載、覆蓋(重寫(xiě))、隱藏(重定義)的對(duì)比
抽象類(lèi)
抽象類(lèi)的概念
純虛函數(shù):在虛函數(shù)的后面加上=0就是純虛函數(shù),有純虛函數(shù)的類(lèi)就是抽象類(lèi),也叫接口類(lèi),抽象類(lèi)無(wú)法實(shí)例化對(duì)象。抽象類(lèi)的子類(lèi)不重寫(xiě)父類(lèi)的虛函數(shù)的話,也是一個(gè)抽象類(lèi)。
//抽象類(lèi)的定義 class Car { public: virtual void run()=0; //不用實(shí)現(xiàn)只寫(xiě)接口就行。 };
純虛函數(shù)不寫(xiě)函數(shù)體,并不意味著不能實(shí)現(xiàn),只是我們不寫(xiě)。因?yàn)閷?xiě)出來(lái)也沒(méi)有人用。
虛函數(shù)的作用
- 強(qiáng)制子類(lèi)重寫(xiě)虛函數(shù),完成多態(tài)。
- 表示抽象類(lèi)。
接口繼承和實(shí)現(xiàn)繼承
普通函數(shù)的繼承就是實(shí)現(xiàn)繼承,虛函數(shù)的繼承就是接口繼承。子類(lèi)繼承了函數(shù)的實(shí)現(xiàn),可以直接使用。虛函數(shù)重寫(xiě)后只會(huì)繼承接口,重寫(xiě)實(shí)現(xiàn)。所以如果不用多態(tài),就不要把函數(shù)寫(xiě)為虛函數(shù)。
純虛函數(shù)就體現(xiàn)了接口函數(shù)。下面我們來(lái)實(shí)現(xiàn)一道題,展現(xiàn)一下接口繼承。
class A { public: virtual void fun(int val=0) { cout<<"A->val = "<<val <<endl; } void Fun() { fun(); } }; ? class B:public A { public: virtual void fun(int val=1) { cout<<"B->val"<<val<<endl; } }; ? int main() { B b; A* a=&b; a->Fun(); return 0; }
結(jié)果打印為 :B->val=0
子類(lèi)對(duì)象切片給父類(lèi)指針,傳給Fun函數(shù),滿足多態(tài),會(huì)去調(diào)用子類(lèi)的fun函數(shù),但是子類(lèi)的虛函數(shù)繼承了父類(lèi)的接口,所以val是父類(lèi)的0。
多態(tài)的原理
虛函數(shù)表
class A { public: virtual void fun() { } protected: int _a; };
sizeof(A)是多少?
打印出來(lái)是8。
我們定義了一個(gè)A類(lèi)型的對(duì)象a,打開(kāi)調(diào)試窗口,發(fā)現(xiàn)a的內(nèi)容如下
我們發(fā)現(xiàn)出了成員變量_a以外,還多了一個(gè)指針,這個(gè)指針是不準(zhǔn)確的,實(shí)際上應(yīng)該是 _vftptr(virtual function table pointer),虛函數(shù)表指針。在計(jì)算類(lèi)大小的時(shí)候要加上這個(gè)指針的大小。虛表就是存放虛函數(shù)的地址地方,當(dāng)我們?nèi)フ{(diào)用虛函數(shù),編譯器就會(huì)通過(guò)虛表指針去虛表里查找。
class A { public: void fun1() { } virtual void fun2() {} }; ? int main() { A* a=nullptr; a->fun1();//調(diào)用函數(shù),因?yàn)檫@是普通函數(shù)的調(diào)用 a->fun2();//調(diào)用失敗,虛函數(shù)需要對(duì)指針操作,無(wú)法操作空指針。 return 0; }
實(shí)現(xiàn)一個(gè)繼承
class A { public: virtual void fun1() {} virtual void fun2() {} }; class B : public A { public: virtual void fun1() {} virtual void fun2() {} }; ? int main() { A a; B b; return 0; }
子類(lèi)與父類(lèi)一樣有一個(gè)虛表指針。
子類(lèi)的虛函數(shù)表一部分繼承自父類(lèi)。如果重寫(xiě)了虛函數(shù),那么子類(lèi)的虛函數(shù)會(huì)在虛表上覆蓋父類(lèi)的虛函數(shù)。
本質(zhì)上虛函數(shù)表是一個(gè)虛函數(shù)指針數(shù)組,最后一個(gè)元素是nullptr,代表虛表的結(jié)束。所以,如果繼承了虛函數(shù),那么
- 子類(lèi)先拷貝一份父類(lèi)虛表,然后用一個(gè)虛表指針指向這個(gè)虛表。
- 如果有虛函數(shù)重寫(xiě),那么在子類(lèi)的虛表上用子類(lèi)的虛函數(shù)覆蓋。
- 子類(lèi)新增的虛函數(shù)按其在子類(lèi)中的聲明次序增加到子類(lèi)虛表的最后。
虛函數(shù)表放在內(nèi)存的那個(gè)區(qū),虛函數(shù)又放在哪?
虛函數(shù)與虛函數(shù)表都放在代碼段。
多態(tài)的原理
我們現(xiàn)在來(lái)看多態(tài)的原理
class person { public: virtual void fun() { cout<<"全價(jià)票"<<endl; } }; class student : public person { public: virtual void fun() { cout<<"半價(jià)票"<<endl; } }; void buyticket(person* p) { p->fun(); }
這樣就實(shí)現(xiàn)了不同對(duì)象去調(diào)用同一函數(shù),展現(xiàn)出不同的形態(tài)。 滿足多態(tài)的函數(shù)調(diào)用是程序運(yùn)行是去對(duì)象的虛表查找的,而虛表是在編譯時(shí)確定的。 普通函數(shù)的調(diào)用是編譯時(shí)就確定的。
動(dòng)態(tài)綁定與靜態(tài)綁定
1.靜態(tài)綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態(tài)多態(tài),比如:函數(shù)重載
2.動(dòng)態(tài)綁定又稱后期綁定(晚綁定),是在程序運(yùn)行期間,根據(jù)具體拿到的類(lèi)型確定程序的具體行為,調(diào)用具體的函數(shù),也稱為動(dòng)態(tài)多態(tài)。我們說(shuō)的多態(tài)一般是指動(dòng)態(tài)多態(tài)。
這里我附上一個(gè)有意思的問(wèn)題:
就是在子類(lèi)已經(jīng)覆蓋了父類(lèi)的虛函數(shù)的情況下,為什么子類(lèi)還是可以調(diào)用“被覆蓋”的父類(lèi)的虛函數(shù)呢?
#include <iostream> using namespace std; ? class Base { public: virtual void func() { cout << "Base func\n"; } }; ? class Son : public Base { public: void func() { Base::func(); cout << "Son func\n"; } }; ? int main() { Son b; b.func(); return 0; }
輸出:
Base func
Son func
這是C++提供的一個(gè)回避虛函數(shù)的機(jī)制
通過(guò)加作用域(正如你所嘗試的),使得函數(shù)在編譯時(shí)就綁定。
到此這篇關(guān)于C++ 超全面講解多態(tài)的文章就介紹到這了,更多相關(guān)C++ 多態(tài)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++實(shí)現(xiàn)LeetCode(198.打家劫舍)
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(198.打家劫舍),本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08C++簡(jiǎn)單五子棋的AI設(shè)計(jì)實(shí)現(xiàn)
這篇文章主要為大家詳細(xì)介紹了C++簡(jiǎn)單五子棋的AI設(shè)計(jì)實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-09-09C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單計(jì)算器程序
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單計(jì)算器程序,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-02-02C++實(shí)現(xiàn)團(tuán)購(gòu)訂單管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了如何利用C++實(shí)現(xiàn)團(tuán)購(gòu)訂單管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-12-12vc++ 監(jiān)控指定路徑下文件變化實(shí)現(xiàn)代碼
這篇文章主要介紹了vc++ 監(jiān)控指定路徑下文件變化實(shí)現(xiàn)代碼,需要的朋友可以參考下2019-04-04C++實(shí)現(xiàn)馬踏棋盤(pán)(騎士周游)
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)馬踏棋盤(pán),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02淺談使用Rapidxml 庫(kù)遇到的問(wèn)題和分析過(guò)程(分享)
下面小編就為大家?guī)?lái)一篇淺談使用Rapidxml 庫(kù)遇到的問(wèn)題和分析過(guò)程(分享)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-05-05