虚函数

232 阅读6分钟

了解虚函数前,需要先了解几个概念:重载,重写,多态,虚函数,纯虚函数

重载和重写

重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不 同,或许两者都不同)。

重写:是指子类重新定义父类虚函数的方法。

从实现原理上来说:

重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函 数(至少对于编译器来说是这样的)。如,有两个同名函数:function func(p:integer):int eger;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样 的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的。也就 是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!

重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指 针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数 的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。

多态,虚函数,纯虚函数

多态:是对于不同对象接收相同消息时产生不同的动作。C++的多态性具体体现在运行和编译两个方面:

在程序运行时的多态性通过继承和虚函数来体现;

在程序编译时多态性体现在函数和运算符的重载上;

虚函数:在基类中冠以关键字 virtual 的成员函数。 它提供了一种接口界面。允许在派生类中对 基类的虚函数重新定义。

纯虚函数的作用:在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。作为接口而存在纯虚函数不具备函数的功能,一般不能直接被调用。

从基类继承来的纯虚函数,在派生类中仍是虚函数。如果一个类中至少有一个纯虚函数,那么这个类被称为抽象类(abstract class)。

抽象类中不仅包括纯虚函数,也可包括虚函数。抽象类必须用作派生其他类的基类,而不能用于直接创建对象实例。但仍可使用指向抽象类的指针支持运行时多态性。

我的理解:虚函数就是为了实现重写功能的一个函数名称,体现了C++中多态的特性。

下来我们具体看看虚函数及虚函数表

虚函数

1、C++虚函数是定义

在基类中的函数,子类必须对其进行覆盖。在类中声明(无函数体的形式叫做声明)虚函数的格式如下:

virtual void display();

2、虚函数的作用

虚函数有两大作用:

(1)定义子类对象,并调用对象中未被子类覆盖的基类函数A。同时在该函数A中,又调用了已被子类覆盖的基类函数B。那此时将会调用基类中的函数B,可我们本该调用的是子类中的覆盖函数B。虚函数即能解决这个问题。 以下是没有使用虚函数的例子:

#include<iostream>
using namespace std;
class Father                    // 基类 Father
{
public:
    void display() {cout<<"Father::display()\n";}
    // 在函数中调用了,子类覆盖基类的函数display() 
    void fatherShowDisplay() {display();} 
};

class Son:public Father                 // 子类Son 
{
public:
    //重写基类中的display()函数
    void display() {cout<<"Son::display()\n";}
};

int main()
{
    Son son;        // 子类对象 
    son.fatherShowDisplay();    // 通过基类中未被覆盖的函数,想调用子类中覆盖的display函数 
}

该例子的运行结果是: Father::display()

(2)在使用指向子类对象的基类指针,并调用子类中的覆盖函数时,如果该函数不是虚函数,那么将调用基类中的该函数;如果该函数是虚函数,则会调用子类中的该函数。

以下是没有使用虚函数的例子:

#include<iostream>
using namespace std;
class Father                    // 基类 Father
{
public:
    void display()
    {cout<<"Father::display()\n";}
};

class Son:public Father                 // 子类Son 
{
public:
    void display()          // 覆盖基类中的display函数 
    {cout<<"Son::display()\n";}
};

int main()
{
    Father *fp;     // 定义基类指针 
    Son son;        // 子类对象 
    fp=&son;        // 使基类指针指向子类对象 
    fp->display();  // 通过基类指针想调用子类中覆盖的display函数 
}

该例子的运行结果是: Father::display() 结果说明,通过指向子类对象的基类指针调用子类中的覆盖函数是不能实现的,因此虚函数应运而生。

以下是使用虚函数的例子:

#include<iostream>
using namespace std;
class Father                    //基类 Father
{
public:
    void virtual display()  //定义了虚函数
    {cout<<"Father::display()\n";}
};

class Son:public Father //子类Son 
{
public:
    void display()          //覆盖基类中的display函数 
    {cout<<"Son::display()\n";}
};

int main()
{
    Father *fp;     //定义基类指针 
    Son son;        //子类对象 
    fp=&son;        //使基类指针指向子类对象 
    fp->display();  //通过基类指针想调用子类中覆盖的display函数 
}

