@TOC


多態是C++面向對象三大特性之一,核心價值在於為不同數據類型的對象提供統一接口,讓不同對象執行同一行為時呈現差異化效果,極大提升了代碼的靈活性和可擴展性。

一、多態的核心概念

簡單來説,多態就是“同一行為,不同表現”。生活中最典型的例子就是買票場景:普通人購票需支付全價,學生可享受半價優惠,軍人則擁有優先購票的特權。在編程中,這意味着通過統一的接口調用,不同派生類對象能根據自身特性執行對應的實現邏輯。

二、多態的定義與實現

2.1 虛函數:多態的基礎

類中被virtual關鍵字修飾的成員函數稱為虛函數,它是實現多態的核心載體。

class Person {
public:
    // 聲明虛函數,定義基礎行為
    virtual void BuyTicket() {
        cout << "普通人買票:全價" << endl;
    }
};

2.2 重寫:多態的實現核心

重寫(又稱覆蓋)指派生類中存在一個與基類虛函數函數名、參數列表、返回值完全一致的函數。即便派生類函數未顯式添加virtual關鍵字,只要基類對應函數是虛函數,仍構成重寫(建議顯式添加以保證代碼規範)。

class Student : public Person {
public:
    // 重寫基類虛函數,實現學生購票邏輯
    virtual void BuyTicket() {
        cout << "學生買票:半價" << endl;
    }
};

class Soldier : public Person {
public:
    // 重寫基類虛函數,實現軍人購票邏輯
    virtual void BuyTicket() {
        cout << "軍人買票:優先購票" << endl;
    }
};

2.3 多態的實現條件

要觸發多態行為,需同時滿足以下兩個條件:

  1. 調用方式:必須通過基類的指針或引用調用虛函數。
  2. 函數要求:被調用的函數是虛函數,且派生類已完成重寫。
#include <iostream>
#include <string>
#include <ctime>   
#include <windows.h> 


using namespace std;

class Person {
public:
    // 聲明虛函數,定義基礎行為
    virtual void BuyTicket() {
        cout << "普通人買票:全價" << endl;
    }
};

class Student : public Person {
public:
    // 重寫基類虛函數,實現學生購票邏輯
    virtual void BuyTicket() {
        cout << "學生買票:半價" << endl;
    }
};

class Soldier : public Person {
public:
    // 重寫基類虛函數,實現軍人購票邏輯
    virtual void BuyTicket() {
        cout << "軍人買票:優先購票" << endl;
    }
};

// 接收基類引用,觸發多態
void TicketPurchase(Person& p) {
    p.BuyTicket(); // 調用派生類重寫後的函數
}

int main() {
    Person ordinary;
    Student student;
    Soldier soldier;

    TicketPurchase(ordinary);  // 輸出:普通人買票:全價
    TicketPurchase(student);   // 輸出:學生買票:半價
    TicketPurchase(soldier);   // 輸出:軍人買票:優先購票
    return 0;
}

C++面向對象---多態_虛表

若直接通過對象實例調用(非指針/引用),則不會觸發多態,只會執行基類的函數實現。

2.4 虛函數重寫的特殊情況

2.4.1 協變

派生類重寫虛函數時,返回值可與基類虛函數不同,只要滿足“基類函數返回基類指針/引用,派生類函數返回派生類指針/引用”,仍構成重寫。

class Person {
public:
    // 基類虛函數返回基類引用
    virtual Person& GetIdentity() {
        return *this;
    }
};

class Student : public Person {
public:
    // 派生類虛函數返回派生類引用,構成協變
    virtual Student& GetIdentity() {
        return *this;
    }
};
2.4.2 析構函數的重寫

基類析構函數若為虛函數,派生類析構函數無論是否添加virtual、函數名是否一致(編譯器會統一處理為destructor),均構成重寫。這一特性可確保通過基類指針刪除派生類對象時,能正確調用派生類析構函數,避免內存泄漏。

class Person {
public:
    // 基類虛析構函數
    virtual ~Person() {
        cout << "Person 析構" << endl;
    }
};

class Student : public Person {
public:
    // 派生類析構函數,自動與基類構成重寫
    ~Student() {
        cout << "Student 析構" << endl;
    }
};

int main() {
    Person* p1 = new Person;
    Person* p2 = new Student;
    
    delete p1; // 輸出:Person 析構
    delete p2; // 輸出:Student 析構 → Person 析構
    return 0;
}

三、抽象類:接口規範

在虛函數後添加=0,該函數即為純虛函數。包含純虛函數的類稱為抽象類(也叫接口類),其核心作用是規範派生類的行為。

  • 抽象類無法直接實例化對象。
  • 派生類必須重寫所有純虛函數,才能實例化對象;未完全重寫則仍為抽象類。
