C++ 多态

一、基本概念

多态是 C++ 面向对象的三大特性之一(封装、继承、多态)

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过 继承关联时,就会用到多态。

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型执行不同的函数

在 C++ 中,有两种类型的多态

  • 静态多态:函数重载、运算符重载。

  • 动态多态:由派生类虚函数实现运行时多态

两种类型的区别

  • 静态多态在编译期确定函数地址

  • 动态多态在运行阶段确定函数地址

下面,结合实例加以解释

class Animal
{
public:
    void speak(void)
    {
        std::cout << "动物在说话" << std::endl;
    }
};

class XueBao : public Animal
{
public:
    void speak(void)
    {
        std::cout << "雪豹在说话" << std::endl;
    }
};

class DingZheng : public Animal
{
public:
    void speak(void)
    {
        std::cout << "丁真在说藏话" << std::endl;
    }
};

void letAnimalSpeak(Animal &animal)
{
    animal.speak();
}

void test(void)
{
    XueBao xueBao;
    letAnimalSpeak(xueBao);
    DingZheng dingZheng;
    letAnimalSpeak(dingZheng);
}

int main(void)
{
    test();
}

上述代码的执行结果为

动物在说话
动物在说话

很明显,执行结果与我们的预期并不相同,调用函数被编译器设置为父类中的 speak ,这就是静态多态,在编译期,函数地址就已经被确定

想要实现动态多态,只需要在父类的 speak 函数前加上 virtual 关键字即可

virtual void speak(void)

修改后的 speak 函数也叫做 虚函数

下面,我们重点讲解动态多态

二、动态多态的满足条件

一般来说,满足动态多态,需要两个条件:

  • 有继承关系

  • 子类重写父类的虚函数

重写:与重载不同,重写需满足 返回值类型、函数名、形参列表完全相同

注意,在动态多态中,存在一种特殊的重写:当待重写函数的返回值为类本身的指针或引用时,返回值类型可以不同

三、动态多态的使用

想要使用动态多态,必须使用父类指针或者父类的引用指向子类对象

反映到上述的 speak 方法:

// 传引用或指针
void letAnimalSpeak(Animal &animal)
{
    animal.speak();
}

这里,传入的是父类的引用

如果不采用指针或引用的方式传递,那么会调用父类的 speak 函数

四、纯虚函数与抽象类

可以发现:在上述的 Animal 父类中,父类的 speak 虚函数的内部实现是完全没有意义的

事实上,在多态中,通常父类的虚函数的实现是毫无意义的,一般调用的都是子类重写的内容

因此,我们一般将父类的虚函数改写为纯虚函数

4.1 纯虚函数

4.1.1 定义

被表明为不具体实现的虚拟成员函数称为纯虚函数

简单来说,就是告诉编译器,这里我不给出函数的具体实现,具体实现在子类中(有点类似函数声明)

4.1.2 语法

virtual 函数返回值类型 函数名(形参列表) = 0;

4.1.3 适用情况

定义一个父类时,会遇到无法定义父类中虚函数的具体实现,其实现依赖于不同的子类。

4.2 抽象类

4.2.1 定义

当一个类中只要有一个纯虚函数,这个类就成为了抽象类

4.2.2 特点
  • 无法实例化对象

  • 子类必须重写父类的纯虚函数,否则,子类仍为抽象类

4.3 实例

下面以一个计算器类作为例子,介绍纯虚函数与抽象类的使用

#include<iostream>
// 实现两个数的运算

// 抽象类
class Calculator
{
protected:
    double num0;
    double num1;

public:
    // 纯虚函数
    // 只要有纯虚函数,就是抽象类
    // 抽象类无法实例化对象
    virtual double calculate(void) = 0; // 告知编译器,在父类中,我无法给出 calculate 的具体实现
};

class Add : public Calculator
{
public:
    Add(double num0, double num1)
    {
        this->num0 = num0;
        this->num1 = num1;
    }
    // 重写父类中的纯虚函数,否则子类仍然是抽象类
    double calculate(void)
    {
        return num0 + num1;
    }
};

// 多态的优点
// 1. 结构清晰
// 2. 可读性高
// 3. 扩展性强、易于维护(想要扩展父类的功能,只需要再编写一个子类,无需修改之前的代码)

// 扩展一个减法类

class Subtract : public Calculator
{
public:
    Subtract(double num0, double num1)
    {
        this->num0 = num0;
        this->num1 = num1;
    }
    double calculate(void)
    {
        return num0 - num1;
    }
};


