当前位置: 首页 > news >正文

【C++】:多态

目录

多态的概念

多态的实现

协变

析构函数的重写

override和final(C++11)

override 关键字

final 关键字

重载、重写、重定义的区别

抽象类(纯虚函数)

多态的原理

虚函数表(vtable)

虚指针(vptr)

多态的实现步骤

动态绑定和静态绑定


多态的概念

多态就是函数调用的多种形态,使用多态能够使得不同的对象去完成同一件事时,产生不同的动作和结果。

多态是 C++ 实现「一个接口,多种实现」的核心机制。就像现实中的多功能工具(如瑞士军刀)通过统一的手柄适配不同功能模块,多态让代码灵活适应变化,是构建大型可扩展系统的基石。

支付系统 

class Payment 
{
public:
    virtual void process(double amount) = 0;
};

class Alipay : public Payment 
{
    void process(double amount) override { /* 支付宝支付逻辑 */ }
};

class WeChatPay : public Payment 
{
    void process(double amount) override { /* 微信支付逻辑 */ }
};

// 用户选择支付方式时,无需修改核心代码
Payment* payment = new Alipay();
payment->process(100.0);

多态的实现

多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。在继承中要想构成多态需要满足构成运行时多态的四大条件:

  1. 必须存在类继承体系
  2. 基类中必须用 virtual 关键字声明虚函数
  3. 派生类必须重写(override)基类虚函数
  4. 必须通过基类的指针或者引用调用虚函数

一、虚函数

virtual修饰的类成员函数被称为虚函数。

class Shape {
public:
    virtual void draw() { /* 虚函数 */ } 
};
class Circle : public Shape {
public:
    void draw() { /*  */ }
};
  1. 只有类的非静态成员函数前可以加virtual,普通函数前不能加virtual
  2. 虚函数这里的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有任何关系。
    1. 虚函数这里的virtual是为了实现多态
    2. 而虚继承的virtual是为了解决菱形继承的数据冗余和二义性

二、重写

虚函数的重写也叫做虚函数的覆盖,派生类的虚函数重写了基类的虚函数的条件:

  1. 若派生类中有一个和基类完全相同的虚函数
  2. 返回值类型相同
  3. 函数名相同
  4. 参数列表相同
class Animal 
{
public:
    virtual void speak() { cout << "Animal sound"; }
};
class Cat : public Animal {
public:
    void speak() override 
    { 
        // 正确重写
        cout << "Meow"; 
    }
};

三、基类指针/引用调用 

错误示范

class Animal 
{
public:
    virtual void speak() { cout << "Animal sound"; }
};
class Cat : public Animal {
public:
    void speak() override 
    { 
        // 正确重写
        cout << "Meow"; 
    }
};
Cat cat;
cat.speak(); // 直接调用,静态绑定
Animal animal = cat; // 对象切片
animal.speak(); // 调用 Animal::speak()

正确调用 

// 条件1:继承关系
class Animal 
{
public:
    // 条件2:虚函数声明
    virtual void speak() 
    {
        cout << "Animal sound\n";
    }
    virtual ~Animal() = default; // 虚析构函数
};

class Dog : public Animal 
{ // 继承
public:
    // 条件3:派生类重写
    void speak() override 
    {
        cout << "Woof!\n";
    }
};

int main() 
{
    // 条件4:基类指针调用
    Animal* animal = new Dog();
    animal->speak(); // 输出 Woof!
    delete animal;
}

协变

协变是虚函数重写的例外,一个特殊例子

  • 返回值协变:允许派生类虚函数的返回类型是基类虚函数返回类型的 子类类型

  • 类型方向:与继承方向一致(协变 = 共同变化)

class Base { /*...*/ };
class Derived : public Base { /*...*/ };

class A 
{
public:
    virtual Base* create(); // 基类返回 Base*
};

class B : public A 
{
public:
    Derived* create() override; // 协变:返回更具体的 Derived*
};

协变的实现条件

