跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C++ 面向对象核心:多态详解

C++ 多态分为编译时和运行时两种,重点在于运行时多态。通过虚函数、基类指针或引用调用派生类重写后的函数实现动态绑定。文章涵盖虚函数定义、override/final 关键字使用、析构函数虚化必要性、纯虚函数与抽象类概念,以及底层虚表指针机制。特别解析了默认参数静态绑定等面试陷阱,帮助深入理解多态原理与内存布局。

steve发布于 2026/3/24更新于 2026/9/2262 浏览
C++ 面向对象核心:多态详解

C++ 面向对象核心:多态详解

一、多态的概念及实现

1. 什么是多态?

多态的核心在于'多种形态'。它是 C++ 面向对象编程最重要的特性之一,分为编译时多态(静态多态)和运行时多态(动态多态)。这里我们重点讨论运行时多态。

编译时多态主要涉及函数重载和模板,通过参数类型匹配在编译期决定调用哪个函数。之所以叫编译时多态,是因为实参传给形参的匹配是在编译阶段完成的。

运行时多态则更灵活:同一个行为(函数),传入不同的对象,执行不同的逻辑。比如买票这个行为,普通人全价,学生优惠,军人优先;再比如动物叫,猫是喵,狗是汪。这就是通过不同对象实现了同一接口的不同表现。

2. 虚函数

类成员函数前加 virtual 修饰,该函数即为虚函数。注意非成员函数不能加 virtual。

class Person {
public:
    // 加上 virtual,BuyTicket 就是虚函数
    virtual void BuyTicket() {
        cout << "买票 - 全价" << endl;
    }
};
虚函数的重写/覆盖

虚函数重写(Override)发生在派生类中,要求基类必须有被 virtual 修饰的虚函数。

重写是实现多态的关键。定义是:在派生类中提供一个与基类中某个虚函数具有完全相同函数名、参数列表、返回值类型的函数。

重写的目的是在运行时,当通过基类指针或引用指向派生类对象时,能够调用派生类的版本,而不是基类的版本。这就是'动态绑定'的基础。

// Person 类与 Student 类中的 buyticket 构成重写
class Person {
public:
    virtual void buyticket() {
        cout << "全价票" << endl;
    }
};

class Student : public Person {
public:
    virtual void buyticket() {
        cout << "半价票" << endl;
    }
};

需要注意一点:在重写基类虚函数时,派生类的虚函数在不加 virtual 关键字时,也可以构成重写。因为子类继承基类后,基类的虚函数属性会被继承下来,在派生类依旧保持虚函数属性。不过这种写法不规范,不建议使用,但在考试选择题中常作为陷阱出现。

// A 类与 B 类中的 func 构成重写
class A {
public:
    virtual void func() {}
};

class B : public A {
public:
    void func() {} // 未加 virtual,但依然构成重写
};
override 和 final 关键字

C++ 对虚函数重写的要求比较严格,有时疏忽导致函数名或参数写错,无法构成重写,而编译器不会报错,只有运行后才发现问题。因此 C++11 提供了 override 关键字来辅助检测。

class Person {
public:
    virtual void buyticket() {}
};

class Student : public Person {
public:
    virtual void buyticket() override {} // 如果签名不匹配,编译会报错
};

如果我们不想让派生类继续重写这个虚函数,可以用 final 修饰。

3. 多态的实现

多态实现的条件

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

  1. 必须是基类的指针或者引用调用虚函数。
  2. 被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。

第一点是因为只有基类的指针或引用才能同时指向基类和派生类对象。第二点是因为派生类必须重写虚函数,否则基类和派生类之间没有区别,也就没有多态的效果。

class Person {
public:
    virtual void buyticket() {
        cout << "全价票" << endl;
    }
};

class Student : public Person {
public:
    virtual void buyticket() override {
        cout << "半价票" << endl;
    }
};

void Func(Person* ptr) {
    ptr->buyticket(); // 用基类指针调用虚函数
}

int main() {
    Person p;
    Student s;
    Func(&p); // 调用基类虚函数
    Func(&s); // 调用派生类虚函数
    return 0;
}
多态场景下的经典面试题

看下面这段代码,输出结果是什么?

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

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

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

很多人直觉会选 D(B->0),因为指针指向 B 类对象。但正确答案是 B (B->1)。

原因在于:默认参数是静态绑定的。虽然 test 函数是虚函数,支持动态绑定调用函数体,但默认参数的值是在编译时根据调用者的指针类型决定的。这里 p 是 B* 类型,但 test() 属于基类 A 的接口声明,所以默认参数 val 会使用 A 中声明的值 1,而不是 B 中的 0。

形象理解:虚函数重写后,派生类中的虚函数其实是由基类虚函数的函数名和参数列表(包括默认值)与派生类的虚函数实现部分组成的。

4. 虚函数重写的特殊场景

析构函数的重写

基类的析构函数为虚函数时,派生类析构函数无论是否加 virtual,都与基类析构函数构成重写。这是因为编译器对析构函数名称做了特殊处理,统一成 destructor。

为什么要这样设计? 这关系到内存安全。

当一个基类指针 ptr 指向派生类对象时,如果基类析构函数不是虚函数,delete ptr 时只会调用基类析构函数(静态绑定)。如果派生类中有额外资源(如动态分配的内存),就会导致内存泄漏。

