C++ 多态深入解析:从概念本质、语法规则到底层实现(结合实战代码的全方位指南)

 更新时间:2026年11月04日 09:58:37   作者:草莓熊Lotso  
本文深入讲解C++运行时多态的核心机制,涵盖虚函数、重写条件、协变、析构函数虚拟化等关键知识点,并结合实例分析override与final的应用,帮助开发者理解多态底层实现及常见误区,感兴趣的朋友一起看看吧

前言:

多态是 C++ 面向对象 三大特性(封装、继承、多态) 的核心,它让 “同一行为作用于不同对象产生不同结果” 成为可能。本文将从多态的基础概念切入,逐步拆解多态的构成条件、虚函数重写规则及关键细节,帮你彻底掌握运行时多态的实现逻辑。

一. 多态的概念:从“多种形态说起”

1.1 多态的概念解析

多态通俗来说就是 “多种形态” ,在C++中分为两类:

  • 编译时多态(静态多态):通过函数模板,重载来实现,编译阶段确定调用的函数(如add(1,2)和add(1,2,3,4)调用的不同的函数)
  • 运行时多态(动态多态):本篇博客核心讲解的地方,通过”基类指针/引用 + 虚函数重写“,运行阶段根据指向的对象的类型确定调用的函数

本文代码示例所需头文件:

#include <iostream>
using namespace std;

代码仓库:多态 - Gitee.com

1.2 生活中的多态示例

最经典的场景就是“买票行为”:

  • 普通人买票 -> 全价
  • 学生买票 -> 半价

用样是“买票”,不同对象执行不同逻辑,这就是多态的本质。我们接下来会在代码中通过继承和多态来实现“调用同一函数”产生不同结果。

二. 多态的构成条件和核心语法

多态是一个 继承关系 下的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象优惠买票(当然再来个军人对象继承Person的话,这个对象去调用也会由不同的结果)

要实现多态,除了要有继承关系,还必须满足下面这两个强制条件,缺一不可:

  • 必须是基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,并且派生类对基类的虚函数完成了 “重写” (覆盖)。

说明:要实现多态的效果,第一必须是基类的指针或者引用,因为只有基类的指针或引用才能既指向基类对象又指向派生类对象;第二派生类必须对基类的虚函数完成重写/覆盖,重写了,基类和派生类之间才能有不同的函数,多态的不形态效果才能达到。

2.1 条件 1:虚函数的定义

类成员函数前加virtual关键字,该函数即为虚函数(非成员函数和静态成员函数不能加virtual)。虚函数的作用是 “标记” 该函数需要参与多态,让编译器为其生成动态绑定逻辑。

// 基类:Person
class Person 
{
public:
    // 虚函数:标记为需要参与多态
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

2.2 条件 2:虚函数的重写(覆盖)

派生类中定义一个 “与基类虚函数完全一致” 的函数,即为重写。这里 “完全一致” 指:

