C++ ----- 多态

目录

一、多态

1. 静态多态 

2. 动态多态

        2.1 构成多态的条件

        2.2 虚函数

        2.3 虚函数的重写

        2.4 虚函数重写的两个例外

        2.5 final和 override

        2.6 重载、重写与隐藏三者的区别

二、抽象类

        1. 纯虚函数

                1.1 纯虚函数

                 1.2 抽象类(接口类)

        2. 接口继承和实现继承

                 2.1 接口继承

                 2.2 实现继承

三、多态原理

        1. 虚函数表 

         2. 原理

               构成多态 

                不构成多态

四、单继承与多重继承关系的虚函数表

        1. 单继承的虚函数表

                1.1 虚函数表的初始化时机

                 1.2 子类虚表的生成过程

         2. 多继承的虚函数表

五、多态面试题


一、多态

引入:        

        多态就是函数调用的多种形态。使得我们的程序更加灵活。

        多态的优点:1、代码组织结构清晰   2、代码可读性强   3、利于前期和后期的拓展维护,

                                符合对拓展开放,对修改进行关闭的原则。

        多态分为:静态多态动态多态

        可以使用多态的例子:

                “动物” 这个名词是个抽象的概念,不存在“动物”这种动物。而小猫,小狗都属于动物。我

               们假设动物都可以叫,只是叫声不同而已,那么我们就可以利用C++多态的特性去实现。

1. 静态多态

        静态多态:指程序在编译阶段就可以确定函数的地址

        静态多态包括函数重载运算符重载等,因为它们的实现都是通过函数进行实现的,并且它们的共同点是在编译阶段就可以确定函数的地址

2. 动态多态

        动态多态:指程序在运行后才可以确定函数的地址。

        注意:多态离不开继承

#include <iostream>
using namespace std;
#if 1
class Animal  // 动物
{
public:
    virtual void Call()
    {
        std::cout << "动物 在叫" << std::endl;
    }
};
class Cat_Animal : public Animal  // 猫
{
public:
    virtual void Call()
    {
        std::cout << "小猫在叫" << std::endl;
    }
};
void func1(Animal a)
{
    a.Call();
}
void func2(Animal *a_ptr)
{
    a_ptr->Call();
}
void func3(Animal &a_ref)
{
    a_ref.Call();
}

void test01()
{
    std::cout << "sizeof(Animal) = " << sizeof(Animal) << std::endl;
    std::cout << "sizeof(Cat_Animal) = " << sizeof(Cat_Animal) << std::endl;

    Animal animal_1;
    Cat_Animal cat_1;
    
    func1(animal_1);
    func1(cat_1); // 父类只会拷贝子类中的父类属性,不会覆盖虚表中的指针地址

    std::cout << "------------" << endl;
    // 通过指针
    func2(&animal_1);
    func2(&cat_1);

    std::cout << "------------" << endl;
    // 通过引用
    func3(animal_1);
    func3(cat_1);
}

int main()
{
    test01();
    return 0;
}
#endif

       运行结果:

        解释:

                        1、sizeof(Animal) = 8 的原因是只要父类的成员函数用 virtual 关键字进行修饰,无论是否构成重写或者是否被继承,编译器都会自动存放一个虚函数表指针(vfptr)。

class Base final
{
public:
    virtual void func() { }
};
int main()
{
    std::cout << "sizeof(Base) = " << sizeof(Base) << std::endl;
    return 0;
}

运行结果:        (一个指针 8 个字节)

          2、父类调用父类函数不构成多态。

          3、实现多态的条件

        2.1 构成多态的条件

                 构成条件:1、存在继承关系      2、子类重写父类的虚函数

                 实现条件:1、必须通过父类的指针或者引用指向子类对象

                                2、必须调用的是虚函数(直接调用或者间接调用都可以,但是必须调用),且子类必须对父类的虚函数进行重写。

           

        2.2 虚函数

                被 virtual 关键字修饰的成员函数叫做虚函数。

                注意:(1)只有类的非静态成员函数才可以被 virtual 修饰。

                        (2)虚函数中的 virtual 与虚继承中的 virtual 完全不同。前者是实现多态,后者是为了解决菱形继承数据冗余和二义性问题

        2.3 虚函数的重写

                 重写(覆盖):函数 返回值相同(协变的返回值不一样,但是也能构成多态)函数名称相同(析构函数除外)函数参数也要相同。

                注意:1、子类在重写父类的虚函数时,一定要注意是否实现重写,可以通过 override 关键字进行检查。                 2、子类重写父类虚函数时最好加上 virtual 关键字。

        

        2.4 虚函数重写的两个例外

               (1) 协变:(返回值的类型相同(必须是指针或引用),且是父子关系)

                         

                (2) 析构函数的重写(子类和父类的析构函数名字不同)

                        如果父类的析构函数前面加上 virtual 关键字就行修饰,那么子类的析构函数无论是否加上 virtual 修饰,都与父类的析构函数构成重写。因为编译器对析构函数的函数名统一处理成destructor

        2.5 final和 override

                final 关键字:(1)修饰虚函数;如果一个虚函数不想被重写,可以在虚函数后面加 final 进行修饰。

                        

        (2)修饰类;如果一个类不想被继承,那么就在这个类后面加 final 进行修饰。

                        

                override 关键字:检查派生类(子类)虚函数是否重写完成,如果没有重写完成就编译报错。发生在编译时期。

                        

         2.6 虚析构的重要性

                下面我们在使用多态时常进行的一个操作:

