C++多態
時間:2024-05-11 來源:華清遠見
一、什么是多態?
在面向對象編程中,我們通常將多態分為兩種類型:靜態多態(靜態多態,也稱為編譯時多態)和動態多態(動態多態性,也稱為運行時多態)。這兩種多態性是多態概念的不同表現方式。
1、靜態多態
(1)靜態多態是指在編譯時就能確定要調用的方法,通過函數重載和運算符重載來實現。
(2)函數重載是指在一個類中定義多個同名函數,但是參數類型或個數或前后順序不同,編譯器根據調用時傳入的參數類型和數量來確定調用那個函數。
(3)運算符重載是指重新定義運算符的行為,以使其適用于自定義類。編譯器在編譯階段就會確定調用那個運算符的重載版本。
2、動態多態
(1)動態多態是指在運行時根據對象的實際類型來確定要調用的函數,通過繼承和函數覆蓋來實現。
(2)繼承允許派生類繼承基類的函數,并且派生類可以重寫基類的函數以實現自己的行為。
(3)當使用基類的指針或引用指向派生類對象時,通過虛函數機制,程序在運行時會調用對應派生的函數。
靜態多態性發生在編譯時,因為在編譯階段編譯器就可以確定要調用的函數;而動態多態性發生在運行時,因為具體調用那個函數是在程序運行時根據對象的實際類型確定的。
注:本文中后續說的多態均為動態多態。
二、多態的概念
多態可以理解為“一種接口,多種狀態”,只需要編寫一個函數接口,根據傳入的參數類型,執行不同的策略代碼。
多態的使用具有三個前提條件:
(1). 公有繼承
(2). 函數覆蓋
(3). 基類引用/指針指向派生類對象
多態的優點:多態的優勢包括代碼的靈活性、可擴展性和可維護性。它能夠使代碼更具通用性,減少重復代碼的編寫,并且能夠輕松地添加新的派生類或擴展現有的功能。
多態的缺點:多態的缺點包括代碼的復雜性、運行效率、易讀性。當類的繼承關系復雜時,理解和維護多態性相關的代碼會變得困難。動態多態在運行中會產生一些額外的開銷,因為需要在運行時確定對象的實際類型并調用相應的函數。這種開銷通常比靜態多態調用要高。過度使用多態性可能會導致代碼不易理解。
三、多態的實現
3.1 函數覆蓋
函數覆蓋、函數隱藏。這兩個比較相似,但是函數隱藏不支持多態,而函數覆蓋是多態的必備條件。函數覆蓋比函數隱藏有以下幾點區別:
(1). 函數隱藏是派生類中存在與基類中同名同參的函數,編譯器會將基類的同名同參數的函數進行隱藏。注:基類中的函數得是非虛函數的普通函數。
(2). 函數覆蓋是基類中定義了一個虛函數,派生類編寫一個同名同參數的函數將基類中的虛函數進行重寫并覆蓋。注:覆蓋的基類函數必須是虛函數。
3.2 虛函數的定義
一個函數使用virtual關鍵字修飾,就是虛函數,虛函數是函數覆蓋的前提,在Qt Creator中虛函數的函數名稱使用斜體字。
例如: virtual void eat();
class Animal
{
public:
// 虛函數
virtual void eat()
{
cout << "動物吃東西"<< endl;
}
};
虛函數具有以下性質:
(1). 虛函數具有傳遞性,基類中被覆蓋的函數是虛函數,派生類中新覆蓋的函數也是虛函數。例如:
class Animal
{
public:
// 虛函數
virtual void eat()
{
cout << "動物吃東西"<< endl;
}
};
class Dog:public Animal
{
public:
// 覆蓋基類中的虛函數,派生類的vritual可寫可不寫
void eat()
{
cout << "狗吃骨頭"<< endl;
}
};
(2). 只有普通成員函數與析構函數可以聲明為虛函數。
例如:
class Animal
{
public:
// 錯誤 構造函數不能聲明為虛函數
// virtual Animal()
// {
// cout << "測試:構造函數虛函數" << endl;
// }
// 錯誤 靜態函數不能為虛函數
// virtual static void testStatic()
// {
// cout << "測試:靜態成員函數虛函數" << endl;
// }
// 虛函數
virtual void eat()
{
cout << "動物吃東西"<< endl;
}
};
class Dog:public Animal
{
public:
// 覆蓋基類中的虛函數,派生類的vritual可寫可不寫
void eat()
{
cout << "狗吃骨頭"<< endl;
}
};
(3). 在C++11中,可以在派生類的新覆蓋的函數上使用override關鍵字驗證覆蓋是否成功。
例如:
class Animal
{
public:
// 虛函數
virtual void eat()
{
cout << "動物吃東西"<< endl;
}
void funHide()
{
cout << "測試:override關鍵字函數"<< endl;
}
};
class Dog:public Animal
{
public:
// 覆蓋基類中的虛函數,派生類的vritual可寫可不寫
void eat() override
{
cout << "狗吃骨頭"<< endl;
}
// 錯誤 標記覆蓋但是沒覆蓋。
// 注:這是函數隱藏,并不是函數覆蓋因為基類中的同名函數不是虛函數
// void funHide() override
// {
// cout << "測試:override關鍵字函數"<< endl;
// }
};
3.3 多態實現
我們在開篇時提到過,要實現動態多態,需要有三個必要前提條件:
(1). 公有繼承 (上述代碼已經實現)
(2). 函數覆蓋 (上述代碼已經實現)
(3). 基類引用/指針指向派生類對象(還未編寫)
【思考】為什么要基類引用/指針指向派生類對象?
(1). 實現運行時多態:通過將基類的指針指向派生類對象,可以實現運行時多態。當使用基類的指針或引用來指向派生類對象時,程序在運行時會根據對象的實際類型來調用相應的函數,而不是根據指針或引用的類型。
(2)統一接口:基類的指針可以作為一個通用的接口,用于操作不同類型的派生類對象。這樣可以使代碼更靈活,減少重復的代碼,并且支持代碼的擴展和維護。
代碼如下:
#include <iostream>
using namespace std;
class Animal
{
public:
// 虛函數
virtual void eat()
{
cout << "動物吃東西"<< endl;
}
};
class Dog:public Animal
{
public:
void eat()
{
cout << "狗愛吃骨頭"<< endl;
}
};
int main()
{
// 基類指針指向派生類對象
Animal *a1 = new Dog;
// 調用派生類覆蓋的虛函數
a1->eat(); // 狗愛吃骨頭
// 基類的引用指向派生類對象
Dog d1;
Animal &a2 = d1;
// 調用派生類覆蓋的虛函數
a2.eat(); // 狗愛吃骨頭
return 0;
}
我們也可以提供通用接口,參數設計為基類的指針或引用,這樣這個函數就可以訪問到此基類所有派生類中的虛函數了。代碼如下:
#include <iostream>
using namespace std;
class Animal
{
public:
// 虛函數
virtual void eat()
{
cout << "動物吃東西"<< endl;
}
};
class Dog:public Animal
{
public:
void eat()
{
cout << "狗吃骨頭"<< endl;
}
};
class Cat:public Animal
{
public:
void eat()
{
cout << "貓吃魚" << endl;
}
};
// 提供通用函數,形參為基類引用
void animal_eat1(Animal &al)
{
al.eat();
}
// 提供通用函數,形參為基類指針
void animal_eat2(Animal *al)
{
al->eat();
}
int main()
{
// 傳遞引用
Animal a1;
Dog d1;
Cat c1;
animal_eat1(a1); //動物吃東西
animal_eat1(d1); //狗吃骨頭
animal_eat1(c1); //貓吃魚
// 傳遞指針
Animal *a2 = new Animal;
Dog *d2 = new Dog;
Cat *c2 = new Cat;
animal_eat2(a2); //動物吃東西
animal_eat2(d2); //狗吃骨頭
animal_eat2(c2); //貓吃魚
return 0;
}
四、多態的原理
具有虛函數的類會存在一張虛函數表,這張表被當前類所有對象共用,每個類的對象內部會有一個隱藏的虛函數表指針成員,指向當前類的虛函數表。

多態實現流程:

在代碼運行時,通過對象的虛函數表指針找到虛函數表,在表中定位到虛函數的調用地址,從而執行對應的虛函數內容。

