C++多態(tài)定義及實(shí)現(xiàn)深度剖析
前言
在前面我們對(duì)C++的封裝,繼承等特性都有了了解和學(xué)習(xí),接下來(lái)我們將對(duì)C++的第三大特性-多態(tài)進(jìn)行認(rèn)識(shí)和掌握。內(nèi)容分為來(lái)兩大部分,第一個(gè)是對(duì)多態(tài)的認(rèn)識(shí)和運(yùn)用,第二大部分是對(duì)多態(tài)原理的了解和擴(kuò)展。
1.多態(tài)的概念
多態(tài)(Polymorphism)是面向?qū)ο缶幊蹋∣OP)中的一個(gè)核心概念,它指的是同一個(gè)行為具有多個(gè)不同表現(xiàn)形式或形態(tài)的能力。在編程中,多態(tài)通常通過(guò)繼承(inheritance)和接(interfaces來(lái)實(shí)現(xiàn)。
以下是多態(tài)的幾個(gè)主要方面:
編譯時(shí)多態(tài)(靜態(tài)多態(tài)):這是在編譯時(shí)確定的多態(tài)性,通常通過(guò)函數(shù)重載(function overloading)和模板(templates)來(lái)實(shí)現(xiàn)。編譯器根據(jù)函數(shù)的參數(shù)類型或數(shù)量來(lái)決定調(diào)用哪個(gè)函數(shù)。
運(yùn)行時(shí)多態(tài)(動(dòng)態(tài)多態(tài)):這是在程序運(yùn)行時(shí)確定的多態(tài)性,主要通過(guò)虛函數(shù)(virtual functions)和繼承來(lái)實(shí)現(xiàn)。在運(yùn)行時(shí),根據(jù)對(duì)象的實(shí)際類型來(lái)調(diào)用相應(yīng)的成員函數(shù)。
之所以叫編譯時(shí)多態(tài),是 因?yàn)樗麄儗?shí)參傳給形參的參數(shù)匹配是在編譯時(shí)完成的,我們把編譯時(shí)一般歸為靜態(tài),運(yùn)行時(shí)歸為動(dòng)態(tài)。
運(yùn)行時(shí)多態(tài),具體點(diǎn)就是去完成某個(gè)行為(函數(shù)),可以傳不同的對(duì)象就會(huì)完成不同的行為,就達(dá)到多種 形態(tài)。比如買票這個(gè)行為,當(dāng)普通人買票時(shí),是全價(jià)買票;學(xué)生買票時(shí),是優(yōu)惠買票(5折或75折);軍人買票時(shí)是優(yōu)先買票。再比如,同樣是動(dòng)物叫的一個(gè)行為(函數(shù)),傳貓對(duì)象過(guò)去,就是”(>^ω^<)喵“,傳狗對(duì)象過(guò)去,就是"汪汪"。
多態(tài)的關(guān)鍵特性包括:
- 繼承:子類繼承父類的屬性和行為,可以對(duì)這些行為進(jìn)行重寫(override)。
- 虛函數(shù):在基類中聲明為虛的成員函數(shù),可以在派生類中被重寫,使得通過(guò)基類指針或引用調(diào)用函數(shù)時(shí),能夠根據(jù)對(duì)象的實(shí)際類型來(lái)調(diào)用相應(yīng)的函數(shù)版本。
- 虛函數(shù)表:用于實(shí)現(xiàn)運(yùn)行時(shí)多態(tài)的數(shù)據(jù)結(jié)構(gòu),它存儲(chǔ)了虛函數(shù)的地址,使得程序能夠在運(yùn)行時(shí)確定調(diào)用哪個(gè)函數(shù)。
- 向上轉(zhuǎn)型:將派生類對(duì)象的引用或指針轉(zhuǎn)換為基類類型的引用或指針,這是多態(tài)實(shí)現(xiàn)的基礎(chǔ)。
2.多態(tài)的定義及實(shí)現(xiàn)
2.1多態(tài)的構(gòu)成條件
多態(tài)是一個(gè)繼承關(guān)系的下的類對(duì)象,去調(diào)用同一函數(shù),產(chǎn)生了不同的行為。比如Student繼承了
Person。Person對(duì)象買票全價(jià),Student對(duì)象優(yōu)惠買票。
2.1.1重要條件
被調(diào)用的函數(shù)必須是虛函數(shù)
指針或者引用調(diào)用虛函數(shù)
說(shuō)明:要實(shí)現(xiàn)多態(tài)效果,第一必須是基類的指針或引用,因?yàn)橹挥谢惖闹羔樆蛞貌拍芗戎赶蚺缮?nbsp;類對(duì)象;第二派生類必須對(duì)基類的虛函數(shù)重寫/覆蓋,重寫或者覆蓋了,派生類才能有不同的函數(shù),多 態(tài)的不同形態(tài)效果才能達(dá)到。

