zoukankan      html  css  js  c++  java
  • 虚函数详解

    一、多态与重载

    1、多态的概念

      面向对象的语言有三大特性:继承、封装、多态。虚函数作为多态的实现方式,重要性毋庸置疑。

      多态意指相同的消息给予不同的对象会引发不同的动作(一个接口,多种方法)。其实更简单地来说,就是“在用父类指针调用函数时,实际调用的是指针指向的实际类型(子类)的成员函数”。多态性使得程序调用的函数是在运行时动态确定的,而不是在编译时静态确定的。

    2、重载—编译期多态的体现

      重载,是指在一个类中的同名不同参数的函数调用,这样的方法调用是在编译期间确定的。

    3、虚函数—运行期多态的体现

      运行期多态发生的三个条件:继承关系、虚函数覆盖、父类指针或引用指向子类对象。

    二、虚函数实例

    #include <iostream>
    #include <conio.h>
    using namespace std;
    
    class Base
    {
    public:
        virtual void vir_fun() { cout << "vitrual function,this is class Bass" <<endl;}
        void fun(){ cout << "normal function,this is class Bass" <<endl;}
    };
    class A : public Base
    {
    public:
        virtual void vir_fun() { cout << "vitrual function,this is class A" <<endl;}
        void fun(){ cout << "normal function,this is class A" <<endl;}
    };
    
    class B : public Base
    {
    public:
        virtual void vir_fun() { cout << "vitrual function,this is class B" <<endl;}
        void fun(){ cout << "normal function,this is class B" <<endl;}
    };
    
    int main()
    {
        Base * b1 = new (Base);
        Base *b2 = new (A);
        Base *b3 = new (B);
        b1->fun();  //调用的都是基类base的函数
        b2->fun();   //调用的都是基类base的函数
        b3->fun ();  //调用的都是基类base的函数
    
        cout << "############################## " << endl ;
        b1->vir_fun();  //调用的是指针指向的实际类型的函数   BASE
        
        b2->vir_fun();  //调用的是指针指向的实际类型的函数   A
        
        b3->vir_fun();  //调用的是指针指向的实际类型的函数   B
        cout << "############################## " << endl ;
        ((A*) b2)->vir_fun();    //A
        ((B *)b3)->vir_fun();    //B
    
        cout << "############################## " << endl ;
        ((A*) b2)->fun();       //A
        ((B *)b3)->fun();       //B
    
        //当使用类的指针调用成员函数时,普通函数由指针类型决定,
        //而虚函数由指针指向的实际类型决定
    }

    显示的内容

    /* 显示内容
        normal function,this is class Bass
        normal function,this is class Bass
        normal function,this is class Bass
        ##############################
        vitrual function,this is class Bass
        vitrual function,this is class A
        vitrual function,this is class B
        ##############################
        vitrual function,this is class A
        vitrual function,this is class B
        ##############################
        normal function,this is class A
        normal function,this is class B
        */

    在上述例子中,我们首先定义了一个基类base,基类有一个名为vir_func的虚函数,和一个名为func的普通成员函数。而类A,B都是由类base派生的子类,并且都对成员函数进行了重载。然后我们定义三个base类型的指针Base、a、b分别指向类base、A、B。可以看到,当使用这三个指针调用func函数时,调用的都是基类base的函数。而使用这三个指针调用虚函数vir_func时,调用的是指针指向的实际类型的函数。最后,我们将指针b做强制类型转换,转换为A类型指针,然后分别调用func和vir_func函数,发现普通函数调用的是类A的函数,而虚函数调用的是类B的函数。

      以上,我们可以得出结论当使用类的指针调用成员函数时,普通函数由指针类型决定,而虚函数由指针指向的实际类型决定。

      虚函数的实现过程:通过对象内存中的vptr找到虚函数表vtbl,接着通过vtbl找到对应虚函数的实现区域并进行调用。

    三、虚函数的实现(内存布局)

      虚函数表中只存有一个虚函数的指针地址,不存放普通函数或是构造函数的指针地址。只要有虚函数,C++类都会存在这样的一张虚函数表,不管是普通虚函数亦或是纯虚函数,亦或是派生类中隐式声明的这些虚函数都会生成这张虚函数表。

      虚函数表创建的时间:在一个类构造的时候,创建这张虚函数表,而这个虚函数表是供整个类所共有的。虚函数表存储在对象最开始的位置。虚函数表其实就是函数指针的地址。函数调用的时候,通过函数指针所指向的函数来调用函数。

    1、无继承情况

    #include <iostream>
    using namespace std;
     
    class Base
    {
    public:
        Base(){cout<<"Base construct"<<endl;}
        virtual void f() {cout<<"Base::f()"<<endl;}
        virtual void g() {cout<<"Base::g()"<<endl;}
        virtual void h() {cout<<"Base::h()"<<endl;}
        virtual ~Base(){}
    };
     
    int main()
    {
        typedef void (*Fun)();  //定义一个函数指针类型变量类型 Fun
        Base *b = new Base();
        //虚函数表存储在对象最开始的位置
        //将对象的首地址输出
        cout<<"首地址:"<<*(int*)(&b)<<endl;
     
        Fun funf = (Fun)(*(int*)*(int*)b);
        Fun fung = (Fun)(*((int*)*(int*)b+1));//地址内的值 即为函数指针的地址,将函数指针的地址存储在了虚函数表中了
        Fun funh = (Fun)(*((int *)*(int *)b+2));
     
        funf();
        fung();
        funh();
     
        cout<<(Fun)(*((int*)*(int*)b+4))<<endl; //最后一个位置为0 表明虚函数表结束 +4是因为定义了一个 虚析构函数
     
        delete b;
        return 0;
    }

    2、单继承情况(无虚函数覆盖)

      假设有如下所示的一个继承关系:


      请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:

    【Note】:

    • 覆盖的f()函数被放到了虚表中原来父类虚函数的位置。

    • 没有被覆盖的函数依旧在原来的位置。

    这样,我们就可以看到对于下面这样的程序,

    Base *b = new Derive();
    b->f();

    由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

    4、多重继承情况(无虚函数覆盖)

      下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。

     对于子类实例中的虚函数表,是下面这个样子:

    Note】:

    • 每个父类都有自己的虚表(有几个基类就有几个虚函数表)。

    • 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)。

    5、多重继承情况(有虚函数覆盖)

      下面我们再来看看,如果发生虚函数覆盖的情况。下图中,我们在子类中覆盖了父类的f()函数。

    下面是对于子类实例中的虚函数表的图:

     我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:

    Derive d;
    Base1 *b1 = &d;
    Base2 *b2 = &d;
    Base3 *b3 = &d;
    b1->f(); //Derive::f()
    b2->f(); //Derive::f()
    b3->f(); //Derive::f()
    b1->g(); //Base1::g()
    b2->g(); //Base2::g()
    b3->g(); //Base3::g()

    四、虚函数的相关问题

    1、构造函数为什么不能定义为虚函数

      构造函数不能是虚函数。

      首先,我们已经知道虚函数的实现则是通过对象内存中的vptr来实现的。而构造函数是用来实例化一个对象的,通俗来讲就是为对象内存中的值做初始化操作。那么在构造函数完成之前,vptr是没有值的,也就无法通过vptr找到作为虚函数的构造函数所在的代码区。

    2、析构函数为什么要定义为虚函数?

      析构函数可以是虚函数且推荐最好设置为虚函数。

    class B
    {
    public:
        B() { printf("B()
    "); }
        virtual ~B() { printf("~B()
    "); }
    private:
        int m_b;
    };
     
    class D : public B
    {
    public:
        D() { printf("D()
    "); }
        ~D() { printf("~D()
    "); }
    private:
        int m_d;
    };
     
    int main()
    {
        B* pB = new D();
        delete pB;
        return 0;
    }

    C++中有这样的约束:执行子类构造函数之前一定会执行父类的构造函数;同理,执行子类的析构函数后,一定会执行父类的析构函数,这也是为什么我们一直建议类的析构函数写成虚函数的原因。

    3、如何去验证虚函数表的存在

    typedef void(*Fun)(void);
    // 取类的一个实例
    Base b;
    Fun pFun = NULL;
    // 把&b转成int ,取得虚函数表的地址
    cout << "虚函数表地址:" << (int*)(&b) << endl;
    // 再次取址就可以得到第一个虚函数的地址了
    cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
    pFun = (Fun)*((int*)*(int*)(&b));
    pFun();
  • 相关阅读:
    通过path绘制点击区域
    能添加图标的label
    便利的初始化view以及设置tag值
    递归搜寻NSString中重复的文本
    自动移除的通知中心
    BadgeValueView
    SpringCloud的入门学习之Eureka(Eureka的单节点)
    Elasticsearch 6.x版本全文检索学习之分布式特性介绍
    关于window10更新之后,15.5版本虚拟机不能使用的情况:检测更新版本
    RabbitMQ的消息确认ACK机制
  • 原文地址:https://www.cnblogs.com/rosesmall/p/14850324.html
Copyright © 2011-2022 走看看