条件说明违反示例
1. 继承关系派生类返回类型必须从基类返回类型派生Base* vs Unrelated*
2. 虚函数重写必须使用 override 关键字明确重写缺少 override 导致隐藏
3. 类型兼容性必须为指针或引用类型(值类型会导致对象切片)Base vs Derived(值类型)
4. 函数签名一致性参数列表必须严格相同参数不同导致函数隐藏

注意:值类型不适用协变 ,会发生对象切片

析构函数的重写

确保通过基类指针删除派生类对象时,正确调用派生类的析构函数。

错误示范:虚构函数未重写 (内存泄露)

class Base 
{
public:
    ~Base() { cout << "释放基类" << endl; } // 错误!
};

class Derived : public Base {
    int* data;
public:
    Derived() { data = new int[100]; }
    ~Derived() 
    { 
        delete[] data;
        cout << "派生类" << endl;
    } // 不会被调用!
};

int main() 
{
    Base* obj1 = new Base();
    Base* obj2 = new Derived();

    delete obj1;
    delete obj2; // 仅调用 Base::~Base() → 内存泄漏!
    // 释放基类
}

正确示范:基类析构函数需要重写 

class Base 
{
public:
    virtual ~Base() { cout << "释放基类" << endl; } // 错误!
};

class Derived : public Base 
{
    int* data;
public:
    Derived() { data = new int[100]; }
    ~Derived() 
    { 
        delete[] data;
        cout << "释放派生类" << endl;
    } // 不会被调用!
};

int main() 
{
    Base* obj1 = new Base();
    Base* obj2 = new Derived();

    delete obj1;
    delete obj2; // 仅调用 Base::~Base() → 内存泄漏!
    // 释放派生类
    // 释放基类
}

析构函数的重写要点 

  1. 隐式虚继承:一旦基类析构函数为虚,所有派生类析构函数自动成为虚函数,无需显式声明

  2. 析构函数调用顺序

    1. 销毁顺序:派生类析构 → 成员对象析构 → 基类析构

    2. 无需显式调用基类析构函数(编译器自动处理)

在继承当中,子类和的析构函数和父类的析构函数构成隐藏的原因就在这里,这里表面上看子类的析构函数和父类的析构函数的函数名不同,但是为了构成重写,编译后析构函数的名字会被统一处理成 destructor();

override和final(C++11)

override 关键字

作用

  • 显式标记重写:明确指示派生类中的函数旨在重写基类的虚函数。

  • 编译时检查:编译器会验证派生类函数是否确实重写了基类的虚函数(函数签名一致),避免因签名不匹配导致的隐藏而非重写。

使用场景

  • 当派生类重写基类的虚函数时,建议始终使用override

  • 避免因参数类型、常量性或引用限定符不匹配导致的意外行为。

class Base 
{
public:
    virtual void func(int x) const;
};

class Derived : public Base 
{
public:
    void func(int x) const override; // 正确:签名一致
    // void func(double x) override; // 错误:参数类型不匹配,编译报错
};

注意事项

  • 只能用于虚函数重写,不可用于非虚函数。

  • 若基类没有对应的虚函数,或签名不一致,编译器会报错。

final 关键字

作用

  • 禁止继承:修饰类时,表示该类不可被继承。

  • 禁止重写:修饰虚函数时,表示该函数在派生类中不可被进一步重写。

使用场景

  • 类设计:工具类、单例类或核心组件类,确保其行为不被继承破坏。

  • 虚函数设计:关键接口或算法,防止派生类修改核心逻辑。

修饰类(禁止继承)

class Base final 
{ 
    // ...
};

// class Derived : public Base {}; // 错误:Base 被声明为 final

修饰虚函数(禁止重写) 

class Base 
{
public:
    virtual void func() final; // 此虚函数不可被重写
};

class Derived : public Base 
{
public:
    // void func() override; // 错误:func 是 final 的
};

