多态与虚函数

一、什么是多态

多态就是向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为。也就是说,每个对象可以用自己的方式去响应共同的消息。使用相同的代码实现不同的动作,体现了面向对象编程的优越性。

在C++中,多态就是利用基类指针指向子类实例,通过基类指针调用子类(虚)函数从而实现“一个接口,多种形态”的效果。

【多态分为两种】

1、编译时多态:主要通过函数的重载和模板来实现。

2、运行时多态:主要通过虚函数来实现。

二、override、overload、hide区别

1、覆盖、重写(override)

override指基类的某个成员函数为虚函数,派生类又定义一成员函数,除函数体的其余部分都与基类的成员函数相同(一定要函数名、形参、返回类型都相同)。注意,如果只是函数名相同,形参或返回类型不同的话,就不能称为override,而是hide。

2、重载(overload)

指同一个作用域有多个函数名相同,但形参不同的函数。编译器在编译的时候,通过实参的个数和类型,选择最终调用的函数。

3、隐藏(hide)分为两种:

1)局部变量或者函数隐藏了全局变量或者函数

2)派生类拥有和基类同名的成员函数或成员变量

产生的结果:使全局或基类的变量、函数不可见。

三、虚函数

1、什么函数可以成为虚函数

1)只有类的成员函数才能说明为虚函数,因为虚函数仅适合用与有继承关系的类对象,所以普通函数不能说明为虚函数。 

2)静态成员函数不能是虚函数,因为静态成员函数的特点是不受限制于某个对象。

3)内联(inline)函数不能是虚函数,因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义定义,但是在编译的时候系统仍然将它看做是非内联的。 

4)构造函数不能是虚函数,因为构造的时候,对象还是一片位定型的空间,只有构造完成后,对象才是具体类的实例。

5)析构函数可以是虚函数,而且通常声名为虚函数。 

2、虚函数详细解释

以下内容转自:https://blog.csdn.net/lihao21/article/details/5068833

当类B继承类A的时候,类B会同时继承类A的各种函数的调用权,如果一个基类包含了一个虚函数,那么这个继承的类B也拥有自己的虚函数表。

虚函数的作用是什么呢?

这个问题要分成两步:

一、为什么要用父类指针指向子类对象

当我们使用一些类库、框架的时候,这些类库、框架是事先就写好的。我们在使用的时候不能直接修改类库的源码,我们只能派生类库中的类来覆盖一些成员函数以实现我们的功能,但这些成员函数有的是由框架调用的。

多态与虚函数

多态与虚函数

二、为什么要用虚函数

对于一个类成员函数的调用:

Animal *a = new Dog()

a->bark()

a->bark()实际上被编译器变成了Animal::bark(a)

此时还没运行,还是在编译阶段,也就是内存中什么Dog、Animal的对象还没有存在,所以只能根据a这个指针的类型 来决定使用哪个,也就是 Animal::bark()。所以,无论a指向什么子类,他调用的bark函数都是Animal版本的父类版本的bark函数。

所以,现在没有办法达到多态的效果,这个在编译时决定函数是哪个类的函数的方式就叫做静态联编。

现在希望调用的是Dog::bark(a),所以就不能使用静态联编,静态联编在编译的时候决定函数是哪个。

怎么把静态联编改成动态联编呢?

编译器在静态联编时做了以下转换:a->bark ---> Animal::bark(a)

当bark函数被设置为虚函数时,就不会进行那个转换了,而是转化为:a->bark ----> (a->vtpl[1])(a)

先通过a指针找到对象,再找到对象中的虚表,再在虚表里面找到该调用的那个函数的函数指针,因为必须要在a指向的对象里面找,所以必须等到a被创建出来,所以必须是运行时,所以这就是动态联编,运行时决定调用哪个函数。

每个包含了虚函数的类都包含一个虚函数表。 

如下,类A包含虚函数vfunc1,vfunc2,由于类A包含虚函数,故类A拥有一个虚表。

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};

虚函数表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。 

虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。

【虚函数表的构造过程】

①拷贝基类的虚函数指针

②替换重写后的虚函数指针

③追加子类自己的虚函数指针

 

多态与虚函数

虚函数表是属于类的,而不是属于某个具体的对象,同一个类的所有对象都使用同一个虚表。 
为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。

多态与虚函数

复杂的继承关系:类A是基类,类B继承类A,类C又继承类B。

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};

class B : public A {
public:
    virtual void vfunc1();    //子类如果有与基类完全相同的函数可以省略virtual,一样表示虚函数
    void func1();
private:
    int m_data3;
};
 
class C: public B {
public:
    virtual void vfunc2();
    void func2();
private:
    int m_data1, m_data4;
};

由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类A的虚表(A vtbl),类B的虚表(B vtbl),类C的虚表(C vtbl)。类A,类B,类C的对象都会拥有一个虚表指针,*__vptr,用来指向自己所属类的虚表。

类A包括两个虚函数,故A vtbl包含两个指针,分别指向A::vfunc1()和A::vfunc2()。 

类B继承于类A,故类B可以调用类A的函数,但由于类B重写了B::vfunc1()函数,故B vtbl的两个指针分别指向B::vfunc1()和A::vfunc2()。 

类C继承于类B,故类C可以调用类B的函数,但由于类C重写了C::vfunc2()函数,故C vtbl的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和C::vfunc2()。 

虽然下图看起来有点复杂,但是只要抓住“对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数”这个特点,便可以快速将这几个类的对象模型在自己的脑海中描绘出来。

非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。

多态与虚函数

如上图执行下图代码:

int main() 
{
    B bObject;
    A *p = & bObject;
    p->vfunc1();
}

程序在执行p->vfunc1()时,会发现p是个指针,且调用的函数是虚函数,接下来便会进行以下的步骤。

虽然指针p是基类A*类型,但是指针p指向的是B的对象bobject,所以可以根据虚表指针p->__vptr来访问对象bObject对应的虚表。然后,在虚表中查找所调用的函数对应的条目。由于虚表在编译阶段就可以构造出来了,所以可以根据所调用的函数定位到虚表中的对应条目。B vtbl的第一项指向B::vfunc1(),所以 p->vfunc1()实质会调用B::vfunc1()函数。

int main() 
{
    A aObject;
    A *p = &aObject;
    p->vfunc1();
}

当aObject在创建时,它的虚表指针__vptr已设置为指向A vtbl,这样p->__vptr就指向A vtbl。vfunc1在A vtbl对应在条目指向了A::vfunc1()函数,所以 p->vfunc1()实质会调用A::vfunc1()函数。

可以看到,通过使用这些虚函数表,即使使用的是基类的指针来调用函数,也可以达到正确调用运行中实际对象的虚函数。 
我们把经过虚表调用虚函数的过程称为动态绑定,其表现出来的现象称为运行时多态。动态绑定区别于传统的函数调用,传统的函数调用我们称之为静态绑定,即函数的调用在编译阶段就可以确定下来了。

那么,什么时候会执行函数的动态绑定?这需要符合以下三个条件。

1、通过指针来调用函数
2、指针upcast向上转型(继承类向基类的转换称为upcast)
3、调用的是虚函数

如果函数调用符合以上三个条件,编译器就会把该函数调用编译成动态绑定,其函数的调用过程走的是上述通过虚表的机制。

【特别注意】

在多重继承时,派生类有几个父类,系统就自动为其生成几个__vptr指针指向一个针对其中一个基类的虚函数表,如下图:

多态与虚函数