// 抽象類:定義"交通工具"接口
class Vehicle {
public:
    // 純虛函數,規範派生類必須實現"行駛"功能
    virtual void Run() = 0;
};

// 派生類:重寫純虛函數,實現具體邏輯
class Car : public Vehicle {
public:
    virtual void Run() {
        cout << "汽車:四輪行駛,速度較快" << endl;
    }
};

class Bicycle : public Vehicle {
public:
    virtual void Run() {
        cout << "自行車:兩輪騎行,綠色環保" << endl;
    }
};

int main() {
    // Vehicle v; // 錯誤:抽象類無法實例化
    Vehicle* car = new Car;
    Vehicle* bike = new Bicycle;
    
    car->Run();  // 輸出:汽車:四輪行駛,速度較快
    bike->Run(); // 輸出:自行車:兩輪騎行,綠色環保
    return 0;
}

C++面向對象---多態_派生類_02

補充説明:普通函數的繼承屬於實現繼承,派生類通過繼承獲得基類函數的實現並可直接使用。而虛函數的繼承則是接口繼承,派生類主要繼承基類虛函數的接口規範,其目的是通過重寫實現多態特性。需要注意的是,若不需要實現多態功能,則不應將函數聲明為虛函數。

#include <iostream>
#include <string>
#include <ctime>   
#include <windows.h> 


using namespace std;

class A
{
public:
    virtual void func(int val = 1) { cout << "A->" << val << endl; }
    virtual void test() { func(); }
};

class B : public A
{
public:
    void func(int val = 0) { cout << "B->" << val << endl; }
};

int main()
{
    B* p = new B;
    p->test();
    return 0;
}

C++面向對象---多態_虛表_03

四、多態的底層原理:虛函數表

4.1 虛函數表的本質

含有虛函數的類,實例化對象時會自動生成一個虛函數表指針(_vfptr),通常位於對象內存佈局的起始位置。該指針指向一個存儲虛函數地址的數組,這個數組就是虛函數表(簡稱虛表)。

  • 虛表在編譯階段生成,存儲於程序的常量區。
  • 每個類(基類、派生類)都有獨立的虛表。
  • 派生類虛表會繼承基類虛表的內容,並重寫被覆蓋的虛函數地址,新增的虛函數地址則追加到虛表末尾。
#include <string>
#include <ctime>   
#include <windows.h> 


using namespace std;

// 抽象類:定義"交通工具"接口
class Vehicle {
public:
    // 純虛函數,規範派生類必須實現"行駛"功能
    virtual void Run() { cout << "Run 運行" << endl; }

    int  _vehicle;
};



int main() {
    Vehicle v;


    return 0;
}

C++面向對象---多態_虛函數_04

默認位於類起始位置的_vfptr稱為虛函數表指針(簡稱虛表指針)。類中定義的虛函數地址均存儲於此。由於_vgptr存儲的是指針數據,其本質是一個指針數組。

4.2 虛表的驗證與打印

#include <iostream>
#include <string>
#include <ctime>   
#include <windows.h> 


using namespace std;

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Person::買票-全價" << endl;
	}

	virtual void Func1()
	{
		cout << "Person::Func1()" << endl;
	}
};
//只要是虛函數都會存虛表裏去
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Student::買票-半價" << endl;
	}
	virtual void Func2()
	{
		cout << "Student::Func2()" << endl;
	}
};

int main() {
	Person p;
	Student s;

	return 0;
}

C++面向對象---多態_虛函數_05

通過代碼可打印虛表內容,驗證虛函數的存儲情況(以下代碼需在32和64位位環境下都可運行):

BuyTicket和Func1都被存入了虛表,但Func2並未顯示。實際上Func2同樣被存入了虛表,只是VS在此處進行了特殊處理,導致其未顯示出來。

打印虛表

#include <iostream>
#include <string>
#include <ctime>
#include <windows.h>

using namespace std;

// 定義虛函數指針類型
typedef void(*VFPTR)();

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Person::買票-全價" << endl;
	}

	virtual void Func1()
	{
		cout << "Person::Func1()" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket() override // 加override更規範
	{
		cout << "Student::買票-半價" << endl;
	}
	virtual void Func2()
	{
		cout << "Student::Func2()" << endl;
	}
};

// 打印虛表(增加count參數,控制循環次數,避免越界)
void PrintVTable(VFPTR vTable[], int count) {
	cout << "虛表地址:" << (void*)vTable << endl;
	for (int i = 0; i < count; ++i) { // 按已知虛函數數量循環
		printf("第%d個虛函數地址:%p → ", i, vTable[i]);
		vTable[i](); // 調用虛函數
	}
	cout << endl;
}