注意事项

  • final可同时与override联用,表示重写后禁止进一步修改。「重写后禁止进一步重写」 指的是当一个虚函数在某个派生类中被重写后,可以通过 final 关键字明确禁止后续更下层的派生类再次重写该方法。这种设计可以 冻结继承链中某层的行为,确保从该层开始的所有子类都使用当前版本的函数实现。

  • class Derived : public Base {
    public:
        void func() override final; // 先重写,再禁止后续派生类修改
    };
  • 不可用于非虚函数,否则编译错误。

重载、重写、重定义的区别

特性重载(Overloading)重写(Overriding)重定义(Redefining)
作用域同一作用域(类内/命名空间内)继承体系(基类→派生类)继承体系(基类→派生类)
函数关系同名不同参同名同参,基类虚函数同名(参数可不同),基类非虚函数
多态性编译时多态(静态绑定)运行时多态(动态绑定)无多态(静态绑定)
关键字要求virtual + override
常见用途提供同一操作的多种实现实现多态接口修改基类非虚函数行为

抽象类(纯虚函数)

抽象类(Abstract Class) 是通过包含 纯虚函数(Pure Virtual Function) 定义的类,其核心目的是为派生类提供统一的接口规范。

在虚函数的后面写上 =0 ,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

//抽象类(接口类)
class Car
{
public:
	//纯虚函数
	virtual void Drive() = 0;
};

Car c; //抽象类不能实例化出对象
	

派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写

// 抽象类(接口类)
class Car
{
public:
	// 纯虚函数
	virtual void Drive() = 0;
};
// 派生类
class Benci : public Car
{
public:
	//重写纯虚函数
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
// 派生类
class BMW : public Car
{
public:
	//重写纯虚函数
	virtual void Drive()
	{
		cout << "BMV-操控" << endl;
	}
};

// 不同对象用基类指针调用Drive函数,完成不同的行为
Car* p1 = new Benci();
Car* p2 = new BMW();
p1->Drive();  // Benci-舒适
p2->Drive();  // BMW-操控

实现继承: 普通函数的继承是一种实现继承,派生类继承了基类函数的实现,可以使用该函数。

接口继承: 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。

建议:如果不实现多态,就不要把函数定义成虚函数。

多态的原理

多态(Polymorphism) 的核心原理基于 虚函数表(vtable) 和 虚指针(vptr) 的机制,通过 动态绑定(Dynamic Binding) 实现运行时多态。

我们来做一个简单的实验 

Base类实例化出对象的大小是多少? 

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

Base a;
sizeof(a); // 16

 我们发现Base对象只有一个成员变量(_b),但是Base对象的大小却是16

我么启动监视窗口,a对象多出来一个 _vfptr指针

这个指针叫做虚函数表指针,简称虚表指针,虚表指针指向一个虚函数表,简称虚表,每一个含有虚函数的类中都至少有一个虚表指针。

虚函数表(vtable)

  • 定义:每个包含虚函数的类都有一个隐式生成的虚函数表(编译期创建)。

  • 存储内容

