c++之多态详解

一、多态的基本概念

多态是c++面向对象的三大特性之一。

多态分为两类:

  • 静态多态:函数重载 和 运算符重载
  • 动态多态:派生类 和 虚函数 实现运行时多态。

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

  • 静态多态的函数地址早绑定:编译阶段进行绑定。
  • 动态多态的函数地址晚绑定:运行阶段进行绑定。
class Animal
{
public:
    virtual void speak(){ //虚函数virtual关键字
        cout << "动物在说话" << endl;
    }
};
class Cat : public Animal
{
public:
    void speak(){
        cout << "小猫说话" << endl;
    }
}
void DoSpeak(Animal & animal)
{
    animal.speak();
}
int main()
{
    Cat cat;
    DoSpeak(cat);
    return 0;
}

上述代码,如果我们运行就会发现,输出结果是“小猫说话”。如果我们去掉virtual关键字,那么就变成了“动物在说话”,这就是多态的基本使用。

从上述例子中可以看出,多态满足的条件是:

  • 有继承关系;
  • 子类重写父类中的虚函数;(重写:函数返回值类型  函数名  参数列表  完全一致的)

多态使用条件:

  • 父类指针或引用 指向 子类的对象。

二、多态基本原理

根据上述代码,我们分析:

Aniaml类的内部结构,如果没有virtual关键字,那么他就是一个空类,sizeof可以看到的大小是1。但是加上virtual之后,就相当于类内部多了一个指针vfptr(虚函数(表)指针),这个指针指向了vftable(虚函数表),而vftable里存储了虚函数表,表内部记录了虚函数的地址,所以这时候用sizeof可看到Animal类的大小变为了4,多了一个指针的大小,这个指针指向虚函数的地址,也就是&Aniaml.speak函数的地址,加取址符。

同理,Cat类内部也一样,因为继承了Animal类,所以也拥有了父类中的指针vfptr和指针指向的虚函数表vftable。如果这时候子类Cat并没有重写speak函数的话,子类的虚函数表vftable内记录的会是父类的地址,也就是&Aniaml.speak。如果子类对speak进行了重写,子类的虚函数表vftable记录的地址将会产生覆盖,即变成了&Cat.speak。

  • 当子类重写父类的虚函数,子类中的虚函数表内部会 替换 成子类的虚函数地址。

下面我们进行验证:

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

可以看到当我们没有使用virtual时,Animal类内大小为1。如果我们加上virtual的话:

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

这时候我们可以看到,这时候类的大小变为4,并且类内多了一个指针vfptr,指向了vftable函数表,表内记录了Animal::speak的地址(Animal类作用域下的speak函数的地址)。这时候我们看一下Cat类的情况:

Class Cat{
public:

};

可以看到如果我们什么都不写的话,Cat类内会继承到父类的vfptr指针,和vftable的虚函数表,而内部记录的是Animal作用域下的speak函数;这时候我们在子类中重写一下speak函数:

class Cat{
public:
    void speak(){
        cout << "小猫说话" << endl;
    }
};

这时候我们会看到,Cat类内的指针指向的虚函数表中记录的地址发生了变化,它记录的是Cat类作用域下的speak函数了。验证了我们上面提到的。

三、纯虚函数和抽象类

在多态中,通道父类中虚函数的实现是没有意义的,主要都是调用子类重写的内容。因此可以将父类中的虚函数改为纯虚函数

纯虚函数语法:virtual  返回值类型  函数名  (参数列表) = 0;

当类中有纯虚函数,这个类也就称为抽象类。抽象类的特点如下:

  • 无法实例化对象;
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
Class Base{
public:
    virtual int Addfunc() = 0;
};
Class Son : public Base{
public:
    int Addfunc(){
        cout << "子类调用" << endl;
    }
};

在上述代码中,如果我们在实例化对象时写入Base b;则会发生报错,因为抽象类中不允许实例化对象。同时,如果我们在子类中不重写纯虚函数的话,也一样无法实例化对象出来。

四、虚析构和纯虚析构

在多态的使用中,如果子类中有属性开辟到堆区,那么父类指针在释放时是无法调用到子类中的析构函数的。解决的方法就是把父类中的析构函数改为虚析构或纯虚析构。

虚析构和纯虚析构的特性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构的区别:

  • 如果是纯虚析构,那么该类属于抽象类,无法实例化对象。

虚析构语法:virtual ~类名(){}

纯虚析构语法:virtual ~类名()= 0; (类外)类名::~类名(){};

Class Animal{
public:
    virtual void speak(){}
    Animal(){
        cout << "父类构造函数" << endl;
    }
    ~Animal(){
        cout << "父类析构函数" << endl;
    }
};
Class Cat : public Animal{
public:
    string *m_Name;
    Cat(string name){
        cout << "子类构造函数" << endl;
        m_Name = new string(name);
    }
    void speak(){
        cout << *m_Name << "小猫说话" << endl;
    }
    ~Cat(){
        if(m_Name != NULL){
            cout << "子类析构函数" << endl;
            delete m_Name;
        }
    }
};
void test01(){
    Animal * animal = new Cat("Tom");
    animal->speak();
    delete animal;
}

在上述代码中,我们运行之后会发现输出的是:

并没有进行子类析构函数的调用,也就是说子类中开辟的堆区数据并没有进行释放,这就是多态中的问题。这时候我们可以把父类中的析构函数改为虚析构或者纯虚析构,这个时候如果我们在运行就会发现子类析构函数被成功调用了。

总结:

  • 虚析构或者纯虚析构就是用来解决通过父类指针释放子类对象;
  • 如果子类中没有堆区数据,可以不用写虚析构或纯虚析构;
  • 拥有纯虚析构函数的类也属于抽象类。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值