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

C++---多态(一个接口多种实现)

C++的多态(Polymorphism)是面向对象编程(OOP)的三大核心特性之一(另外两个是封装和继承),其核心思想是一个接口,多种实现,即同一操作作用于不同对象时,可产生不同的执行结果。多态让代码更灵活、可扩展,是构建复杂系统的重要工具。

一、多态的分类

C++的多态分为两类:静态多态(编译时多态)和动态多态(运行时多态),二者的核心区别在于“确定调用哪个函数的时机”——前者在编译期确定,后者在运行期确定。

在这里插入图片描述

1. 静态多态(编译时多态)

静态多态是通过函数重载运算符重载实现的,编译器在编译阶段根据函数的参数列表(类型、数量、顺序)或运算符的操作数类型,确定具体要调用的函数。

示例:函数重载实现静态多态

#include <iostream>
using namespace std;// 重载:同一作用域内,函数名相同,参数列表不同
int add(int a, int b) {return a + b;
}double add(double a, double b) {  // 参数类型不同return a + b;
}int add(int a, int b, int c) {  // 参数数量不同return a + b + c;
}int main() {cout << add(1, 2) << endl;       // 调用int add(int, int)cout << add(1.5, 2.5) << endl;   // 调用double add(double, double)cout << add(1, 2, 3) << endl;    // 调用int add(int, int, int)return 0;
}

编译器在编译时会根据实参的类型和数量,自动匹配到对应的重载函数,这就是静态多态的体现。

2. 动态多态(运行时多态)

动态多态是C++多态的核心,它通过继承+虚函数实现,函数的具体调用在程序运行时才确定,而非编译时。这种机制让基类的指针/引用可以灵活指向不同派生类对象,并调用对应派生类的实现。

核心条件

  • 必须存在继承关系(基类与派生类);
  • 基类中声明虚函数(用virtual关键字修饰);
  • 派生类重写(override)基类的虚函数(函数名、参数列表、返回值必须完全一致,协变返回类型除外);
  • 通过基类的指针或引用调用虚函数。

二、动态多态的实现原理

动态多态的核心是虚函数表(vtable)虚指针(vptr),这是编译器在背后自动实现的机制

1. 虚函数表(vtable)
  • 当一个类中声明了虚函数(或继承了虚函数),编译器会为该类生成一个虚函数表(本质是一个函数指针数组),存储该类所有虚函数的地址。
  • 若派生类重写了基类的虚函数,派生类的虚函数表中会用自己的函数地址覆盖基类对应虚函数的地址;未重写的虚函数,地址仍指向基类的实现。
2. 虚指针(vptr)
  • 每个含有虚函数的类的对象,都会隐含一个虚指针(vptr),指向该类的虚函数表(vtable)。
  • 当通过基类指针/引用调用虚函数时,程序会通过对象的vptr找到对应的vtable,再从vtable中取出函数地址并调用,这个过程在运行时完成(动态绑定)。

示例:动态多态的直观体现

#include <iostream>
using namespace std;// 基类:形状
class Shape {
public:// 虚函数:绘制virtual void draw() {  // 用virtual声明为虚函数cout << "绘制基础形状" << endl;}// 虚析构函数(避免内存泄漏)virtual ~Shape() {}
};// 派生类:圆形(继承Shape)
class Circle : public Shape {
public:// 重写基类的draw()void draw() override {  // override关键字显式声明重写(C++11)cout << "绘制圆形" << endl;}
};// 派生类:矩形(继承Shape)
class Rectangle : public Shape {
public:// 重写基类的draw()void draw() override {cout << "绘制矩形" << endl;}
};// 统一接口:接收基类引用,调用draw()
void render(Shape& shape) {shape.draw();  // 运行时根据实际对象类型,调用对应draw()
}int main() {Circle circle;Rectangle rectangle;render(circle);    // 输出:绘制圆形(实际是Circle对象)render(rectangle); // 输出:绘制矩形(实际是Rectangle对象)// 基类指针指向派生类对象Shape* shape1 = new Circle();Shape* shape2 = new Rectangle();shape1->draw();  // 输出:绘制圆形shape2->draw();  // 输出:绘制矩形delete shape1;   // 虚析构函数确保派生类析构被调用delete shape2;return 0;
}

运行机制解析

  • Shape类有虚函数draw(),编译器为其生成vtable,存储Shape::draw()的地址。
  • CircleRectangle继承Shape重写draw(),它们的vtable中,draw()的地址被替换为各自的实现(Circle::draw()Rectangle::draw())。
  • render函数接收CircleRectangle对象的引用时(本质是基类引用指向派生类对象),调用draw()时会通过对象的vptr找到对应vtable,最终执行派生类的实现——这就是运行时多态。

三、重写(Override)的细节

派生类重写基类虚函数时,必须满足以下条件(否则可能变成“隐藏”而非“重写”):

  1. 函数名、参数列表完全相同:参数的类型、数量、顺序必须一致(若参数不同,会变成派生类的新函数,隐藏基类函数)。
  2. 返回值类型相同:除非是“协变返回类型”(即基类虚函数返回基类指针/引用,派生类重写函数返回派生类指针/引用)。
    class Base {};
    class Derived : public Base {};class A {
    public:virtual Base* func() { return new Base(); }  // 基类返回Base*
    };class B : public A {
    public:Derived* func() override { return new Derived(); }  // 派生类返回Derived*(协变)
    };
    
  3. 基类函数必须是虚函数:若基类函数未用virtual修饰,派生类即使同名同参,也只是“隐藏”基类函数,而非重写(无法触发多态)。
  4. 访问权限不影响多态:即使派生类重写的函数是private,通过基类指针/引用调用时仍能正常触发(因为访问权限检查在编译期,多态调用在运行期)。

