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

C++ 多态:概念、实现与底层原理详解

C++ 多态通过基类指针或引用调用虚函数,在运行时根据实际对象类型执行不同行为。实现需满足继承关系、虚函数声明及重写条件。底层依赖虚函数表(vtable)和虚函数指针(vptr),通过动态绑定机制完成地址解析。纯虚函数定义抽象类,强制派生类实现接口。掌握 override 和 final 关键字可增强代码安全性与扩展性控制。

山野诗人发布于 2026/3/24更新于 2026/7/623 浏览
C++ 多态:概念、实现与底层原理详解

多态

多态(polymorphism)是面向对象编程的灵魂之一。它让同一个接口在不同对象上表现出不同的行为,从而大大提升了代码的复用性、扩展性与灵活性。本文将深入理解多态的核心原理,从概念、实现条件、虚函数、重写规则,到虚函数表与动态绑定机制,逐步揭开多态背后的运行逻辑。

一、多态的概念

通俗来说,多态就是'多种形态'。分为编译时多态(静态多态)和运行时多态(动态多态)。编译时多态主要通过函数重载和模板实现,参数匹配在编译期完成;运行时多态则是通过基类指针或引用调用虚函数,根据实际对象类型决定执行哪个函数。比如同样是'买票'行为,普通人全价,学生优惠,军人优先。

二、多态的定义及实现

多态发生在继承体系下,基类指针或引用调用同一函数产生不同行为。例如 Student 继承 Person,Person 买票全价,Student 买票半价。

2.1 多态的构成条件

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

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

解释一下:只有基类的指针或引用才能既指向基类对象又指向派生类对象;而派生类必须对基类的虚函数完成重写,这样两者之间才有不同的函数实现,多态效果才能显现。

2.1.2 虚函数

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

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

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

即调用同一函数,产生了不同的行为。

2.1.3 虚函数的重写/覆盖

派生类中有跟基类完全相同的虚函数(返回值类型、函数名字、参数列表完全相同),称为重写。 注意:派生类重写时虽然可以不加 virtual 关键字(因为继承后属性保持),但为了规范建议加上。考试选择题常在此设坑。

#include <iostream>
using namespace std;

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

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

void fun(Person& p) { p.BuyTicket(); }

void test() {
    Person p;
    fun(p);
    Student s;
    fun(s);
}

int main() {
    test();
    return 0;
}

这里可以看到 fun 函数虽然都是 Person 引用 p 在调用 BuyTicket,但结果由 p 指向的实际对象决定。

2.1.4 多态场景的一个选择题

以下程序输出结果是什么? A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

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(int argc, char* argv[]) {
    B* p = new B;
    p->test();
    return 0;
}

这道题有些难度。对于虚函数重写,我们理解为只重写实现(函数体)。所以派生类可以不写 virtual。 但在 p->test() 时,this 指针实际上是 A* 类型。 func 函数是否构成重写?由于默认参数不一致,且 val 参数不同,这里存在陷阱。 最终输出应该是 B->1。

2.1.5 虚函数重写的一些其他问题
  • 协变:派生类重写基类虚函数时,若基类返回基类指针/引用,派生类返回派生类指针/引用,称为协变。
  • 析构函数的重写:基类析构函数为虚函数时,派生类析构函数无论是否加 virtual 都构成重写。这很重要,否则 delete 基类指针时只会调用基类析构函数,导致内存泄漏。
class A {
public:
    virtual ~A() { cout << "~A()" << endl; }
};

class B : public A {
public:
    ~B() { cout << "~B()->delete:" << _p << endl; delete _p; }
protected:
    int* _p = new int[10];
};

int main() {
    A* p1 = new A;
    A* p2 = new B;
    delete p1;
    delete p2; // 只有基类析构函数为虚函数,这里才能正确调用 B 的析构函数
    return 0;
}

一句话:为了防止通过基类指针删除派生类对象时,只调用基类析构函数而不调用派生类析构函数,导致内存泄漏。

2.1.6 override 和 final 关键字

C++11 提供了 override 帮助检测是否重写成功,避免拼写错误导致的静默失败。如果想禁止派生类继续重写,可用 final 修饰。

class Car {
public:
    virtual void Drive() {}
};

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

若拼写错误,编译器会报错。若基类加了 final,派生类再 override 也会报错。

2.1.7 重载/重写/隐藏的对比

这是面试常考点。重载是同一作用域同名不同参;重写是继承关系中同名同参;隐藏是派生类屏蔽基类同名函数(无论参数是否相同)。

三、纯虚函数和抽象类

虚函数后写 =0 即为纯虚函数。包含纯虚函数的类叫抽象类,不能实例化。派生类若不重写纯虚函数,也是抽象类。这强制了派生类实现接口。

class Car {
public:
    virtual void Drive() = 0;
};

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

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

