C++ 多态机制完全解析:从虚函数重写到动态绑定原理

引言

多态(polymorphism)是面向对象编程的三大特性之一,字面意思即"多种形态"。C++ 中的多态分为编译时多态(静态多态)运行时多态(动态多态)。编译时多态主要指函数重载和函数模板,它们在编译阶段根据参数类型或数量确定调用哪个函数。运行时多态则是指在程序运行时,通过基类的指针或引用调用同一个函数名,根据实际指向的对象类型执行不同的行为。本文聚焦于运行时多态,详细阐述其构成条件、虚函数重写、纯虚函数与抽象类、多态的原理(虚函数表与动态绑定),以及常见考点如析构函数重写、override/final 关键字等。


目录

引言

一、多态的概念

二、多态的定义及实现

[2.1 多态的构成条件](#2.1 多态的构成条件)

[2.2 虚函数](#2.2 虚函数)

[2.3 虚函数的重写(覆盖)](#2.3 虚函数的重写(覆盖))

[2.4 多态场景选择题分析](#2.4 多态场景选择题分析)

[2.5 虚函数重写的特殊情形](#2.5 虚函数重写的特殊情形)

[2.5.1 协变(Covariance)](#2.5.1 协变(Covariance))

[2.5.2 析构函数的重写](#2.5.2 析构函数的重写)

[2.6 override 和 final 关键字(C++11)](#2.6 override 和 final 关键字(C++11))

[2.7 重载、重写、隐藏的对比](#2.7 重载、重写、隐藏的对比)

三、纯虚函数和抽象类

四、多态的原理

[4.1 虚函数表指针(_vfptr)](#4.1 虚函数表指针(_vfptr))

[4.2 多态的实现机制](#4.2 多态的实现机制)

[4.3 虚函数表的内容](#4.3 虚函数表的内容)

[4.4 虚函数和虚表的存储位置](#4.4 虚函数和虚表的存储位置)

五、总结


一、多态的概念

运行时多态的具体表现为:执行某个行为(函数)时,传入不同的对象会完成不同的操作。例如:

  • 买票行为:普通人买票全价,学生买票打折,军人买票优先。

  • 动物叫声:猫对象传入发出"喵",狗对象传入发出"汪汪"。

这种"同一接口,不同实现"的能力,正是多态的核心价值。


二、多态的定义及实现

2.1 多态的构成条件

要实现运行时多态,必须同时满足两个条件:

  1. 调用方式 :必须通过基类的指针引用来调用虚函数。只有基类的指针或引用才能在运行时既指向基类对象又指向派生类对象。

  2. 函数属性 :被调用的函数必须是虚函数 ,并且派生类必须对该虚函数进行重写(覆盖)

2.2 虚函数

在类成员函数声明前加上 virtual 关键字,该函数即成为虚函数。非成员函数不能加 virtual

cpp

复制代码
class Person {
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

2.3 虚函数的重写(覆盖)

派生类中有一个与基类虚函数完全相同的函数(返回值类型、函数名、参数列表均相同),则称派生类的虚函数重写了基类的虚函数。

注意 :派生类重写时,可以省略 virtual 关键字。因为基类的虚函数被继承后,在派生类中仍然保持虚函数属性,但为了代码规范,建议显式写出 virtual。考试选择题中常利用省略 virtual 来考察是否构成重写。

cpp

复制代码
class Person {
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person {
public:
    virtual void BuyTicket() { cout << "买票-打折" << endl; }  // 重写
};

void Func(Person* ptr) {
    ptr->BuyTicket();  // 多态调用:由ptr指向的对象决定调用哪个版本
}

int main() {
    Person ps;
    Student st;
    Func(&ps);  // 输出:买票-全价
    Func(&st);  // 输出:买票-打折
    return 0;
}

2.4 多态场景选择题分析

题目(来自课件):

cpp

复制代码
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() {
    B* p = new B;
    p->test();
    return 0;
}

输出结果B->1

解析

  • p->test() 调用从 A 继承来的 test() 函数(B 未重写 test)。

  • test() 内部调用 func(),由于 func 是虚函数且通过 this 指针(相当于基类指针)调用,满足多态条件,因此调用 B 中重写的 func

  • 关键陷阱 :虚函数的重写只覆盖函数体,不覆盖默认参数 。默认参数在编译阶段根据调用者的静态类型 确定。此处 test() 是在 A 中定义的,编译时 this 的类型是 A*,所以默认参数使用基类 funcval = 1。因此输出 B->1

2.5 虚函数重写的特殊情形

2.5.1 协变(Covariance)

派生类重写基类虚函数时,返回值类型可以不同,但必须满足:基类虚函数返回基类对象的指针/引用,派生类虚函数返回派生类对象的指针/引用。这种特性称为协变,实际应用较少。

cpp

复制代码
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; }
};
2.5.2 析构函数的重写

基类的析构函数建议定义为虚函数。虽然基类和派生类的析构函数名称不同(~Person vs ~Student),但编译器会将所有析构函数名统一处理为 destructor。因此,只要基类析构函数是虚函数,派生类的析构函数无论是否加 virtual,都与基类析构函数构成重写。

重要性 :若基类析构函数不是虚函数,则通过基类指针 delete 派生类对象时,只会调用基类的析构函数,不会调用派生类的析构函数,导致派生类中动态分配的资源无法释放,造成内存泄漏。

cpp

复制代码
class A {
public:
    virtual ~A() { cout << "~A()" << endl; }  // 虚析构
};
class B : public A {
public:
    ~B() { cout << "~B()" << endl; delete[] _p; }
private:
    int* _p = new int[10];
};

int main() {
    A* p2 = new B;
    delete p2;  // 先调用~B(),再调用~A()
    return 0;
}

2.6 override 和 final 关键字(C++11)

  • override:显式声明派生类函数重写了基类的虚函数。如果实际未构成重写(如函数名拼写错误、参数列表不同),编译器会报错,避免运行时意外。

  • final:修饰虚函数,禁止派生类重写该函数;或修饰类,禁止该类被继承。

cpp

复制代码
class Car {
public:
    virtual void Drive() {}
};
class Benz : public Car {
public:
    virtual void Drive() override { cout << "Benz-舒适" << endl; }  // 正确重写
};

class Car2 {
public:
    virtual void Drive() final {}  // 禁止重写
};
class Benz2 : public Car2 {
public:
    virtual void Drive() {}  // 编译错误:无法重写final函数
};

2.7 重载、重写、隐藏的对比

比较项 重载(Overload) 重写/覆盖(Override) 隐藏(Hide)
作用范围 同一类中 基类和派生类之间 基类和派生类之间
函数名 相同 相同 相同
参数列表 不同(类型、个数、顺序) 完全相同(协变除外) 可以相同也可以不同
返回值 无要求 相同或协变 无要求
virtual 不需要 基类必须加 virtual,派生类可加可不加 不需要
访问方式 编译时决定 运行时多态(基类指针/引用调用) 派生类对象直接调用时隐藏基类同名成员

三、纯虚函数和抽象类

在虚函数声明后加上 = 0,该函数即为纯虚函数 。纯虚函数通常不需要定义(但语法上允许提供实现)。包含纯虚函数的类称为抽象类,抽象类不能实例化对象。派生类必须重写所有纯虚函数,否则派生类仍然是抽象类。纯虚函数强制派生类实现特定接口。

cpp

复制代码
class Car {
public:
    virtual void Drive() = 0;  // 纯虚函数
};

class Benz : public Car {
public:
    virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

class BMW : public Car {
public:
    virtual void Drive() override { cout << "BMW-操控" << endl; }
};

int main() {
    // Car car;  // 错误:抽象类不能实例化
    Car* pBenz = new Benz;
    pBenz->Drive();
    Car* pBMW = new BMW;
    pBMW->Drive();
    return 0;
}

四、多态的原理

4.1 虚函数表指针(_vfptr

一个含有虚函数的类,其实例化对象中会多出一个指针,称为虚函数表指针_vfptr,v 代表 virtual,f 代表 function)。该指针指向一个虚函数表(简称虚表),虚表中存放该类所有虚函数的地址。

cpp

复制代码
class Base {
public:
    virtual void Func1() { cout << "Func1()" << endl; }
protected:
    int _b = 1;
    char _ch = 'x';
};

int main() {
    Base b;
    cout << sizeof(b) << endl;  // 在32位平台下,通常为12字节:_b(4) + _ch(1) + 对齐(3) + _vfptr(4)
    return 0;
}
  • 同一个类的不同对象共享同一张虚表。

  • 派生类对象中包含基类部分,基类部分的虚表指针与基类对象的虚表指针不是同一个(但指向的虚表内容不同)。

4.2 多态的实现机制

当通过基类指针或引用调用虚函数时,编译器不会在编译时直接确定函数地址,而是:

  1. 运行时取出指针/引用所指向对象的 _vfptr

  2. 从虚表中获取对应的虚函数地址。

  3. 调用该函数。

这就是动态绑定 (运行时绑定)。如果不满足多态条件(如通过对象直接调用虚函数,或调用的不是虚函数),则在编译时确定函数地址,称为静态绑定

cpp

复制代码
void Func(Person* ptr) {
    ptr->BuyTicket();  // 动态绑定:运行时到ptr指向对象的虚表中查找BuyTicket地址
}

4.3 虚函数表的内容

以如下代码为例:

cpp

复制代码
class Base {
public:
    virtual void func1() { cout << "Base::func1" << endl; }
    virtual void func2() { cout << "Base::func2" << endl; }
    void func5() { cout << "Base::func5" << endl; }
protected:
    int a = 1;
};

class Derive : public Base {
public:
    virtual void func1() override { cout << "Derive::func1" << endl; }  // 重写
    virtual void func3() { cout << "Derive::func3" << endl; }
    void func4() { cout << "Derive::func4" << endl; }
protected:
    int b = 2;
};

虚表结构

  • 基类 Base 的虚表 :存放 &Base::func1&Base::func2,以 0 结尾(VS 编译器)。

  • 派生类 Derive 的虚表

    • 首先存放继承自 Base 的虚函数地址,但被重写的 func1 被替换为 &Derive::func1

    • 然后存放 &Base::func2(未重写)。

    • 最后存放派生类自己的虚函数 &Derive::func3

    • 普通成员函数 func4func5 不在虚表中。

4.4 虚函数和虚表的存储位置

  • 虚函数 :和普通函数一样,编译后成为指令,存放在代码段(或常量区)。虚表中存储的是这些函数的地址。

  • 虚表 :C++ 标准未规定具体位置。在 VS 编译器中,虚表通常存放在常量区(代码段)。可通过对比栈、堆、静态区、常量区的地址验证。

cpp

复制代码
int main() {
    int i = 0;                // 栈
    static int j = 1;         // 静态区
    int* p1 = new int;        // 堆
    const char* p2 = "xxxx";  // 常量区
    Base b;
    printf("栈:%p\n", &i);
    printf("静态区:%p\n", &j);
    printf("堆:%p\n", p1);
    printf("常量区:%p\n", p2);
    printf("Base虚表地址:%p\n", *(int**)&b);  // 虚表地址
    printf("虚函数地址:%p\n", &Base::func1);
    printf("普通函数地址:%p\n", &Base::func5);
    return 0;
}

运行结果示例(VS)显示虚表地址与常量区地址相近,证明虚表存放在常量区。


五、总结

本文从运行时多态的基本概念出发,系统阐述了其实现所需的两个核心条件(基类指针/引用 + 虚函数重写),并深入分析了虚函数重写的各种细节,包括协变、析构函数重写、默认参数陷阱等。C++11 引入的 overridefinal 关键字为虚函数重写提供了编译期检查,提高了代码安全性。纯虚函数和抽象类则提供了接口强制实现的机制。

多态的原理基于虚函数表指针和虚表:每个含虚函数的对象都有一个 _vfptr 指向所属类的虚表,虚表中存放该类所有虚函数的地址。通过基类指针/引用调用虚函数时,运行时动态地从虚表中获取函数地址,实现动态绑定。这种机制使得程序可以在运行时根据实际对象类型决定行为,极大地提升了代码的可扩展性和复用性。

理解多态的内部原理,对于编写正确的继承体系代码、避免内存泄漏(虚析构)、理解动态绑定开销等均有重要意义。在实际工程中,应合理运用多态来设计可扩展的接口,同时注意避免过度复杂的继承层次。

相关推荐
IT_陈寒21 分钟前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro1 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax2 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH2 小时前
Koa和Express的区别
后端
MariaH2 小时前
Koa框架的使用
后端
luckdewei3 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某4 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy4 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom4 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
唐青枫8 小时前
Java JDBC 实战指南:从 Connection 到事务和连接池
java