class Animal      // 动物
{
public:
    Animal()
    {
        std::cout << "Animal 构造函数调用" << std::endl;
    }

    virtual void Call()
    {
        std::cout << "动物 在叫" << std::endl;
    }

    ~Animal()  // 不是虚函数
    {
        std::cout << "Animal 析构函数调用" << std::endl;
    }
};

class Cat_Animal : public Animal  // 猫
{
public:
    Cat_Animal()
    {
        p_ptrArr_ = new int[100];
        std::cout << "Cat_Animal 构造函数调用" << std::endl;
    }

    virtual void Call()
    {
        std::cout << "小猫在叫" << std::endl;
    }

    ~Cat_Animal()  // 不构成重写
    {
        std::cout << "Cat_Animal 析构函数调用" << std::endl;

        if(nullptr != p_ptrArr_)
        {
            delete[] p_ptrArr_;
            p_ptrArr_ = nullptr;
        }
    }

private:
    int *p_ptrArr_;
};

void doCall(Animal *animal_ptr_)  // 用父类指针去接收
{
    if(nullptr == animal_ptr_) return ;

    animal_ptr_->Call();

    delete animal_ptr_;
    animal_ptr_ = nullptr;
}

void test01()
{
    doCall(new Cat_Animal);
}

int main()
{
    test01();
    return 0;
}

                运行结果如下:

                

 我们可以看到,运行结果中并没有调用 子类 Cat_Animal 的析构函数,所以导致了内存泄漏!

解决办法:在父类的析构函数前面加 virtual 关键字进行修饰,使其成为虚析构

 修改后的运行结果:可以看到程序调用了子类中的析构函数

当函数执行 delete  animal_ptr_ 时,可以分为两步:

1、调用父类的虚析构函数。由于发生了多态,那么编译器就会调用子类的析构函数,而子类的析构函数会自动调用父类的析构函数 (继承时析构的顺序也是 1、子类的析构函数调用 -----> 2、父类的析构函数调用 )。

2、free 掉子类自身。即直接释放掉对象的内存。  

        2.7 重载、重写与隐藏三者的区别

                重载:即函数重载,函数重载的要求:1、同一作用域下   2、函数名称相同   3、函数的形参个数、顺序、类型不同

                重写(覆盖):对应的成员函数函数 返回值相同(协变的返回值不一样,但是也能构成多态)函数名相同(析构函数除外)函数参数也要相同。

                隐藏(重定义):即子类中重新定义的成员函数的函数名与父类中的成员函数的 函数名相同,不管是静态成员函数还是非静态成员函数,都会被子类的成员函数覆盖(或者也可以说被隐藏了),所以当在子类中需要调用父类的成员函数时最好加上作用域。注意:成员属性也是一样。

              子类和父类中的同名非静态成员函数不构成重写就构成重定义。

class Animal
{
public:
    void func()
    {
        std::cout << "Animal::func()调用" << std::endl;
    }
    int func(int a)
    {
        std::cout << "Animal::func(int a)调用" << std::endl;
        return 0;
    }

    static void func(double a)
    {
        std::cout << "static Animal::func(double a)调用" << std::endl;
    }
};

class Cat_Animal : public Animal
{
public:
    int func()
    {
        std::cout << "Cat_Animal::func()调用" << std::endl;
        return 0;
    }
    int func(int a)
    {
        std::cout << "Cat_Animal::func(int a)调用" << std::endl;
        return 0;
    }
    void func(double a)
    {
        std::cout << "Cat_Animal::func(double a)调用" << std::endl;
    }

};

void test01()
{
    Cat_Animal cat;

    cat.func();
    cat.func(1);
    cat.func(1.1);
}

int main()
{
    test01();
    return 0;
}

                

二、抽象类

        1. 纯虚函数

                1.1 纯虚函数

                        定义:在虚函数的后面写 = 0 ,即不给予函数实现。

                        如: virtual void func() = 0;

                        不需要调用父类的虚函数实现时,就可以将虚函数设置为纯虚函数。

                 1.2 抽象类(接口类)

                          定义:只要包含纯虚函数的类就是抽象类。

                         性质:抽象类不能实例话对象。子类继承抽象类后也不能实例化,必须重写父类纯虚函数,子类才能实例化对象。其作用其实和 override 关键字的作用有点相似。

                        理解/意义:1、能庞统的表达一种抽象话实物,如 人,动物,形状等等。世界上不存在人这个人,也不存在动物这个动物。它们都是一个抽象的总称。并没有具体的事物。

 2、体现了实例化对象的思想,我可以用一个人名,比如 “小明”这个人他属于人,他是一个实体。