// 传入父类的指针
void calculating(Calculator *cal)
{
    // 一个接口,有多种形态,可以实现多种功能
    std::cout << cal->calculate() << std::endl;
    delete cal;
    cal = nullptr;
}

int main(void)
{
    double num0, num1;
    char op;
    std::cin >> num0 >> op >> num1;
    switch (op)
    {
    case '+':
    {
        calculating(new Add(num0, num1));
        break;
    }
    case '-':
    {
        calculating(new Subtract(num0, num1));
        break;
    }
    default:
        std::cout << "Check Your Input!" << std::endl;
        break;
    }
}

与之前的动物类不同,在计算器类中,我们使用的是指针来传递对象

通过计算器类,可以发现多态的优点:

  • 结构清晰

  • 可读性强

  • 扩展性高、易于维护(想要扩展父类的功能,只需要再编写一个子类,无需修改之前的代码)

因此,在实际开发中,如果发现满足多态的使用条件,应使用多态解决

五、虚析构与纯虚析构

在使用多态时,如果父类不提供虚析构或纯虚析构函数,那么子类的析构函数就无法调用,可能造成内存泄漏

因此,建议继承层次关系的父类应该定义虚函数性质的析构函数,即使这个析构函数不做任何的事情。

5.1 虚析构函数的语法

在父类的析构函数前加上 virtual 关键字,父类的析构函数就变为虚析构函数

virtual ~父类名称()
{
    具体实现
}

5.2 纯虚析构函数的语法

与纯虚函数类似

// 父类内
virtual ~父类名称() = 0;
------------------------
// 父类外
父类名称::~父类名称()
{
    具体实现
}

5.3 注意事项

无论是虚析构还是纯虚析构,都需要有具体的函数实现(就算是空实现)

5.4 测试代码

#include <iostream>
// 虚析构与纯虚析构
// 继承层次关系的父类应该定义虚函数性质的析构函数,即使这个析构函数不做任何的事情。
class Animal
{
public:
    Animal()
    {
        std::cout << "Calling Animal's constructor!" << std::endl;
    }
    // ~Animal()
    // {
    //     std::cout << "Calling Animal's destructor!" << std::endl;
    // }
    // 如果父类不提供虚析构函数或者纯虚析构函数
    // 就无法调用子类的虚构函数
    // 此时,若子类含有在堆区开辟的成员
    // 就会造成内存泄露

    // 虚析构函数
    virtual ~Animal()
    {
        std::cout << "Calling Animal's virtual destructor!" << std::endl;
    }

    // 纯虚析构函数
    // virtual ~Animal() = 0;

    virtual void speak(void) = 0;
};

// 类外给出具体实现(可以是空实现)
// Animal::~Animal()
// {
//     std::cout << "Calling Animal's virtual destructor!" << std::endl;
// }

// 子类含有在堆区开辟的成员
class Cat : public Animal
{
    std::string *name;

public:
    Cat(std::string name)
    {
        std::cout << "Calling Cat's constructor!" << std::endl;
        this->name = new std::string(name);
    }
    ~Cat()
    {
        std::cout << "Calling Cat's destructor!" << std::endl;
        delete name;
        name = nullptr;
    }
    void speak(void)
    {
        std::cout << *name << " cat is talking right now!" << std::endl;
    }
};

void test(void)
{
    Animal *animal = new Cat("Tom");
    animal->speak();
    delete animal;
    animal = nullptr;
}

int main(void)
{
    test();
    // 父类不提供虚析构函数的输出:
    // Calling Animal's constructor!
    // Calling Cat's constructor!
    // Tom cat is talking right now!
    // Calling Animal's destructor!
    // 可以发现:没有调用 Cat 的析构函数
}

六、总结

多态作为 C++ 面向对象的三大特性之一,应该提倡使用

一般有两种类型的多态,即静态多态与动态多态

  • 静态多态在编译期确定函数地址

  • 动态多态在运行阶段确定函数地址

想要使用动态多态,必须使用父类指针或者父类的引用指向子类对象

一般父类的虚函数内部没有什么意义,可改写为纯虚函数,此时,该类就变为了抽象类

在使用多态时,如果父类不提供虚析构或纯虚析构函数,那么子类的析构函数就无法调用。因此,建议继承层次关系的父类应该定义虚函数性质的析构函数,即使这个析构函数不做任何的事情。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值