该例子的运行结果是: Son::display()

总结: C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。 这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

说到这里,为了理解虚函数是怎么实现的,就不得不说一下虚函数表

虚函数表概述

虚函数表是指在每个包含虚函数的类中都存在着一个函数地址的数组。当我们用父类的指针来操作一个子类的时候,这张虚函数表指明了实际所应该调用的函数。 C++的编译器保证虚函数表的指针存在于对象实例中最前面的位置,这样通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。 按照上面的说法,来看一个实际的例子:

#include <iostream>

using namespace std;

class Base {
public:
    virtual void f() { cout << "f()" << endl; }
    virtual void g() { cout << "g()" << endl; }
    virtual void h() { cout << "h()" << endl; }
};

int main()
{
    Base t;
    (     ((void(*)())*((int*)(*((int*)&t)) + 0))   )     ();
    (     ((void(*)())*((int*)(*((int*)&t)) + 1))   )     ();
    (     ((void(*)())*((int*)(*((int*)&t)) + 2))   )     ();
    return 0;
}

编译后: 我们成功地通过实例对象的地址,得到了对象所有的类函数。

main定义Base类对象t,把&b转成int ,取得虚函数表的地址vtptr就是:(int)(&t),然后再解引用并强转成int 得到第一个虚函数的地址,也就是Base::f()即(int)(((int)&t)),那么,第二个虚函数g()的地址就是(int*)(((int)&t)) + 1,依次类推。

单继承下的虚函数表

派生类未覆盖基类虚函数#

下面我们来看下派生类没有覆盖基类虚函数的情况,其中Base类延用上一节的定义。从图中可看出虚函数表中依照声明顺序先放基类的虚函数地址,再放派生类的虚函数地址。

可以看到下面几点:

1)虚函数按照其声明顺序放于表中。

2)父类的虚函数在子类的虚函数前面。

派生类覆盖基类虚函数

再来看一下派生类覆盖了基类的虚函数的情形,可见:

1.虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置 (显然的,不然虚函数失去意义)

2.派生类没有覆盖的虚函数延用基类的

测试代码:

#include <iostream>

using namespace std;

class Base {
public:
    virtual void f() { cout << "f()" << endl; }
    virtual void g() { cout << "g()" << endl; }
    virtual void h() { cout << "h()" << endl; }
};

class Derive :public Base{
public:
    virtual void x() { cout << "x()" << endl; }
    virtual void f() { cout << "Derive::f()" << endl; }
};

int main()
{
    Derive t;
    (((void(*)())   *((int*)(*((int*)&t)))))   ();

    (((void(*)())*((int*)(*((int*)&t)) + 1)))     ();

    (((void(*)())*((int*)(*((int*)&t)) + 2)))     ();
    //(((void(*)())*((int*)(*((int*)&t)) + 3)))     ();

    return 0;
}

测试效果:

多继承下的虚函数表

无虚函数覆盖

如果是多重继承的话,问题就变得稍微复杂一丢丢,主要有几点:

1.每个基类都有自己的虚函数表

2.派生类的虚函数地址存依照声明顺序放在第一个基类的虚表最后(这点和单继承无虚函数覆盖相同),具体见下图所示:

派生类覆盖基类虚函数

我们再来看一下派生类覆盖了基类的虚函数的情形,可见:

1.虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置

2.派生类没有覆盖的虚函数延用基类的

总结

** 单继承**

1.虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置

2.派生类没有覆盖的虚函数就延用基类的。同时,虚函数按照其声明顺序放于表中,父类的虚函数在子类的虚函数前面。

多继承

1.每个基类都有自己的虚函数表

2.派生类的虚函数地址存依照声明顺序放在第一个基类的虚表最后

安全性问题

当我们直接通过父类指针调用子类中的未覆盖父类的成员函数,编译器会报错,但通过实验,我们可以用对象的地址访问到各个子类的成员函数,就违背了C++语义,操作会有一定的隐患,当我们使用时要注意这些危险的东西!

参考:

www.jianshu.com/p/d07e0ac0b…
www.cnblogs.com/Mered1th/p/…