2.1.2 虛函數(shù)
類成員函數(shù)前面加virtual修飾,那么這個(gè)成員函數(shù)被稱為虛函數(shù)。注意?成員函數(shù)不能加virtual修
飾。
class Person {
public:
virtual void BuyTicket() {
cout << "買票全額" << endl;
}
};2.1.3 虛函數(shù)的重寫/覆蓋
虛函數(shù)的重寫/覆蓋: 派生類中有一個(gè)跟基類完全相同的虛函數(shù)(即派生類虛函數(shù)與基類虛函數(shù)的返回值類型、函數(shù)名字、參數(shù)列表(類型,數(shù)量)完全相同),稱派生類的虛函數(shù)重寫了基類的虛函數(shù)。
注意:在重寫基類虛函數(shù)時(shí),派生類的虛函數(shù)在不加virtual關(guān)鍵字時(shí),雖然也可以構(gòu)成重寫(因?yàn)槔^承 后基類的虛函數(shù)被繼承下來(lái)了在派生類依舊保持虛函數(shù)屬性),但是該種寫法不是很規(guī)范,不建議這樣 使用,不過(guò)在考試選擇題中,經(jīng)常會(huì)故意買這個(gè)坑,讓判斷是否構(gòu)成多態(tài)。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() {
cout << "買票全額" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket() {
cout << "學(xué)生票半價(jià)" << endl;
}
};
//引用調(diào)用
void func(Person& p) {
p.BuyTicket();
}
//指針調(diào)用
void func1(Person* p) {
p->BuyTicket();
// 這?可以看到雖然都是Person指針Ptr在調(diào)?BuyTicket
// 但是跟ptr沒(méi)關(guān)系,?是由ptr指向的對(duì)象決定的。
}
int main() {
Person p1;
Student s1;
Person* p2 = new Person();
Student* s2 = new Student();
func(p1);
func(s1);
p1.BuyTicket();
s1.BuyTicket();
func1(&p1);
func1(&s1);
p2->BuyTicket();
s2->BuyTicket();
return 0;
}
void func(Student& p) {
p.BuyTicket();
}
//指針調(diào)用
void func1(Student* p) {
p->BuyTicket();
}如果改成Student,就會(huì)出問(wèn)題,就不是多態(tài)了,也就不能傳Person對(duì)象了。
#include <iostream>
using namespace std;
class Pet {
public:
virtual void eat() const{
cout << "Eat food" << endl;
}
};
class Dog : public Pet{
public:
virtual void eat() const {
cout << "Dog eats meat!" << endl;
}
};
class Cat :public Pet {
public:
virtual void eat()const {
cout << "Cat eats fish!" << endl;
}
};
void func(const Pet& p) {
p.eat();
}
int main() {
Pet p;
Dog g;
Cat c;
func(p);
func(g);
func(c);
return 0;
}上述是寵物的一個(gè)多態(tài)實(shí)現(xiàn)。
這里我們測(cè)試一下,基類函數(shù)不加virtual會(huì)怎樣,
class Pet {
public:
void eat() const{
cout << "Eat food" << endl;
}
};
我們會(huì)發(fā)現(xiàn)多態(tài)效果沒(méi)有實(shí)現(xiàn),所以一定要加上virtual.
2.1.4 選擇題
下面程序輸出結(jié)果是什么?(B)
A: A->0 B: B->1 C: A->1 D: B->0 E: 編譯出錯(cuò) F: 以上都不正確
class A {
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A {
public:
void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[]) {
B*p = new B;
p->test();
return 0; }B* p = new B;創(chuàng)建了一個(gè)B類型的對(duì)象,并通過(guò)基類指針p指向它。p->test();調(diào)用了A類的test方法(因?yàn)?nbsp;B類沒(méi)有重寫test方法)。- 在
A類的test方法中,func(val)被調(diào)用,沒(méi)有指定val的值,因此它使用A類func方法的默認(rèn)參數(shù)1。 - 由于
func是虛函數(shù),并且p指向一個(gè)B類型的對(duì)象,所以B類的func方法被調(diào)用,接收到的參數(shù)是1。
2.1.5 虛函數(shù)其他知識(shí)
協(xié)變(了解)
派生類重寫基類虛函數(shù)時(shí),與基類虛函數(shù)返回值類型不同。即基類虛函數(shù)返回基類對(duì)象的指針或者引用,派生類虛函數(shù)返回派生類對(duì)象的指針或者引用時(shí),稱為協(xié)變。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class A {};
class B : public A {};
class Person {
public:
virtual A* BuyTicket()
{
cout << "買票-全價(jià)" << endl;
return nullptr;
}
};
class Student : public Person {
public:
virtual B* BuyTicket()
{
cout << "買票-打折" << endl;
return nullptr;
}
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}析構(gòu)函數(shù)的重寫
基類的析構(gòu)函數(shù)為虛函數(shù),此時(shí)派生類析構(gòu)函數(shù)只要定義,無(wú)論是否加virtual關(guān)鍵字,都與基類的析構(gòu)函數(shù)構(gòu)成重寫,雖然基類與派生類析構(gòu)函數(shù)名字不同看起來(lái)不符合重寫的規(guī)則,實(shí)際上編譯器對(duì)析構(gòu)函數(shù)的名稱做了特殊處理,編譯后析構(gòu)函數(shù)的名稱統(tǒng)一處理成destructor, 所以基類的析構(gòu)函數(shù)加了vialtual修飾,派生類的析構(gòu)函數(shù)就構(gòu)成重寫。
故在C++中,當(dāng)一個(gè)基類的析構(gòu)函數(shù)被聲明為虛函數(shù)時(shí),它確保了當(dāng)通過(guò)基類指針或引用刪除派生類對(duì)象時(shí),會(huì)調(diào)用正確的析構(gòu)函數(shù),即派生類的析構(gòu)函數(shù),然后再調(diào)用基類的析構(gòu)函數(shù)。這是因?yàn)樘撐鰳?gòu)函數(shù)允許動(dòng)態(tài)綁定,確保了派生類對(duì)象被正確地銷毀。
#include <iostream>
using namespace std;
class A {
public:
virtual ~A() {
cout << "delete A" << endl;
}
};
class B :public A {
public:
~B() {
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
int main() {
A* a = new A;
A* b = new B;
delete a;
delete b;
return 0;
}
當(dāng)我們不把基類析構(gòu)函數(shù)設(shè)置成virtual時(shí), 會(huì)發(fā)現(xiàn)沒(méi)有調(diào)用B的析構(gòu),該釋放的資源沒(méi)有釋放掉。
public:
~A() {
cout << "delete A" << endl;
}
}; 
故基類的析構(gòu)函數(shù)我們要設(shè)置成虛函數(shù)。
override 和 final關(guān)鍵字
從上面可以看出,C++對(duì)函數(shù)重寫的要求比較嚴(yán)格,但是有些情況下由于疏忽,比如函數(shù)名寫錯(cuò)參數(shù)寫錯(cuò)等導(dǎo)致無(wú)法構(gòu)成重載,而這種錯(cuò)誤在編譯期間是不會(huì)報(bào)出的,只有在程序運(yùn)行時(shí)沒(méi)有得到預(yù)期結(jié)果才來(lái)debug會(huì)得不償失。
如果不想讓派生類重寫這個(gè)虛函數(shù),那么可以用final去修飾。
在C++中,override 和 final 關(guān)鍵字是C++11標(biāo)準(zhǔn)引入的,用于增強(qiáng)類繼承和虛函數(shù)的聲明。
override 關(guān)鍵字用于明確指出一個(gè)成員函數(shù)旨在重寫(覆蓋)其基類中的一個(gè)虛函數(shù)。如果該函數(shù)沒(méi)有正確地重寫基類中的任何虛函數(shù),編譯器將報(bào)錯(cuò)。這有助于避免因拼寫錯(cuò)誤或參數(shù)列表不匹配而意外地沒(méi)有重寫虛函數(shù)的情況。
class Car {
public:
virtual void Dirve()
{}
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-舒適" << endl; }
};比如上面這個(gè)例子,函數(shù)名寫錯(cuò)了,重寫失敗,編譯報(bào)錯(cuò)。

final 關(guān)鍵字用于防止類被進(jìn)一步派生,或者防止虛函數(shù)被重寫。當(dāng)應(yīng)用于類時(shí),它表示這個(gè)類不能被繼承。當(dāng)應(yīng)用于虛函數(shù)時(shí),它表示這個(gè)虛函數(shù)不能在派生類中被重寫。
class Car {
public:
virtual void Dirve() final
{}
};
class Benz :public Car {
public:
virtual void Dirve(){ cout << "Benz-舒適" << endl; }
};
class Base final { // 不能從這個(gè)類派生其他類
public:
virtual void doSomething() const final {} // 這個(gè)虛函數(shù)不能被重寫
};
// 下面的類聲明會(huì)導(dǎo)致編譯錯(cuò)誤,因?yàn)?Base 是 final 的
// class Derived : public Base {};
// 下面的函數(shù)聲明也會(huì)導(dǎo)致編譯錯(cuò)誤,因?yàn)?doSomething 是 final 的
// class Derived : public Base {
// public:
// void doSomething() const override {} // 錯(cuò)誤:不能重寫 final 函數(shù)
// };使用 final 關(guān)鍵字可以確保類或虛函數(shù)的行為不會(huì)被意外的繼承或重寫改變,這對(duì)于設(shè)計(jì)那些不打算被擴(kuò)展的類或函數(shù)非常有用。
3. 重載,重寫,隱藏的對(duì)比
重載(Overloading)
- 定義:在同一作用域內(nèi),可以定義多個(gè)同名函數(shù),只要它們的參數(shù)列表(參數(shù)的數(shù)量、類型或順序)不同。
- 特點(diǎn):
- 發(fā)生在同一類中。
- 參數(shù)列表必須不同。
- 返回類型可以不同,但不是區(qū)分重載的主要因素。
重寫(Overriding)
- 定義:在派生類中提供一個(gè)與基類中虛函數(shù)同名、參數(shù)列表和返回類型相同的函數(shù),以實(shí)現(xiàn)多態(tài)。
- 特點(diǎn):
- 發(fā)生在基類和派生類之間。
- 參數(shù)列表和返回類型必須相同。
- 基類函數(shù)必須是虛函數(shù)。
- 使用
override關(guān)鍵字可以明確指出重寫意圖。
隱藏(Hiding)
- 定義:在派生類中定義一個(gè)與基類中成員(非虛函數(shù)或非靜態(tài)成員變量)同名的成員,導(dǎo)致基類中的同名成員在派生類中不可見(jiàn)。
- 特點(diǎn):
- 發(fā)生在基類和派生類之間。
- 可以是函數(shù)或變量。
- 如果是函數(shù),參數(shù)列表不必相同。
- 如果派生類中的成員與基類中的成員具有相同的名稱,但不同的參數(shù)列表,則基類成員被隱藏,而不是重載或重寫。
4.純虛函數(shù)和抽象類
在虛函數(shù)的后面寫上 =0 ,則這個(gè)函數(shù)為純虛函數(shù),純虛函數(shù)不需要定義實(shí)現(xiàn)(實(shí)現(xiàn)沒(méi)啥意義因?yàn)橐慌缮愔貙?,但是語(yǔ)法上可以實(shí)現(xiàn)),只要聲明即可。
包含純虛函數(shù)的類叫做抽象類,抽象類不能實(shí)例化出對(duì)象,如果派生類繼承后不重寫純虛函數(shù),那么派生類也是抽象類。純虛函數(shù)某種程度上強(qiáng)制了派生類重寫虛函數(shù),因?yàn)椴恢貙憣?shí)例化不出對(duì)象。
#include <iostream>
using namespace std;
class Car {
public:
virtual void Drive() = 0;
};
class Benchi :public Car {
public:
virtual void Drive() {
cout << "Benchi-舒適" << endl;
}
};
class Baoma :public Car {
public:
virtual void Drive() {
cout << "Baoma-上手" << endl;
}
};
int main() {
Car car;
Car* b = new Benchi();
b->Drive();
Car* m = new Baoma();
m->Drive();
return 0;
}
這里Car是抽象類,所以無(wú)法實(shí)例化對(duì)象。
結(jié)束語(yǔ)
到此這篇關(guān)于C++多態(tài)定義及實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)C++多態(tài)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

