C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++多态(一个接口多种实现)

C++之多态(一个接口多种实现方式)

作者:MzKyle

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

核心条件

二、动态多态的实现原理

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

1. 虚函数表(vtable)

2. 虚指针(vptr)

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

#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;
}

运行机制解析

三、重写(Override)的细节

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

函数名、参数列表完全相同:参数的类型、数量、顺序必须一致(若参数不同,会变成派生类的新函数,隐藏基类函数)。

返回值类型相同:除非是“协变返回类型”(即基类虚函数返回基类指针/引用,派生类重写函数返回派生类指针/引用)。

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*(协变)
};

基类函数必须是虚函数:若基类函数未用virtual修饰,派生类即使同名同参,也只是“隐藏”基类函数,而非重写(无法触发多态)。

访问权限不影响多态:即使派生类重写的函数是private,通过基类指针/引用调用时仍能正常触发(因为访问权限检查在编译期,多态调用在运行期)。

四、纯虚函数与抽象类

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

示例:抽象类与纯虚函数

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. 模拟现实世界的多样性:现实中同一行为(如“绘制”)作用于不同对象(圆、矩形)会有不同结果,多态完美映射这种关系。

六、注意事项

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

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() {},确保派生类析构被调用。

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

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

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

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文