每日一个C++知识点|面向对象之多态

37 阅读6分钟

C++面向对象的三大特性是封装,继承,多态。上两篇文章分别讨论了封装和继承,今天主要是讲解C++的另一个面向对象的特性~~多态

多态的概念

什么是多态呢?
多态的核心是"同一个接口,不同的实现"
简单来说,就是调用同一个函数名,程序会根据上下文和调用对象的实际类型来自动执行对应的函数逻辑,后面我们将会用代码来举例说明

多态的分类

C++多态分为静态多态动态多态

静态多态

静态多态是编译时多态,编译器在编译阶段就确定要调用的函数版本,主要通过函数重载、模板实现,用代码举例如下:

#include <iostream>
using namespace std;

// 重载1:计算两个整数的和
int add(int a,int b) {
    return a + b;
}

// 重载2:计算三个浮点数的和
double add(double a,double b,double c) {
    return a + b + c;
}

int main() {
    cout << add(12) << endl;// 调用int版本,输出3
    cout << add(1.12.23.3) << endl; // 调用double版本,输出6.6
    return 0;
}

这里编译器根据参数的个数、类型,在编译时就确定了要调用的add()函数

动态多态

动态多态是运行时多态,程序在运行阶段才确定要调用的函数版本

动态多态的实现需要以下三个条件

  1. 存在继承关系
  2. 父类声明虚函数,子类重写该虚函数
  3. 使用父类的指针指向子类对象

由于还没有说到虚函数,暂时先不用代码举例,等说完虚函数再一并举例

虚函数

上面说到虚函数是动态多态实现的必要条件之一,那么什么是虚函数呢?

普通虚函数

虚函数是在父类中用virtual关键字修饰的成员函数,调用时要根据对象的实际类型来动态绑定,而不是编译时固定绑定

// 父类:图形
class Shape {
public:
    // 虚函数:绘制图形
    virtual void draw() {
        cout << "绘制基础图形" << endl;
    }
    double getArea(){
        return 1;    
    }
};

上述代码中,用virtual关键字修饰的draw()函数就是虚函数,调用时要根据对象的实际类型来决定其内容,假如它的子类如下所示:

// 子类:圆形
class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}

    // 重写父类的虚函数(override关键字可选,但建议加)
    void draw() override {
        cout << "绘制半径为" << radius << "的圆形" << endl;
     }   
    double getArea(){
        return M_PI * radius * radius;
    } 
};

上述代码中子类Circle的成员函数draw()有关键字override来修饰,是对父类虚函数draw()的重写,假如调用情况如下所示:

int main() {
    // 父类指针指向子类对象(多态的关键)
    Shape* shape = new Circle(5.0);

    // 调用的是子类Circle的draw和getArea(动态绑定)
    shape->draw();       // 输出:绘制半径为5的圆形
    cout << "面积:" << shape->getArea() << endl; // 输出:面积:78.5398

    delete shape;
    return 0;
}

由于实际的对象由Shape* shape = new Circle(5.0);决定,所以draw()函数是执行Circle类的内容而不是父类Shape的内容

这就是虚函数的内容,也是动态多态的内容(满足动态多态的三个条件)

虚函数的实现原理

我们已经知道了虚函数的用法,那么虚函数是怎么实现动态多态的呢?下面我们简单了解其原理

编译器为了实现虚函数的动态绑定做了两件事:分别是创建虚函数表添加虚表指针

虚函数表是每个包含虚函数的类(包括父类和子类)都会有一个独立的虚函数表,表中存储了该类所有虚函数的地址。上述代码中Shape类和Circle类都有各自的虚函数表,表中存储虚函数draw()的地址

虚表指针是每个对象会包含一个隐藏的虚表指针,指向所属类的虚函数表。上述代码Shape* shape = new Circle(5.0);中的对象shape存在一个虚函数指针,指向Circle类里的虚函数表里的draw()的地址

当程序运行时,通过对象的虚表指针找到对应的虚函数表,再调用表中的函数地址,从而实现 “根据对象实际类型调用函数”

纯虚函数

上述虚函数的例子是有实际意义的,有时候父类的虚函数没有实现意义,这时可以定义纯虚函数,格式是在函数后加 = 0

class Shape {
public:
    // 纯虚函数:没有函数体,强制子类必须重写
    virtual double getArea() = 0;
    // 普通虚函数可以有默认实现
    virtual void draw() {
        std::cout << "绘制基础图形" << std::endl;
    }
    virtual ~Shape() {}
};

包含纯虚函数的类称为抽象类,不能实例化对象,只能作为基类被继承,同时子类必须重写所有纯虚函数

虚析构函数

当类中有虚函数时,必须将析构函数声明为虚函数,否则会导致内存泄漏,代码如下:

#include <iostream>
#include <cmath>
using namespace std;

class Shape {
public:
    // 1. 业务虚函数(多态的核心接口)
    virtual void draw() = 0; // 纯虚函数,强制子类实现
    virtual double getArea() = 0;

    // 2. 虚析构函数(配合多态删除场景,必须加)
    virtual ~Shape() {
        cout << "Shape析构" << endl;
    }
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}

    // 重写业务虚函数
    void draw() override {
        cout << "绘制圆形,半径:" << radius << endl;
    }

    double getArea() override {
        return M_PI * radius * radius;
    }

    ~Circle() override {
        cout << "Circle析构" << endl;
    }
};

int main() {
    // 多态场景:父类指针指向子类对象
    Shape* shape = new Circle(5.0);
    shape->draw(); // 调用子类的draw(业务虚函数的作用)
    delete shape;  // 调用子类的析构(虚析构函数的作用)
    return 0;
}

上述代码中由于Shape类和Circle类都有虚函数,所以必须要有虚析构函数,避免内存泄漏

如果想更深入了解虚析构函数可以看我往期关于虚析构函数的文章,那里有更具体的描述~

总结

本文通过动态的概念、分类、虚函数、纯虚函数、虚析构函数这几个方面来描述了C++面向对象之多态的内容,并通过具体代码示例来深入分析多态的实现原理和演示多态的实现过程

本文暂时写到这里,如果文章对你有用的话欢迎点赞收藏~

感兴趣的话也可以关注我,我会持续输出C++相关的内容~