int main() {
    Car* pBenz = new Benz;
    pBenz->Drive();
    Car* pBMW = new BMW;
    pBMW->Drive();
    return 0;
}

四、多态的原理

4.1 虚函数表指针

含有虚函数的类对象中至少有一个虚函数表指针(vptr)。一个类所有虚函数的地址被放到该对象的虚函数表中(简称虚表)。

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

int main() {
    Base b;
    cout << sizeof(b) << endl; // 通常比成员变量总和大,因为多了 vptr
    return 0;
}

虚函数表本质是函数指针数组。

4.2 多态的原理
4.2.1 多态是如何实现的

底层不再是编译时确定函数地址,而是运行时到指向对象的虚表中查找对应虚函数的地址。

class Person {
public:
    virtual void BuyTicket() { cout << "买票全价" << endl; }
private:
    string _name;
};

class Student : public Person {
public:
    virtual void BuyTicket() { cout << "买票打折" << endl; }
private:
    string _id;
};

class Soldier : public Person {
public:
    virtual void BuyTicket() { cout << "买票优先" << endl; }
private:
    string _codename;
};

void Func(Person* ptr) { ptr->BuyTicket(); }

int main() {
    Person ps;
    Student st;
    Soldier sr;
    Func(&ps);
    Func(&st);
    Func(&sr);
    return 0;
}

ptr 指向谁,就去谁的虚表里找 BuyTicket 的地址。

4.2.2 动态绑定与静态绑定
  • 静态绑定:不满足多态条件(如普通函数调用),编译时确定地址。
  • 动态绑定:满足多态条件(指针/引用 + 虚函数),运行时查虚表确定地址。

涉及汇编层面,简单理解即可:静态绑定直接 call 函数地址;动态绑定先取 vptr,再取 vtable 中的函数地址,最后 call。

4.2.3 虚函数表
  • 基类所有虚函数的地址。同类型对象共用同一张虚表。
  • 派生类虚表包含:(1) 基类虚函数地址,(2) 派生类重写的虚函数地址(覆盖),(3) 派生类自己的虚函数地址。
  • 虚函数表一般存放在常量区。VS 编译器会在虚表末尾放 0x00000000 标记,g++ 通常不放。
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() { cout << "Derive::func1" << endl; }
    virtual void func3() { cout << "Derive::func1" << endl; }
    void func4() { cout << "Derive::func4" << endl; }
protected:
    int b = 2;
};

int main() {
    Base b;
    Derive d;
    // 调试时可查看虚表地址
    return 0;
}

总结

多态让'同一接口'在'不同对象'上拥有'不同实现',是 C++ 面向对象思想真正落地的关键。掌握其底层机制有助于写出更健壮、高效的代码。

目录

  1. 多态
  2. 一、多态的概念
  3. 二、多态的定义及实现
  4. 2.1 多态的构成条件
  5. 2.1.2 虚函数
  6. 2.1.3 虚函数的重写/覆盖
  7. 2.1.4 多态场景的一个选择题
  8. 2.1.5 虚函数重写的一些其他问题
  9. 2.1.6 override 和 final 关键字
  10. 2.1.7 重载/重写/隐藏的对比
  11. 三、纯虚函数和抽象类
  12. 四、多态的原理
  13. 4.1 虚函数表指针
  14. 4.2 多态的原理
  15. 4.2.1 多态是如何实现的
  16. 4.2.2 动态绑定与静态绑定
  17. 4.2.3 虚函数表
  18. 总结
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Java 按值传递与按引用传递详解
  • Flutter 三方库 eth_sig_util 的鸿蒙化适配指南
  • Stable Diffusion 本地部署与快速上手指南
  • Fish Speech-1.5 多语种语音合成:中英混合文本发音处理技巧
  • RustFS Docker 部署实战:MinIO 高性能替代方案
  • 医疗 AI 场景下的逻辑回归算法实战
  • 通义万相 2.1 技术解析:多模态生成的突破与应用
  • Git 远程仓库同步实战:基础操作与避坑指南
  • 工业具身智能实操指南:从视觉识别到自主执行
  • C++ 搜索引擎实战:日志宏封装与 Server 交互逻辑
  • ReAct Agent 与 Agent 编排:从单 Agent 闭环到多 Agent 协作
  • 基于 RAG 的智能知识问答探索企业知识边界
  • Django Web 框架实战:从零构建产品管理系统
  • C 与 C++ 设计哲学冲突:显式控制与抽象封装
  • C++ 哈希表核心机制:冲突处理与负载因子
  • Windows 权限提升:自动化枚举方法
  • 使用 LangChain 结合 LLM 实现私有化文档搜索
  • 本地部署 AI 服务零公网 IP 安全访问方案
  • 深入理解 C++ STL list 底层实现与模拟实现
  • 从零构建大模型:深入理解大型语言模型原理与实现

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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