3、体现了接口继承,强制子类去重写父类的纯虚函数(不重写的话,子类也是抽象类)。

        2. 接口继承和实现继承

                 2.1 接口继承

                       虚函数和纯虚函数的继承都是接口继承,子类仅仅只继承父类的接口,父类中可有可无的实现这个接口函数实现。

                        注意:为了达到多态的目的和防止程序员忘记对重写函数的重写,最好将虚函数定义成纯虚函数。否则就不要将成员函数定义成虚函数。

                 2.2 实现继承

                        普通父类成员函数的继承属于接口继承,子类可以直接调用,是一种复用;

三、多态原理

        1. 虚函数表

                虚函数表也叫虚表

                每个包含虚函数的类都包含了一个虚函数表指针(_vfptr,  v: virtual ,),而这个虚函数表指针指向虚表。虚表的本质就是一个指针数组,指针数组里面存放的是指向虚函数的函数指针,里面不包含指向普通成员函数的指针。

                虚表属于类的,而不是属于某一个具体的对象,并且所有类对象共用同一个虚表。这一点和静态成员有点相似。                    

        注意:同一个类的实例化对象共用同一个虚表。               

                                                     

         2. 原理

                构成多态 与  不构成多态:

                        (1)指向谁就调用谁的虚表,或者说去对应的虚表查找虚函数指针。指向的对象有关,跟指针类型/引用类型无关。调用接口继承只是为了方便拓展,而不能决定具体实现的虚函数。

                         (2)子类没有重写父类虚函数时,由于子类中的虚表没有被覆盖,子类虚表中的函数指针依然是指向父类的虚函数。所以不管是父类调用还是子类调用,都是一样的。

                        (3)*************记住最重要的一点就是,看对应对象的虚表!!!子类虚表是由父类的虚表 1、拷贝 + 2、覆盖 + 3、增加(子类自己定义的虚函数) 完成的

                  

四、单继承、多重继承关系的虚函数表

        1. 单继承的虚函数表

                1.1 虚函数表的初始化时机

                           虚表指针是在构造函数初始化列表阶段初始化的,虚表在编译时就已经生成了。

                 1.2 子类虚表的生成过程

                            生成子类虚表时,会单独开辟一块空间,拷贝一份父类的虚表,并将对应位置虚函数的覆盖(如果子类重写完成,就覆盖;否则保留)。最后将子类自己定义的虚函数增加到表尾,没有就不加。虚表以 nullptr 作为结尾标识。

                           所以,子类虚表是由父类的虚表 1、拷贝 + 2、覆盖 + 3、增加(子类自己定义的虚函数) 完成的。

               可以认为多重继承也是单继承的一种,所以多重继承的子类虚函数表的初始化也是一样。参考多重继承的初始化顺序。

class A
{
public:
    A()
    {
        cout << "A 的构造函数" << endl;
    }
};

class B : public A
{
public:
    B()
    {
        cout << "B 的构造函数" << endl;
    }
};

class C : public B
{
public:
    C()
    {
        cout << "C 的构造函数" << endl;
    }
};

void test01() // 测试多重继承的初始化顺序
{
    C c1;
}
int main()
{
    test01();
    return 0;
}

运行结果如下: 

 

五、多态面试题

1、什么是多态?

        答:多态顾名思义多种形态,通过父类指针或引用调用不同子类的行为,产生不同的结果。

2、静态多态与动态多态有什么区别?        

        答:静态多态是编译时绑定;动态多态是运行时绑定。

3、多态的优点是什么?

        答:1、代码组织结构清晰   2、代码可读性强   3、利于前期和后期的拓展维护,符合对拓展开放,对修改进行关闭的原则。

4、多态是原理是什么?

        答:原理:通过虚函数表指针指向虚表,虚表中保存虚函数指针;运行时通过虚表中保存的对应的虚函数地址,才能确定需要运行的函数,达到运行时绑定的目的。

        调用方法:通过父类指针或者引用去指向子类对象,再调用父类中的虚函数接口。

5、为什么要将父类和子类的析构函数设置为虚析构或纯虚析构?

        答:防止内存泄漏。如果父类的析构函数不是虚析构时,当用父类的指针去接收子类堆区申请的内存,在释放时不会释放子类的析构函数,导致内存泄漏。

6、什么是抽象类?为什么要使用抽象类?

        答:包含纯虚函数的类叫做抽象类。

1、抽象类不能实例化对象。子类继承父类后,也不能示例化对象,子类必须全部重写父类纯虚函数后才能示例化对象。

2、体现了接口继承,强制子类去重写虚函数。

3、体现了抽象这一思想,比如不存在 人,动物 这种对象。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值