虚函数

虚函数

为什么使用虚函数

通常说的C++中的多态特性有两种:静态联编和动态联编,静态联编是重载函数,而动态联编就是使用派生类覆盖父类的虚函数。若要相让派生类覆盖父类的函数,就必须要将父类的函数定义为虚函数。

参考下面代码

若基类的func不定义为虚函数的话,用派生类转换为基类的指针调用func函数的输出情况:

class Base{
public:
    void func(){
        cout<<"Base"<<endl;
    }
};
class Derive:public Base{
public:
    void func(){
        cout<<"Derive"<<endl;
    }
};
int main(){
    Derive a;
    Base *b = &a;
    b->func();//输出 Base
}

这里还是调用的函数为Base::func(),若将基类的func函数定义为虚函数,那么再用派生类指向基类的指针调用函数时将使用Derive::func();

class Base{
public:
    virtual void func(){
        cout<<"Base"<<endl;
    }
};
class Derive:public Base{
public:
    void func(){
        cout<<"Derive"<<endl;
    }
};
int main(){
    Derive a;
    Base *b = &a;
    b->func();//输出 Derive
}

总而言之,虚函数的用途就是为了我们能够用派生类转换为基类的指针和引用再调用函数时能够使用派生类重写的函数。之所以说它时动态联编是因为只有在运行时我们才知道它调用的哪个函数。

虚函数的实现方式

虚函数的实现方式是为每个定义的类都分配了一个虚表,虚表的地址存在虚表指针中(此外还有RTII,即运行时类型信息存在虚表种,这里不做讨论).

class Base{
public:
    virtual void func1();
    virtual void func2();
    void func3();
};
class Derive:public Base{
public:
    virtual void func2();
    void func4();
};

image-20210201164145146

Base对象和Derive对象的逻辑存储图如上图,Derive对象没有覆盖Base的func1函数,因此Derive对象的虚函数表中还是Base类的func1,而Derive覆盖了Base的func2,Derive中就不存在基类的func2地址了,替换的是自己的func2,其余非虚函数继承得到。

而使用派生类转换向基类的指针和引用时候,使用的派生类对象,因此调用的是派生类的虚表指针。

为什么要使用派生类向基类的指针

当我们用对象来描述动物时,狗、猫、鸟都来继承Animal这个基类,三种动物都有不同的叫声,如果我们每个每个子类都定义一个不同的函数的话,调用和管理起来会很麻烦,因此我们让子类来重写父类的Cry函数,而且我们当我们要用数据集合来存储这些不同的子类的时候,我们可以用 父类的指针 来声明这个数据集合的类型,而访问这个数据集合的数据时,直接调用父类的函数名即可,子类已经重写过父类的虚函数,调用的是子类的函数。

参考下面代码:

class Animal{
public:
    virtual void Cry(){
        cout<<"Animal"<<endl;
    }
};
class Dog:public Animal{
public:
    void Cry(){
        cout<<"wang wang wang!!!"<<endl;
    }
};
class Cat:public Animal{
public :
    void Cry(){
        cout<<"miao miao miao~~~"<<endl;
    }
};
class Bird:public Animal{
public:
    void Cry(){
        cout<<"jiu jiu jiu---"<<endl;
    }
};
int main(){
    vector<Animal*>animals;
    animals.push_back(new Dog());
    animals.push_back(new Cat());
    animals.push_back(new Bird());
    for(auto animal:animals){
        animal->Cry();
    }
}
//输出为
wang wang wang!!!
miao miao miao~~~
jiu jiu jiu---

派生类转换为基类的可访问性

1.只有当派生类公有的继承基类的时候,才能使用派生类向基类的转换;如果派生类继承基类的方式时受保护的或者私有的,则用户代码不能使用该转换。

2.不论派生类以什么方法继承基类的,派生类的成员函数和 友元函数都可以使用派生类向基类的转换。

3.如B继承A的方式时受保护的或者时共有的,则B的派生类的成员和友元可使用B转换为A的类型转换;反之,如果B继承A的方式时私有的,则B的派生类的成员和有缘不可使用B转换为A的类型转换。

纯虚函数和抽象基类

定义一个纯虚函数很简单:在虚函数声明时在后面加上 = 0;这个虚函数就变成了纯虚函数。

virtual void func() = 0;

而含有纯虚函数的类称为抽象基类

class Animal{
public:
    virtual void Cry() = 0;//这是一个纯虚函数
};

我们不能实例一个抽象基类

Animal animal;// 错误,Animal是一个抽象类

而派生类如果不重写基类的纯虚函数的话,那么派生类也将成为一个抽象基类,派生类不想称为一个抽象基类的话,必须要覆盖父类的纯虚函数。

posted @ 2021-02-01 17:23  Emiria  阅读(78)  评论(0编辑  收藏  举报