  • 函数名相同;
  • 参数列表(参数类型、个数、顺序)相同(缺省参数可以不管);
  • 返回值类型相同(协变除外,下文讲解)。

代码示例(注意看注释):

// 基类:Person
class Person 
{
public:
    // 虚函数:标记为需要参与多态
    virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
// 派生类:Student(继承Person)
class Student : public Person 
{
public:
    // 重写基类虚函数:函数名、参数、返回值完全一致
    // 派生类中virtual也可以省略
    virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
// 派生类:Soldier(继承Person)
class Soldier : public Person 
{
public:
    // 重写基类虚函数
    // 派生类中virtual也可以省略
    virtual void BuyTicket() { cout << "买票-优先" << endl; }
};
// 关键:用基类指针调用虚函数(满足多态条件1)
// 这里也可以用基类引用
void Func(Person* ptr) 
{
    // 运行时根据ptr指向的对象类型,调用对应类的BuyTicket
    // Person* ptr
    ptr->BuyTicket();
    // 如果是基类引用(Person& ptr)
    //ptr.BuyTicket();
}
int main() 
{
    Person ps;    // 基类对象
    Student st;   // 派生类对象(学生)
    Soldier sr;   // 派生类对象(军人)
    Func(&ps);  // 指向基类对象 → 调用Person::BuyTicket → 输出“买票-全价”
    Func(&st);  // 指向学生对象 → 调用Student::BuyTicket → 输出“买票-打折”
    Func(&sr);  // 指向军人对象 → 调用Soldier::BuyTicket → 输出“买票-优先”
    return 0;
}

注意:派生类重写时,即使不加virtual,也能构成重写(因为基类虚函数的 “虚属性” 会被继承),但不建议这么写,可读性差且易出错。
坑点(下面的笔试题会有体现):在 C++ 中,虚函数重写时若基类和派生类的虚函数都指定了缺省参数,调用时的缺省值只由 “基类的函数声明” 决定,与派生类的重写实现无关。

2.3 多态场景的一个笔试选择题(重要):

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

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

图解如下:

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

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

图解如下:

三、虚函数重写的特殊情况

虚函数重写并非只有 “完全一致” 一种情况,还有两种特殊场景需要注意:协变和析构函数重写,这也是面试高频考点。

3.1 协变(了解)

派生类重写基类虚函数时,返回值类型可以不同,但必须满足:

