C++ 面向对象核心-多态

目录

1. 多态

1.1 什么是多态

1.2 函数覆盖

 1.3 虚函数

1.4 多态的实现

1.5 多态原理

1.6 虚析构函数


1. 多态

1.1 什么是多态

态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

多态可以理解为”一种接口,多种状态“,只需要编写一个函数接口,根据传入的参数类型,执行不同的策略代码。

在面向对象编程中,我们通常将多态分为两种类型:静态多态(静态多态,也被称为编译时多态)和动态多态(动态多态,也被称为运行时多态)。

静态多态

● 静态多态是指在编译时就能确定要调用的函数,通过函数重载、运算符重载、模板来实现

动态多态

● 动态多态是指在运行时根据对象的实际类型来确定要调用的函数,通过继承和函数覆盖来实现。

静态多态与动态多态的区别:

  • 静态多态发生在编译时,因为在编译阶段编译器就可以确定要调用的函数。
  • 动态多态发生在运行时,因为具体要调用那个函数是在程序运行时根据实际的对象类型来确定的。

多态的优缺点:

  • 多态的优点:多态的优势包括代码的灵活性、可扩展性和可维护性。它能够使代码更具通用性,减少重复代码的编写,并且能够轻松地添加新的派生类或扩展现有的功能。
  • 多态的缺点:多态的缺点包括代码的复杂性、运行效率、易读性。当类的继承关系复杂时,理解和维护多态性相关的代码会变得困难。动态多态在运行中会产生一些额外的开销。

多态的使用具有三个前提条件:

        ● 公有继承

        ● 函数覆盖

        ● 基类的引用/指针指向派生类的对象

1.2 函数覆盖

函数覆盖、函数隐藏。这两个比较相似,但是函数隐藏不支持多态,而函数覆盖是多态的必要条件之一。

函数覆盖比函数隐藏有以下几点区别:

● 函数隐藏是派生类中存在与基类同名同参数的函数,编译器会将基类的同名同参数的函数进行隐藏。注:基类的函数得是非虚函数的普通成员函数。

● 函数覆盖是基类中定义了一个虚函数,派生类写一个同名同参数的函数将基类中的虚函数进行重写并覆盖。注:覆盖的基类函数必须是虚函数。

函数覆盖的要求:

重写/覆盖: 子类中有一个跟父类完全相同的虚函数,子类的虚函数重写了基类的虚函数

即:子类父类都有这个虚函数 + 子类的虚函数与父类虚函数的 函数名/参数/返回值 都相同 -> 重写/覆盖(注意:参数只看类型是否相同,不看缺省值)

 1.3 虚函数

一个函数使用 virtual关键字修饰,就是虚函数,虚函数是函数覆盖的前提。在Qt Creator中虚函数的函数名称使用斜体字。

虚函数进行函数覆盖练习:

#include <iostream>

using namespace std;

class Mihoyo
{
public:
    virtual void game()
    {
        cout << " 原神来了" << endl;
    }
};

class Mi:public Mihoyo
{
public:
    void game()
    {
        cout << " 崩铁来了" << endl;
    }

};


int main()
{
    cout << "Hello World!" << endl;
    Mihoyo game1;
    game1.game();

    Mi game2;
    game2.game();


    return 0;
}

虚函数的性质:

● 虚函数具有传递性,基类中被覆盖的函数是虚函数,派生类中新覆盖的函数也是虚函数。

● 只有普通成员函数与析构函数可以声明为虚函数。

● 在C++11中,可以在派生类新覆盖的函数上使用override关键字验证覆盖是否成功。

虚函数性质练习:

#include <iostream>

using namespace std;


class GenShin
{
public:
    // 错误,构造函数不能声明为虚函数
    //    virtual GenShin()
    //    {
    //        cout << "原神天下无敌" << endl;
    //    }

    // 错误,静态函数不能为虚函数
    //    virtual static void yuanshen()
    //    {
    //        cout << "原神启动 " << endl;
    //    }

    virtual void kejin()
    {
        cout << "十连三金" << endl;
    }



    void chouka()
    {
        cout << "over 欧皇附体" << endl;
    }

};


class Star : public GenShin
{
public:
    // 覆盖基类中的虚函数,派生类的virtual可写可不写
    void kejin()override
    {
        cout << "原神,狗都不玩" << endl;
    }

    // 错误,标记覆盖但是没有覆盖
    // 注意:这是函数隐藏,不是函数覆盖
    // void chouka() override
    // {
    //     cout << "over 欧皇附体" << endl;
    // }

};


int main()
{
    cout << "Hello World!" << endl;

    return 0;
}

1.4 多态的实现

我们在开篇时提到过,要实现动态多态,需要具有三个前提条件。