int main() {
	Person p;
	Student s;

	// 64位環境:用long long*提取8字節虛表指針(替代原來的int*)
	VFPTR* pVTable = (VFPTR*)*(long long*)&p;
	PrintVTable(pVTable, 2); // Person有2個虛函數

	VFPTR* sVTable = (VFPTR*)*(long long*)&s;
	PrintVTable(sVTable, 3); // Student有3個虛函數(繼承2個+新增1個)

	return 0;
}

C++面向對象---多態_派生類_06

Func2是有被放進虛表裏。

4.3 多繼承下的虛函數表

多繼承場景中,派生類會繼承所有基類的虛表,並將自身重寫的虛函數地址同步更新到各個基類虛表中,新增的虛函數地址則追加到第一個基類的虛表末尾。

#include <iostream>
#include <string>
#include <ctime>
#include <windows.h>

using namespace std;

// 定義虛函數指針類型(和能運行的代碼保持一致)
typedef void(*VFPTR)();

// 修復:要麼用count控制循環(安全),要麼保留nullptr判斷(兼容舊邏輯)
void PrintVFTable(VFPTR vTable[]) {
    cout << "虛表地址:" << (void*)vTable << endl;
    // 循環條件:保留能運行代碼的邏輯(vTable[i]!=nullptr)
    for (int i = 0; vTable[i] != nullptr; ++i) {
        printf("第%d個虛函數地址:%p → ", i, vTable[i]);
        vTable[i](); // 調用虛函數
    }
    cout << endl;
}

class Base1 {
public:
    virtual void func1() { cout << "Base1::func1" << endl; }
    virtual void func2() { cout << "Base1::func2" << endl; }
private:
    int b1 = 1;
};

class Base2 {
public:
    virtual void func1() { cout << "Base2::func1" << endl; }
    virtual void func2() { cout << "Base2::func2" << endl; }
private:
    int b2 = 2;
};

// 多繼承兩個基類
class Derive : public Base1, public Base2 {
public:
    // 重寫兩個基類的func1
    virtual void func1() { cout << "Derive::func1" << endl; }
    // 新增虛函數
    virtual void func3() { cout << "Derive::func3" << endl; }
private:
    int d = 3;
};

int main() {
    Derive d;
    // 修復:64位環境用long long*提取8字節虛表指針(32位用int*也可)
    // 提取Base1的虛表:Derive對象首地址就是Base1的虛表指針
    VFPTR* vTable1 = (VFPTR*)*(long long*)&d;
    PrintVFTable(vTable1);

    // 提取Base2的虛表:偏移Base1的大小(虛表指針+Base1的成員b1)
    // 64位下Base1大小=8字節(虛表指針8字節)+4字節(b1)=12字節(編譯器對齊到16字節?實測sizeof(Base1)=16)
    VFPTR* vTable2 = (VFPTR*)*(long long*)((char*)&d + sizeof(Base1));
    PrintVFTable(vTable2);

    return 0;
}

C++面向對象---多態_虛函數_07

五、關鍵補充知識點

5.1 重載、重寫、重定義的區別

  • 重載:同一作用域內,函數名相同但參數(類型、順序、個數)不同,與virtual無關。
  • 重寫:基類與派生類中,函數名、參數、返回值完全一致,且基類函數為虛函數。
  • 重定義(隱藏):基類與派生類中,函數名相同但不滿足重寫條件,派生類函數會隱藏基類函數。

5.2 虛函數的限制

  • 靜態成員函數不能是虛函數:靜態成員無this指針,無法訪問虛表,且通過::調用時不依賴對象。
  • 構造函數不能是虛函數:虛表在構造函數初始化列表階段生成,對象創建前虛表不存在。
  • 析構函數建議設為虛函數:確保通過基類指針刪除派生類對象時,析構函數能正確調用。

5.3 虛表的存儲位置

虛函數表在編譯階段生成,存儲於程序的常量區(與字符串常量、靜態變量存儲區域一致),而非堆或棧中。

#include <iostream>
#include <string>
#include <ctime>
#include <windows.h>

using namespace std;

class A
{
public:
	virtual void func1() { cout << "A::func1()" << endl; }
};

class B : public A {};

int main()
{
	B b;
	printf("虛表:%p\n", *((int*)&b));//可以看出虛表存在常量區

	static int x = 0;
	printf("static變量:%p\n", &x);

	const char* ptr = "hello world";
	printf("常量:%p\n", ptr);

	int y = 0;
	printf("局部變量:%p\n", &y);

	printf("new變量:%p\n", new int);


	return 0;
}

C++面向對象---多態_虛表_08