只要基类析构函数设为虚函数,delete ptr 就会根据指针真正指向的类型调用相应的析构函数(动态绑定),派生类析构函数先执行释放资源,随后自动调用基类析构函数,确保资源被正确清理。

class A {
public:
    virtual ~A() {} // 虚析构函数
protected:
    int _a;
};

class B : public A {
public:
    ~B() { 
        delete[] _b; 
        _b = nullptr; 
    }
protected:
    int* _b = new int[10]; // 派生类有额外资源
};

int main() {
    A* a1 = new B;
    delete a1; // 正确释放,不会泄漏
    return 0;
}
协变(了解)

派生类重写基类虚函数时,返回值类型可以不同。如果基类返回基类对象的指针或引用,派生类返回派生类对象的指针或引用,称为协变。

class Person {
public:
    virtual Person* BuyTicket() {
        cout << "买票 - 全价" << endl;
        return nullptr;
    }
};

class Student : public Person {
public:
    virtual Student* BuyTicket() { // 返回派生类指针
        cout << "买票 - 打折" << endl;
        return nullptr;
    }
};

5. 纯虚函数和抽象类

在虚函数后面写上 =0,该函数为纯虚函数。包含纯虚函数的类叫做抽象类,抽象类不能实例化对象。

如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数强制了派生类必须重写,否则无法实例化。

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

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

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

二、多态的原理

1. 虚函数表指针

含有虚函数的类中至少都有一个虚函数表指针(vptr)。在 32 位机器下,一个指针占 4 字节。如果一个类有虚函数,对象大小会增加一个指针的大小。

例如:

class B {
    char _ch;
    int _b;
    // 隐含了一个 _vfptr 指针
};

对象大小不仅仅是成员变量之和,还要考虑对齐和 vptr。

2. 虚函数表

  • 基类对象的虚函数表中存放基类所有虚函数的地址。 同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表。
  • 派生类虚函数表:包含继承下来的基类虚函数地址(可能被覆盖)、派生类重写的虚函数地址、派生类自己的虚函数地址。
  • 虚函数存储位置:虚函数本身存储在代码段,虚函数表通常也存储在代码段(常量区)。

3. 多态的底层实现

满足多态条件后,底层不再是编译时确定函数地址,而是运行时到指向对象的虚表中查找对应的虚函数地址。这就实现了指针或引用指向基类就调用基类虚函数,指向派生类就调用派生类对应虚函数。

4. 动态绑定和静态绑定

  • 静态绑定:对不满足多态条件的函数调用(如普通函数、非虚函数),在编译时确定调用地址。
  • 动态绑定:满足多态条件的函数调用,在运行时到虚函数表中找到调用地址。

三、总结

多态是 C++ 面向对象编程非常重要的特性,在处理相似特性的问题时作用巨大。面试中关于多态的考点很多,尤其是虚函数机制、析构函数虚化以及默认参数的静态绑定等细节,需要结合底层原理深入理解。

目录

  1. C++ 面向对象核心:多态详解
  2. 一、多态的概念及实现
  3. 1. 什么是多态?
  4. 2. 虚函数
  5. 虚函数的重写/覆盖
  6. override 和 final 关键字
  7. 3. 多态的实现
  8. 多态实现的条件
  9. 多态场景下的经典面试题
  10. 4. 虚函数重写的特殊场景
  11. 析构函数的重写
  12. 协变(了解)
  13. 5. 纯虚函数和抽象类
  14. 二、多态的原理
  15. 1. 虚函数表指针
  16. 2. 虚函数表
  17. 3. 多态的底层实现
  18. 4. 动态绑定和静态绑定
  19. 三、总结

更多推荐文章

查看全部
  • DeepSeek R1 671B 本地部署与动态量化技术详解
  • 前端缓存策略最佳实践
  • 电科金仓 KES V9 2025:融合 AI 与多模态数据库技术解析
  • OpenClaw 多机器人多 Agent 模式详解
  • LW-CTrans: 用于 3D 医学图像分割的轻量级 CNN-Transformer 混合网络
  • Jenkins 自动化部署故障排查实战:从错误日志到解决方案
  • PDFPatcher 开源 PDF 处理工具五大技术突破解析
  • MiniMax 开源 MiniMax-M2.5 模型:编程与智能体性能突破
  • Moon VR Video Player 使用指南:支持 8K/12K 多音轨与外挂字幕
  • VS Code 配置 AI Agent Skills 实战指南
  • C++ 图论实战:三种经典最短路径算法解析
  • GitHub 44K Stars Skills:开源智能体技能库与自定义开发指南
  • 5 款免费 AIGC 检测工具推荐及降重方法
  • Unity 语音识别集成方案:Whisper.unity 使用指南
  • Raphael AI:用 Flux 模型驱动的免费图片生成器,无需注册无限制
  • Open WebUI 基于 Docker 本地部署与远程 Ollama 集成指南
  • Windows 系统下 Git 安装与配置完整指南
  • 2024 年转行 AI 产品经理的时机与路径分析
  • 通义万相 2.1 开源视频生成模型功能解析
  • GitHub Copilot、Trae 与 Cursor AI 编程工具对比评测

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online