  • 基类虚函数返回 “基类对象的指针 / 引用”;
  • 派生类虚函数返回 “派生类对象的指针 / 引用”。

这种情况称为 “协变”,实际开发中使用较少,了解即可。

代码示例(注意看注释):

#include <iostream>
using namespace std;
// 基类A
class A {};
// 派生类B(继承A)
class B : public A {};
// 基类Person
class Person 
{
public:
    // 虚函数:返回基类A的指针
    virtual A* BuyTicket() 
    { 
        cout << "买票-全价" << endl; 
        return nullptr;
    }
};
// 派生类Student
class Student : public Person {
public:
    // 重写:返回派生类B的指针(协变)
    virtual B* BuyTicket() { 
        cout << "买票-打折" << endl; 
        return nullptr;
    }
};
void Func(Person* ptr) {
    ptr->BuyTicket(); // 多态调用依然生效
}
int main() 
{
    Person ps;
    Student st;
    Func(&ps); // 输出“买票-全价”
    Func(&st); // 输出“买票-打折”
    return 0;
}

3.2 析构函数的重写(重点)

基类的析构函数为虚函数:基类析构函数加virtual后,派生类析构函数无论是否加virtual,都构成重写。这是因为编译器会将所有析构函数的名称统一处理为destructor,看似名称不同,实则一致。

注意:这个在面试题中经常考到,问基类中的析构函数建不建议写成虚函数?大家可以结合下面的代码示例和为什么去进行回答,这样才能讲清楚

为什么需要析构函数构成重写?
如果基类析构函数不是虚函数,用基类指针指向派生类对象并delete时,只会调用基类析构函数,导致派生类中动态申请的资源无法释放,引发内存泄漏。

代码示例(注意看注释,其中额外测试部分是补充了解的和这里的重点不同):

class A {
public:
    // 基类析构函数加virtual,支持重写
    virtual ~A() {
        cout << "~A()" << endl;
    }
};
class B : public A {
public:
    // 派生类析构函数:自动构成重写(加不加virtual都可以)
    ~B() {
        cout << "~B()->delete:" << _p << endl;
        delete _p; // 释放派生类动态申请的资源
    }
protected:
    int* _p = new int[10]; // 派生类动态申请的数组
};
void test()
{
    cout << "--------额外测试结果--------" << endl;
    //额外测试,这个是正常场景,加不加都行
    //只是为了让大家了解一下这个析构顺序
    //析构顺序:~B(),~A(),~A()
    //其中第一个~A()是因为子类B析构完后调用基类的(先子后父),后面一个是a对象析构
    A a;
    B b;
}
// 基类只要保障了析构函数是虚函数,下面场景就不会存在内存泄漏
int main() 
{
    // 基类指针指向派生类对象
    A* ptr1 = new B;
    delete ptr1; // 多态调用:先调用~B(),再先子后父自动调用~A(),无内存泄漏
    // 基类指针指向基类对象
    A* ptr2 = new A;
    delete ptr2; // 调用~A()
    test();
    return 0;
}

主要测试版块:如果基类析构不加 virtual :delete ptr1只会调用~A(),B类中_p指向的数组未释放,导致内存泄漏。额外测试那里加不加都行。

四. C++11:override 与 final 关键字

虚函数重写对语法要求严格(如函数名写错、参数类型不匹配),这些错误编译时不会报错,只会在运行时出现非预期结果。C++11 提供override和final两个关键字,帮我们在编译阶段检测错误。

4.1 override:检测是否重写

在派生类虚函数后加override,编译器会检查该函数是否真的重写了基类虚函数。若未重写(如函数名错、参数错),直接编译报错。
代码示例(注意看注释):

class Car {
public:
    // 基类虚函数:Drive(注意拼写是Drive,不是Dirve)
    virtual void Drive() { 
        cout << "Car-行驶" << endl; 
    }
};
class Benz : public Car {
public:
    // 错误示例:函数名写成Dirve,加override后编译报错
    // virtual void Dirve() override { cout << "Benz-舒适" << endl; }
    // 正确示例:函数名正确,override检测通过
    virtual void Drive() override { 
        cout << "Benz-舒适" << endl; 
    }
};
int main() 
{
    Car* p = new Benz;
    p->Drive(); // 多态调用:输出“Benz-舒适”
    return 0;
}

4.2 final:禁止重写

在基类虚函数后加final,表示该虚函数不允许任何派生类重写。若派生类强行重写,编译报错。
代码示例(注意看注释):

class Car 
{
public:
    // 基类虚函数加final:禁止派生类重写
    virtual void Drive() final 
    {
        cout << "Car-行驶" << endl;
    }
};
class Benz : public Car 
{
public:
    // 错误:Drive()被final修饰,无法重写,编译报错
    // virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main() 
{
    return 0;
}

五. 易混淆概念:重载、重写、隐藏的对比(常考)

多态相关的三个概念 (重载、重写(覆盖)、隐藏(重定义)) 极易混淆,我们通过下面的图片,代码示例和表格来加强一下对它们的区分:

代码示例(借鉴-注意看注释):

class Base 
{
public:
    // 1. 重载:同一作用域,函数名相同,参数不同
    void func(int a) { cout << "Base::func(int)" << endl; }
    void func(double b) { cout << "Base::func(double)" << endl; }
    // 虚函数:用于重写
    virtual void show() { cout << "Base::show()" << endl; }
};
class Derive : public Base 
{
public:
    // 2. 重写:基类与派生类,虚函数+函数名/参数/返回值相同
    virtual void show() override { cout << "Derive::show()" << endl; }
    // 3. 隐藏:基类与派生类,函数名相同但不构成重写
    void func(int a, int b) { cout << "Derive::func(int,int)" << endl; }
};
int main() 
{
    Derive d;
    d.func(1, 2);    // 调用Derive::func(隐藏基类func)
    // d.func(3);    // 编译报错:基类func(int)被隐藏,需显式调用Base::func(3)
    Base* p = &d;
    p->show();       // 多态调用:Derive::show(重写)
    return 0;
}
特性重载(Overload)重写(Override)隐藏(Hide)
作用域同一类(同一作用域)基类与派生类(不同作用域)基类与派生类(不同作用域)
函数名必须相同必须相同必须相同
参数列表必须不同(类型/个数/顺序)必须相同可相同可不同
返回值类型无要求必须相同(协变除外)无要求
虚函数要求无必须都是虚函数无
核心场景同一类中同名函数的不同实现多态的核心,动态绑定派生类屏蔽基类同名成员(非重写)
底层机制编译期静态绑定,通过参数列表区分函数运行期动态绑定,依赖虚函数表编译期静态绑定,通过作用域区分
示例class A { void func(int); void func(double); }class A { virtual void func(); }; class B : public A { void func() override; }class A { void func(); }; class B : public A { void func(int); }
注意事项仅在同一类中生效,派生类中若与基类函数同名且参数不同,会隐藏基类函数重写时函数签名(函数名+参数+返回值)必须严格一致,析构函数重写有特殊性若派生类函数与基类虚函数同名但参数不同,会隐藏基类虚函数,导致多态失效

结尾:

结语:C++ 多态的本质,是用 “统一接口” 包裹 “差异化实现”,让代码既能保持调用逻辑的一致性,又能适配不同对象的特性 —— 从 “买票” 的场景差异,到析构函数的资源安全释放,多态始终在平衡 “通用性” 与 “灵活性”。

到此这篇关于C++ 多态深入解析:从概念本质、语法规则到底层实现(结合实战代码的全方位指南)的文章就介绍到这了,更多相关C++ 多态语法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C++实现团购订单管理系统