● 公有继承(已经实现)

● 函数覆盖(已经实现)

● 基类的指针/引用指向派生类对象(还未编写)

【思考】为什么要基类引用/指针指向派生类对象?

● 实现运行时多态:当使用基类的指针或引用来指向派生类对象时,程序在运行时会根据对象的实际类型来调用相应的函数,而不是根据指针或引用的类型。

● 统一接口:基类的指针可以作为一个通用的接口,用于操作不同类型的派生类对象。这样可以使代码更灵活,减少重复的代码,并且支持代码的扩展和维护。

我们可以提供通用接口,参数设计为基类的指针或者引用,这样这个函数就可以访问到此基类所有派生类的虚函数了。

通过基类指针提供通用解救实例:

void animal_eat1(Animal *a1) 与 void animal_eat2(Animal &a1)。

通过上述两个函数接口,当传入的参数为dog类,就调用dog类中的函数,当传入的参数为cat类,就调用cat类中的函数。

#include <iostream>

using namespace std;

class Animal
{
public:
    virtual void eat()
    {
        cout << "动物爱吃饭" << endl;
    }
};

class Dog : public Animal
{
    void eat() override
    {
        cout << "狗爱吃骨头" << endl;
    }
};


class Cat : public Animal
{
public:
    void eat()
    {
        cout << "猫爱吃鱼" << endl;
    }
};

// 通用函数接口
void animal_eat1(Animal *a1)
{
    a1->eat();
}

void animal_eat2(Animal &a1)
{
    a1.eat();
}


int main()
{
    // 基类指针指向派生类对象
    Animal *a1 = new Dog;
    a1->eat();  // 狗爱吃骨头

    Dog d1;
    Animal &a2 = d1;
    a2.eat();   // 狗爱吃骨头

    // 通用函数接口
    Dog d2;
    Cat c2;
    animal_eat1(&d2);
    animal_eat1(&c2);

    animal_eat2(d2);
    animal_eat2(c2);

    return 0;
}

1.5 多态原理

具有虚函数的类会存在一张虚函数表,每个类的对象内部都会有一个隐藏的虚函数表指针成员变量,指向当前类的虚函数表。

多态的实现流程:

在代码运行时,通过对象的虚函数表指针找到虚函数表,在表中定位到虚函数的调用地址,从而执行对应虚函数的内容。 

具体原理:

如果类中有虚函数,那么就会生成一张虚函数表,里面保存着虚函数的地址,同时生成一个虚函数指针指向虚函数表。

派生类在继承时,会将虚函数表也一块继承,同时生成一个虚函数指针指向继承的虚函数表。

如果子类中对从基类继承的的虚函数进行重写,那么虚函数表中的地址就会发生改变,指向重写后的函数地址。

这样父类和子类之中保存的虚函数表中保存的虚函数地址是不一样的,通过传入指针或者引用(本质也是指针)确定去子类还是父类之中去寻找虚函数表中寻找对应函数的地址,最后达到调用不同虚函数的目的。当调用子类中的函数时,调用的也就是重写后的函数。

1.6 虚析构函数

如果不使用虚析构函数,且基类指针或引用指向派生类对象,使用delete销毁对象时,只能触发基类的析构函数,如果在派生类中申请内存资源,则会导致无法释放,出现内存泄漏的问题。

解决方案是给基类的析构函数使用virtual关键字修饰为虚析构函数,通过传递性可以把各个派生类的析构函数都变为虚析构函数。因此建议给一个可能为基类的类中析构函数设置为虚析构函数。

通过虚析构函数释放空间实例:实例中

// Animal类型的指针,指向Dog类中的从Animal中继承的虚函数表,
// 只能调用到Animal类型的函数,也就是只能调用到基类中的析构函数,
// 但是基类中的析构函数变为虚函数之后,基类中的析构函数也会加入到虚函数表中,
// 子类中继承到的虚函数表中也会有基类的析构函数。在子类中也有定义的析构函数,新定义的析构函数就会覆盖掉虚函数表中基类的析构函数
// 再调用Animal的析构函数时,因为发生了函数覆盖,系统会调用覆盖后的子类的析构函数
// 而调用子类的析构函数时,还会默认调用基类中的析构函数,就可以把空间完全释放了

#include <iostream>

using namespace std;

class Animal
{
public:
    virtual ~Animal()
    {
        cout << "Animal的析构函数" << endl;
    }
};

class Dog : public Animal
{
public:
    ~Dog()
    {
        cout << "Dog的析构函数" << endl;
    }

};


int main()
{
    cout << "Hello World!" << endl;

    
    Animal *a1 = new Dog;
    delete a1;

    return 0;
}

  • 14
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值