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

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

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