    虚表条目说明
    虚函数地址按声明顺序存储类的所有虚函数地址
    RTTI 信息指针指向类型信息(用于 typeid 和 dynamic_cast
  • 内存位置:通常位于程序的 只读数据段(.rodata)

虚函数表存储的内容

class A
{
public:
	virtual void Func1(){ cout << "A::Func1()" << endl;}
	virtual void Func2(){ cout << "A::Func2()" << endl;}
	void Func3(){ cout << "A::Func3()" << endl;}
private:
	int _a = 1;
};
class B : public A
{
public:
	virtual void Func1(){ cout << "B::Func1()" << endl;}
private:
	int _b = 1;
};
 
A* a = new A();
a->Func1();

A* b = new B();
b->Func1();

父类对象a和子类对象b当中除了自己的成员变量之外,父类和子类对象都有一个虚表指针,分别指向属于自己的虚表。

虚指针(vptr)

  • 定义:每个对象实例内部隐式包含一个指向其所属类 vtable 的指针。

  • 内存布局:通常位于对象内存布局的 起始位置

  • 初始化时机

    • 对象构造时,由构造函数自动设置 vptr 指向正确的 vtable。

    • 派生类对象的 vptr 在构造过程中会被多次修改(基类→派生类)。

多态的实现步骤

当通过基类指针调用虚函数时:

A* a = new A();
a->Func1();    // A::Func1()

A* b = new B();
b->Func1();    // B::Func1()

底层执行步骤

  1. 获取 vptr:通过对象地址访问其 vptr(mov rax, [obj])。

  2. 查找虚表:根据 vptr 找到对应的虚函数表。

  3. 计算偏移:确定目标虚函数在虚表中的位置(如 func1 是第一个条目,偏移 0)。

  4. 调用函数:通过函数地址跳转执行

 错误示范:

使用父类对象调用虚函数时:

A a1;
B b1;

A ptr1 = a1; // 拷贝构造
ptr1.Func1();    // A::Func1() 

A ptr2 = b1;
ptr2.Func1();    // A::Func1()

对象切片得到部分成员变量后,会调用父类的拷贝构造函数对那部分成员变量进行拷贝构造,而拷贝构造出来的父类对象p1和p2当中的虚表指针指向的都是父类对象的虚表。因为同类型的对象共享一张虚表,他们的虚表指针指向的虚表是一样的。

因此,我们后序用 ptr1 和 ptr2 调用虚函数时,ptr1 和 ptr2 通过虚表指针找到的虚表是一样的,最终调用的函数也是一样的,也就无法构成多态。

总结一下:

  1. 构成多态,指向谁就调用谁的虚函数,跟对象有关。
  2. 不构成多态,对象类型是什么就调用谁的虚函数,跟类型有关。

从编译到运行的完整流程

编译阶段

  • 虚表构建:编译器为每个含虚函数的类生成唯一的虚表。

  • vptr 插入:在对象布局中插入虚指针。

  • 虚函数调用转换:将 obj->func() 编译为通过 vptr 和虚表的间接调用。

运行时阶段

  • 对象构造:构造函数设置 vptr 指向当前类的虚表,虚表实际上是在构造函数初始化列表阶段进行初始化的

  • 动态绑定:通过虚指针查找虚表,确定实际调用的函数地址。

  • 析构过程:析构函数反向调整 vptr(从派生类到基类)。

操作普通函数调用虚函数调用
寻址方式直接地址调用两次间接寻址(vptr → vtable → 函数)
汇编指令call <固定地址>call [寄存器+偏移]
性能影响无额外开销多约 10-20% 的指令周期

动态绑定和静态绑定

静态绑定: 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,比如:函数重载。

动态绑定: 动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

相关文章:

  • redis的淘汰策略
  • Linux15-epoll、数据库
  • k8s概念及k8s集群部署(Centos7)
  • 基于 Python 实现问卷数据分析的详细示例
  • AI编程工具节选
  • 【商城实战(11)】解锁商品搜索与筛选功能,提升用户购物体验
  • 数据结构与算法(两两交换链表中的结点)
  • 鬼泣:动画2
  • 桂链:区块链模型介绍
  • 【贪心算法2】
  • Manus详细介绍,Manus核心能力介绍
  • go map的声明和使用
  • windows 平台如何点击网页上的url ,会打开远程桌面连接服务器
  • 学校地摊尝试实验
  • 《Python基础教程》第2-4章笔记:列表和元组、字符串、字典
  • 数据结构基础(一)
  • DeepSeek × 豆包深度整合指南:工作流全解析
  • 专业学习|多线程、多进程、多协程加速程序运行
  • 08react基础-react原理
  • 【js逆向】图灵爬虫练习平台 第十五题
  • 推荐一些做网站网络公司/2022最新免费的推广引流软件
  • 成都大型网站建设公司/爱站网备案查询
  • 网新科技做网站怎么样/培训计划模板
  • 保定企业网站建设/黑帽seo培训
  • php网站管理系统下载/app营销策略有哪些
  • 彩票网站开发系统/域名注册 万网