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

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

一、多态的分类

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

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

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

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

cpp 复制代码
#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中取出函数地址并调用,这个过程在运行时完成(动态绑定)。

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

cpp 复制代码
#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. 返回值类型相同 :除非是"协变返回类型"(即基类虚函数返回基类指针/引用,派生类重写函数返回派生类指针/引用)。

    cpp 复制代码
    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,表示该函数没有默认实现,必须由派生类重写。
  • 抽象类 :包含纯虚函数的类(或继承纯虚函数且未重写的类),不能实例化对象,只能作为基类被继承。

示例:抽象类与纯虚函数

cpp 复制代码
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. 析构函数建议声明为虚函数:当通过基类指针删除派生类对象时,若基类析构不是虚函数,会只调用基类析构而不调用派生类析构,导致内存泄漏。

    cpp 复制代码
    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++的多态通过"静态多态(重载)"和"动态多态(虚函数)"实现,其中动态多态是核心,依赖虚函数表和虚指针实现运行时绑定。它让代码更灵活、可扩展,是构建大型面向对象系统的基础。

相关推荐
_码农121387 分钟前
模拟tomcat接收GET、POST请求
java·tomcat
KeithTsui20 分钟前
GCC C语言整数转换的理解(Understanding of Integer Conversions in C with GCC)
c语言·开发语言·算法
秉承初心21 分钟前
Node.js 开发 JavaScript SDK 包的完整指南(AI)
开发语言·javascript·node.js
jiunian_cn26 分钟前
【Linux】线程
android·linux·运维·c语言·c++·后端
板板正1 小时前
SpringAI——向量存储(vector store)
java·spring boot·ai
野生技术架构师1 小时前
Spring Boot 定时任务与 xxl-job 灵活切换方案
java·spring boot·后端
云天徽上2 小时前
【数据可视化-96】使用 Pyecharts 绘制主题河流图(ThemeRiver):步骤与数据组织形式
开发语言·python·信息可视化·数据分析·pyecharts
jdlxx_dongfangxing2 小时前
C++ 序列式容器深度解析:vector、string、deque 与 list
c++·stl
苹果醋32 小时前
Java并发编程-Java内存模型(JMM)
java·运维·spring boot·mysql·nginx
你怎么知道我是队长2 小时前
C语言---编译的最小单位---令牌(Token)
java·c语言·前端