四、纯虚函数与抽象类

为了强制派生类必须实现某些功能(如“所有形状都必须能绘制”),C++引入纯虚函数抽象类

  • 纯虚函数:在虚函数声明后加=0,表示该函数没有默认实现,必须由派生类重写。
  • 抽象类:包含纯虚函数的类(或继承纯虚函数且未重写的类),不能实例化对象,只能作为基类被继承。

示例:抽象类与纯虚函数

class Shape {
public:// 纯虚函数:强制派生类实现draw()virtual void draw() = 0;  // =0表示纯虚函数virtual ~Shape() {}  // 抽象类也需要虚析构
};class Circle : public Shape {
public:void draw() override {  // 必须重写,否则Circle也是抽象类cout << "绘制圆形" << endl;}
};int main() {// Shape s;  // 错误:抽象类不能实例化Shape* shape = new Circle();  // 正确:基类指针指向派生类对象shape->draw();  // 输出:绘制圆形delete shape;return 0;
}

抽象类的核心作用是定义“接口规范”,确保派生类遵循统一的行为契约(如Shape规定“必须能绘制”,所有派生类都必须实现draw())。

五、多态的应用与优势

  1. 提高代码复用性:通过基类接口统一处理不同派生类对象(如render函数无需为每个形状单独实现)。
  2. 增强扩展性:新增派生类(如Triangle)时,无需修改现有接口代码(如render),只需实现draw()即可,符合“开闭原则”(对扩展开放,对修改关闭)。
  3. 模拟现实世界的多样性:现实中同一行为(如“绘制”)作用于不同对象(圆、矩形)会有不同结果,多态完美映射这种关系。

六、注意事项

  1. 析构函数建议声明为虚函数:当通过基类指针删除派生类对象时,若基类析构不是虚函数,会只调用基类析构而不调用派生类析构,导致内存泄漏。

    class Base {
    public:~Base() { cout << "Base析构" << endl; }  // 非虚析构(危险)
    };class Derived : public Base {
    public:~Derived() { cout << "Derived析构" << endl; }
    };int main() {Base* p = new Derived();delete p;  // 仅输出"Base析构",Derived析构未调用(内存泄漏)return 0;
    }
    

    解决:将基类析构声明为virtual ~Base() {},确保派生类析构被调用。

  2. 避免在构造/析构函数中调用虚函数:构造派生类对象时,先调用基类构造函数,此时对象的动态类型仍为基类,调用虚函数会执行基类版本;析构时同理,可能导致不符合预期的结果。

  3. 虚函数表的开销:每个含虚函数的类会增加vtable(静态开销),每个对象会增加vptr(动态内存开销,通常为4/8字节),但相比多态带来的灵活性,这种开销通常可接受。


C++的多态通过“静态多态(重载)”和“动态多态(虚函数)”实现,其中动态多态是核心,依赖虚函数表和虚指针实现运行时绑定。它让代码更灵活、可扩展,是构建大型面向对象系统的基础。

http://www.xdnf.cn/news/18614.html

相关文章:

  • YOLO算法:实时目标检测核心技术解析
  • CMake进阶:Ninja环境搭建与加速项目构建
  • UVa1472/LA4980 Hanging Hats
  • webpack开发模式与生产模式(webpack --mode=development/production“, )
  • ubuntu使用fstab挂载USB设备(移动硬盘)
  • Jenkins用户授权管理 企业级jenkins授权策略 jenkins用户权限分配
  • 【go语言】使用Wails开发一款现代化文本编辑器 - 从0到1的实践指南
  • 机器学习之线性回归:原理、实现与实践
  • 动态代理保姆级别
  • 移动应用青少年模式开发成本解析:原生、Flutter与Uniapp方案对比-优雅草卓伊凡
  • Slither 审计自己写的智能合约
  • MySQL InnoDB记录存储结构深度解析
  • 服务发现实例和服务实例是不同的
  • reactive 核心要点
  • Unreal Engine UPrimitiveComponent
  • 数据分析编程第二步: 最简单的数据分析尝试
  • day58 拓扑排序 (kama117. 软件构建) dijkstra(朴素版)(kama47. 参加科学大会)
  • 无人机电机与螺旋桨的匹配原理及方法(一)
  • JavaSSM框架从入门到精通!第三天(MyBatis(二))!
  • Python训练营打卡Day40-简单CNN
  • 【51单片机数码管字符左移】2022-11-11
  • 如何低门槛自制Zigbee 3.0温湿度计?涂鸦上新开发包,开箱即用、完全开源
  • 开源AI编程工具Kilo Code的深度分析:与Cline和Roo Code的全面对比
  • Tiger任务管理系统-13
  • 【jar包启动,每天生成一个日志文件】
  • Unity UnityWebRequest高级操作
  • Ubuntu部署K8S集群
  • Jmeter+Jenkins接口压力测试持续集成
  • 【motion】基于标签重合度的匹配算法1:原理
  • 3D打印小批量低成本打印玩具工艺品模型-中科米堆CASAIM