C++的多態(Polymorphism)是面向對象編程(OOP)的三大核心特性之一(另外兩個是封裝和繼承),其核心思想是一個接口,多種實現,即同一操作作用于不同對象時,可產生不同的執行結果。多態讓代碼更靈活、可擴展,是構建復雜系統的重要工具。
一、多態的分類
C++的多態分為兩類:靜態多態(編譯時多態)和動態多態(運行時多態),二者的核心區別在于“確定調用哪個函數的時機”——前者在編譯期確定,后者在運行期確定。
1. 靜態多態(編譯時多態)
靜態多態是通過函數重載或運算符重載實現的,編譯器在編譯階段根據函數的參數列表(類型、數量、順序)或運算符的操作數類型,確定具體要調用的函數。
示例:函數重載實現靜態多態
#include <iostream>
using namespace std;// 重載:同一作用域內,函數名相同,參數列表不同
int add(int a, int b) {return a + b;
}double add(double a, double b) { // 參數類型不同return a + b;
}int add(int a, int b, int c) { // 參數數量不同return a + b + c;
}int main() {cout << add(1, 2) << endl; // 調用int add(int, int)cout << add(1.5, 2.5) << endl; // 調用double add(double, double)cout << add(1, 2, 3) << endl; // 調用int add(int, int, int)return 0;
}
編譯器在編譯時會根據實參的類型和數量,自動匹配到對應的重載函數,這就是靜態多態的體現。
2. 動態多態(運行時多態)
動態多態是C++多態的核心,它通過繼承+虛函數實現,函數的具體調用在程序運行時才確定,而非編譯時。這種機制讓基類的指針/引用可以靈活指向不同派生類對象,并調用對應派生類的實現。
核心條件:
- 必須存在繼承關系(基類與派生類);
- 基類中聲明虛函數(用
virtual
關鍵字修飾); - 派生類重寫(override)基類的虛函數(函數名、參數列表、返回值必須完全一致,協變返回類型除外);
- 通過基類的指針或引用調用虛函數。
二、動態多態的實現原理
動態多態的核心是虛函數表(vtable) 和虛指針(vptr),這是編譯器在背后自動實現的機制。
1. 虛函數表(vtable)
- 當一個類中聲明了虛函數(或繼承了虛函數),編譯器會為該類生成一個虛函數表(本質是一個函數指針數組),存儲該類所有虛函數的地址。
- 若派生類重寫了基類的虛函數,派生類的虛函數表中會用自己的函數地址覆蓋基類對應虛函數的地址;未重寫的虛函數,地址仍指向基類的實現。
2. 虛指針(vptr)
- 每個含有虛函數的類的對象,都會隱含一個虛指針(vptr),指向該類的虛函數表(vtable)。
- 當通過基類指針/引用調用虛函數時,程序會通過對象的vptr找到對應的vtable,再從vtable中取出函數地址并調用,這個過程在運行時完成(動態綁定)。
示例:動態多態的直觀體現
#include <iostream>
using namespace std;// 基類:形狀
class Shape {
public:// 虛函數:繪制virtual void draw() { // 用virtual聲明為虛函數cout << "繪制基礎形狀" << endl;}// 虛析構函數(避免內存泄漏)virtual ~Shape() {}
};// 派生類:圓形(繼承Shape)
class Circle : public Shape {
public:// 重寫基類的draw()void draw() override { // override關鍵字顯式聲明重寫(C++11)cout << "繪制圓形" << endl;}
};// 派生類:矩形(繼承Shape)
class Rectangle : public Shape {
public:// 重寫基類的draw()void draw() override {cout << "繪制矩形" << endl;}
};// 統一接口:接收基類引用,調用draw()
void render(Shape& shape) {shape.draw(); // 運行時根據實際對象類型,調用對應draw()
}int main() {Circle circle;Rectangle rectangle;render(circle); // 輸出:繪制圓形(實際是Circle對象)render(rectangle); // 輸出:繪制矩形(實際是Rectangle對象)// 基類指針指向派生類對象Shape* shape1 = new Circle();Shape* shape2 = new Rectangle();shape1->draw(); // 輸出:繪制圓形shape2->draw(); // 輸出:繪制矩形delete shape1; // 虛析構函數確保派生類析構被調用delete shape2;return 0;
}
運行機制解析:
Shape
類有虛函數draw()
,編譯器為其生成vtable,存儲Shape::draw()
的地址。Circle
和Rectangle
繼承Shape
并重寫draw()
,它們的vtable中,draw()
的地址被替換為各自的實現(Circle::draw()
和Rectangle::draw()
)。- 當
render
函數接收Circle
或Rectangle
對象的引用時(本質是基類引用指向派生類對象),調用draw()
時會通過對象的vptr找到對應vtable,最終執行派生類的實現——這就是運行時多態。
三、重寫(Override)的細節
派生類重寫基類虛函數時,必須滿足以下條件(否則可能變成“隱藏”而非“重寫”):
- 函數名、參數列表完全相同:參數的類型、數量、順序必須一致(若參數不同,會變成派生類的新函數,隱藏基類函數)。
- 返回值類型相同:除非是“協變返回類型”(即基類虛函數返回基類指針/引用,派生類重寫函數返回派生類指針/引用)。
class Base {}; class Derived : public Base {};class A { public:virtual Base* func() { return new Base(); } // 基類返回Base* };class B : public A { public:Derived* func() override { return new Derived(); } // 派生類返回Derived*(協變) };
- 基類函數必須是虛函數:若基類函數未用
virtual
修飾,派生類即使同名同參,也只是“隱藏”基類函數,而非重寫(無法觸發多態)。 - 訪問權限不影響多態:即使派生類重寫的函數是
private
,通過基類指針/引用調用時仍能正常觸發(因為訪問權限檢查在編譯期,多態調用在運行期)。
四、純虛函數與抽象類
為了強制派生類必須實現某些功能(如“所有形狀都必須能繪制”),C++引入純虛函數和抽象類:
- 純虛函數:在虛函數聲明后加
=0
,表示該函數沒有默認實現,必須由派生類重寫。 - 抽象類:包含純虛函數的類(或繼承純虛函數且未重寫的類),不能實例化對象,只能作為基類被繼承。
示例:抽象類與純虛函數
class Shape {
public:// 純虛函數:強制派生類實現draw()virtual void draw() = 0; // =0表示純虛函數virtual ~Shape() {} // 抽象類也需要虛析構
};class Circle : public Shape {
public:void draw() override { // 必須重寫,否則Circle也是抽象類cout << "繪制圓形" << endl;}
};int main() {// Shape s; // 錯誤:抽象類不能實例化Shape* shape = new Circle(); // 正確:基類指針指向派生類對象shape->draw(); // 輸出:繪制圓形delete shape;return 0;
}
抽象類的核心作用是定義“接口規范”,確保派生類遵循統一的行為契約(如Shape
規定“必須能繪制”,所有派生類都必須實現draw()
)。
五、多態的應用與優勢
- 提高代碼復用性:通過基類接口統一處理不同派生類對象(如
render
函數無需為每個形狀單獨實現)。 - 增強擴展性:新增派生類(如
Triangle
)時,無需修改現有接口代碼(如render
),只需實現draw()
即可,符合“開閉原則”(對擴展開放,對修改關閉)。 - 模擬現實世界的多樣性:現實中同一行為(如“繪制”)作用于不同對象(圓、矩形)會有不同結果,多態完美映射這種關系。
六、注意事項
-
析構函數建議聲明為虛函數:當通過基類指針刪除派生類對象時,若基類析構不是虛函數,會只調用基類析構而不調用派生類析構,導致內存泄漏。
class Base { public:~Base() { cout << "Base析構" << endl; } // 非虛析構(危險) };class Derived : public Base { public:~Derived() { cout << "Derived析構" << endl; } };int main() {Base* p = new Derived();delete p; // 僅輸出"Base析構",Derived析構未調用(內存泄漏)return 0; }
解決:將基類析構聲明為
virtual ~Base() {}
,確保派生類析構被調用。 -
避免在構造/析構函數中調用虛函數:構造派生類對象時,先調用基類構造函數,此時對象的動態類型仍為基類,調用虛函數會執行基類版本;析構時同理,可能導致不符合預期的結果。
-
虛函數表的開銷:每個含虛函數的類會增加vtable(靜態開銷),每個對象會增加vptr(動態內存開銷,通常為4/8字節),但相比多態帶來的靈活性,這種開銷通常可接受。
C++的多態通過“靜態多態(重載)”和“動態多態(虛函數)”實現,其中動態多態是核心,依賴虛函數表和虛指針實現運行時綁定。它讓代碼更靈活、可擴展,是構建大型面向對象系統的基礎。