    C++实现团购订单管理系统

    这篇文章主要为大家详细介绍了如何利用C++实现团购订单管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-12-12
  • 详解C++设计模式编程中对访问者模式的运用

    详解C++设计模式编程中对访问者模式的运用

    这篇文章主要介绍了C++设计模式编程中对访问者模式的运用,访问者模式在不破坏类的前提下为类提供增加新的新操作,需要的朋友可以参考下
    2016-03-03
  • C语言数据结构递归之斐波那契数列

    C语言数据结构递归之斐波那契数列

    这篇文章主要介绍了C语言数据结构递归之斐波那契数列的相关资料,希望通过本文能帮助到大家,让大家理解掌握这部分内容,需要的朋友可以参考下
    2017-10-10
  • C++使用一棵红黑树同时封装出map和set实例代码

    C++使用一棵红黑树同时封装出map和set实例代码

    红黑树(Red Black Tre)是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组,下面这篇文章主要给大家介绍了关于C++使用一棵红黑树同时封装出map和set的相关资料,需要的朋友可以参考下
    2023-04-04
  • c++中new一个结构体初始化过程

    c++中new一个结构体初始化过程

    这篇文章主要介绍了c++中new一个结构体初始化过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-08-08
  • C语言超细致讲解函数递归

    C语言超细致讲解函数递归

    程序调⽤⾃⾝的编程技巧称为递归 recursion)函数⾃⼰调⽤⾃⼰就是递归,你也可以理解成是⼀种嵌套结构,但递归分为俩部分,第⼀是“递”,进⼊嵌套结构。第⼆是”归“,最终会⼀步⼀步返回。第⼀次接触递归都会很懵,慢慢理解这个过程就明⽩了
    2022-05-05
  • C++ 红黑树从零实现:旋转、插入、删除与平衡

    C++ 红黑树从零实现:旋转、插入、删除与平衡

    红黑树是C++里 <u>std::map</u>、<u>std::set</u> 等关联容器的底层实现,核心就是"‌变色 + 旋转‌"来维持平衡,本文介绍C++ 红黑树从零实现:旋转、插入、删除与平衡,感兴趣的朋友一起看看吧
    2026-09-09
  • C++中std::string::npos的用法

    C++中std::string::npos的用法

    这篇文章主要介绍了C++中std::string::npos的用法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-08-08
  • C++ primer基础之容器insert

    C++ primer基础之容器insert

    这篇文章主要介绍了C++ primer基础之容器insert的相关资料,需要的朋友可以参考下
    2017-02-02
  • C++下程序运行时间的四种常用计时方法总结

    C++下程序运行时间的四种常用计时方法总结

    这篇文章主要介绍了C++下程序运行时间的四种常用计时方法,介绍了几种常用的计时方法,包括低精度的clock()和GetTickCount(),以及高精度的gettimeofday()和QueryPerformanceCounter(),需要的朋友可以参考下
    2024-09-09

最新评论