精华内容
下载资源
问答
  • 虚函数表

    2020-12-21 21:31:40
      虚函数表是实现多态的核心,所谓多态,就是“一个函数,多种实现”,当我们通过类指针或引用调用一个函数接口时,编译器在运行期间将会根据该指针或引用实际指向的对象来调用函数,而这就是通过虚函数表来实现的...

      虚函数表是实现多态的核心,所谓多态,就是“一个函数,多种实现”,当我们通过类指针或引用调用一个函数接口时,编译器在运行期间将会根据该指针或引用实际指向的对象来调用函数,而这就是通过虚函数表来实现的,换种方式说,虚函数表,完成了动态联编中寻找虚函数哪个执行代码块的任务。

      现在来理一下编译器使用虚函数表的过程:

      编译器在处理虚函数时,会为基类及相关派生类添加一个隐藏成员,这个成员是用来指向虚函数表的指针,对于每个对象而言,该指针是不同的,该指针指向的虚函数表也是不同的,如果我们把这个指针视作vpr,也就是说:每个类对象具有不同的vpr和虚函数表。

      对于基类而言,其虚函数表指向其所有虚函数的地址表,这个地址表就是指向代码块的所在地址。

      对于派生类而言,情况则相对复杂,因为它继承了基类的虚函数。

      如果派生类没有重新定义其继承的虚函数,那么这个虚函数表将保存其基类的函数地址;如果它重新定义了其继承的虚函数,则将保存它的新的函数地址,而将其基类中对应的函数实现给覆盖掉

      如果派生类自己新定义了一些虚函数,那么这些虚函数地址也将保存到这个表中。

      我们可以结合下面这个示意图示意图来理解:
      假定Derive继承于Base,图中所示的函数都是虚函数。
      在这里插入图片描述
      由于派生类重新实现了基类中的 f 方法,那么其虚函数表如下:
      在这里插入图片描述

      那么一旦运行,这些虚函数表是如何工作的呢?

      如果我们通过引用或指针调用了虚函数,那么根据这些指针或引用所指向的实际对象,编译器会找到实际对象的隐藏对象vpr。

      然后,根据vpr,我们就可以找到对象的虚函数表,然后根据虚函数表,我们就可以找到所要执行的代码块的地址。

      由上可见,虚函数表其实会增加我们存储地址的空间;

      那么针对哪些地方我们要使用虚函数,哪些地方又不能使用呢?

      要使用的地方:

    • 析构函数,这个前面已经说了,如果delete一个基类指针,如果析构函数未声明为虚函数,那么即使该引用或指针指向的是个派生类,也将只调用基类析构函数,而无法执行析构函数,所以,为安全起见,基类析构函数必须为虚函数;
    • 需要在派生类中重新定义的函数,这是当然的;

      不能使用的地方:

    • 构造函数,我们应该不会有想要在派生类中重新定义基类构造函数的冲动,这样有什么用?
    展开全文
  • C++虚函数表剖析

    万次阅读 多人点赞 2016-02-18 20:28:23
    这个技术的核心是虚函数表(下文简称虚表)。本文介绍虚函数表是如何实现动态绑定的。二、类的虚表每个包含了虚函数的类都包含一个虚表。 我们知道,当一个类(A)继承另一个类(B)时,类A会继承类B的函数的调用权...

    关键词:虚函数,虚表,虚表指针,动态绑定,多态

    一、概述

    为了实现 C++ 的多态,C++ 使用了一种动态绑定的技术。这个技术的核心是虚函数表(下文简称虚表)。本文介绍虚函数表是如何实现动态绑定的。

    二、类的虚表

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

    我们知道,当一个类(A)继承另一个类(B)时,类 A 会继承类 B 的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。

    我们来看以下的代码。类 A 包含虚函数vfunc1vfunc2,由于类 A 包含虚函数,故类 A 拥有一个虚表。

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

    类 A 的虚表如图 1 所示。

    在这里插入图片描述

    图 1:类 A 的虚表示意图

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

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

    三、虚表指针

    虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。

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

    在这里插入图片描述

    图 2:对象与它的虚表

    上面指出,一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。

    四、动态绑定

    说到这里,大家一定会好奇 C++ 是如何利用虚表和虚表指针来实现动态绑定的。我们先看下面的代码。

    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();
        void func1();
    private:
        int m_data3;
    };
    
    class C: public B {
    public:
        virtual void vfunc2();
        void func2();
    private:
        int m_data1, m_data4;
    };
    

    类 A 是基类,类 B 继承类 A,类 C 又继承类 B。类 A,类 B,类 C,其对象模型如下图 3 所示。

    在这里插入图片描述

    图 3:类 A,类 B,类 C 的对象模型

    由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类 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()

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

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

    假设我们定义一个类 B 的对象。由于 bObject是类 B 的一个对象,故bObject包含一个虚表指针,指向类 B 的虚表。

    int main() 
    {
        B bObject;
    }
    

    现在,我们声明一个类 A 的指针p来指向对象bObject。虽然p是基类的指针只能指向基类的部分,但是虚表指针亦属于基类部分,所以p可以访问到对象bObject的虚表指针。bObject的虚表指针指向类 B 的虚表,所以p可以访问到 B vtbl。如图 3 所示。

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

    当我们使用p来调用vfunc1()函数时,会发生什么现象?

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

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

    首先,根据虚表指针p->__vptr来访问对象bObject对应的虚表。虽然指针p是基类A*类型,但是*__vptr也是基类的一部分,所以可以通过p->__vptr可以访问到对象对应的虚表。

    然后,在虚表中查找所调用的函数对应的条目。由于虚表在编译阶段就可以构造出来了,所以可以根据所调用的函数定位到虚表中的对应条目。对于 p->vfunc1()的调用,B vtbl 的第一项即是vfunc1对应的条目。

    最后,根据虚表中找到的函数指针,调用函数。从图 3 可以看到,B vtbl 的第一项指向B::vfunc1(),所以 p->vfunc1()实质会调用B::vfunc1() 函数。

    如果p指向类 A 的对象,情况又是怎么样?

    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()函数。

    可以把以上三个调用函数的步骤用以下表达式来表示:

    (*(p->__vptr)[n])(p)
    

    可以看到,通过使用这些虚函数表,即使使用的是基类的指针来调用函数,也可以达到正确调用运行中实际对象的虚函数。

    我们把经过虚表调用虚函数的过程称为动态绑定,其表现出来的现象称为运行时多态。动态绑定区别于传统的函数调用,传统的函数调用我们称之为静态绑定,即函数的调用在编译阶段就可以确定下来了。

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

    • 通过指针来调用函数
    • 指针 upcast 向上转型(继承类向基类的转换称为 upcast,关于什么是 upcast,可以参考本文的参考资料)
    • 调用的是虚函数

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

    五、总结

    封装,继承,多态是面向对象设计的三个特征,而多态可以说是面向对象设计的关键。C++ 通过虚函数表,实现了虚函数与对象的动态绑定,从而构建了 C++ 面向对象程序设计的基石。

    参考资料

    • 《C++ Primer》第三版,中文版,潘爱民等译
    • http://www.learncpp.com/cpp-tutorial/125-the-virtual-table/
    • 侯捷《C++最佳编程实践》视频,极客班,2015
    • Upcasting and Downcasting, http://www.bogotobogo.com/cplusplus/upcasting_downcasting.php

    附:源代码

    https://github.com/haozlee/vtable/blob/master/main.cpp

    展开全文
  • 虚函数表详解

    万次阅读 多人点赞 2018-06-22 17:29:10
    本文转自:...这个技术的核心是虚函数表(下文简称虚表)。本文介绍虚函数表是如何实现动态绑定的。二、类的虚表每个包含了虚函数的类都包含一个虚表。我们知道,当一个类(A)继承另一个类(B)时...

    关键词:虚函数,虚表,虚表指针,动态绑定,多态

    一、概述

    为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表(下文简称虚表)。本文介绍虚函数表是如何实现动态绑定的。

    二、类的虚表

    每个包含了虚函数的类都包含一个虚表。 
    我们知道,当一个类(A)继承另一个类(B)时,类A会继承类B的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。

    我们来看以下的代码。类A包含虚函数vfunc1,vfunc2,由于类A包含虚函数,故类A拥有一个虚表。

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

    类A的虚表如图1所示。 
    这里写图片描述 
    图1:类A的虚表示意图

    虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。 
    虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。

    三、虚表指针

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

    这里写图片描述
    图2:对象与它的虚表

    上面指出,一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。

    四、动态绑定

    说到这里,大家一定会好奇C++是如何利用虚表和虚表指针来实现动态绑定的。我们先看下面的代码。

    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();
        void func1();
    private:
        int m_data3;
    };
    
    class C: public B {
    public:
        virtual void vfunc2();
        void func2();
    private:
        int m_data1, m_data4;
    };
    •  

    类A是基类,类B继承类A,类C又继承类B。类A,类B,类C,其对象模型如下图3所示。

    这里写图片描述
    图3:类A,类B,类C的对象模型

    由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类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()。 
    虽然图3看起来有点复杂,但是只要抓住“对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数”这个特点,便可以快速将这几个类的对象模型在自己的脑海中描绘出来。

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

    假设我们定义一个类B的对象。由于bObject是类B的一个对象,故bObject包含一个虚表指针,指向类B的虚表。

    int main() 
    {
        B bObject;
    }
    • 现在,我们声明一个类A的指针p来指向对象bObject。虽然p是基类的指针只能指向基类的部分,但是虚表指针亦属于基类部分,所以p可以访问到对象bObject的虚表指针。bObject的虚表指针指向类B的虚表,所以p可以访问到B vtbl。如图3所示。
    int main() 
    {
        B bObject;
        A *p = & bObject;
    }
    • 当我们使用p来调用vfunc1()函数时,会发生什么现象?
    int main() 
    {
        B bObject;
        A *p = & bObject;
        p->vfunc1();
    }
    •  

    程序在执行p->vfunc1()时,会发现p是个指针,且调用的函数是虚函数,接下来便会进行以下的步骤。 
    首先,根据虚表指针p->__vptr来访问对象bObject对应的虚表。虽然指针p是基类A*类型,但是*__vptr也是基类的一部分,所以可以通过p->__vptr可以访问到对象对应的虚表。 
    然后,在虚表中查找所调用的函数对应的条目。由于虚表在编译阶段就可以构造出来了,所以可以根据所调用的函数定位到虚表中的对应条目。对于 p->vfunc1()的调用,B vtbl的第一项即是vfunc1对应的条目。 
    最后,根据虚表中找到的函数指针,调用函数。从图3可以看到,B vtbl的第一项指向B::vfunc1(),所以 p->vfunc1()实质会调用B::vfunc1()函数。

    如果p指向类A的对象,情况又是怎么样?

    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()函数。

    可以把以上三个调用函数的步骤用以下表达式来表示:

    (*(p->__vptr)[n])(p)

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

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

    • 通过指针来调用函数
    • 指针upcast向上转型(继承类向基类的转换称为upcast,关于什么是upcast,可以参考本文的参考资料)
    • 调用的是虚函数

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

    五、总结

    封装,继承,多态是面向对象设计的三个特征,而多态可以说是面向对象设计的关键。C++通过虚函数表,实现了虚函数与对象的动态绑定,从而构建了C++面向对象程序设计的基石。

    参考资料

    附录

    示例代码

     
    展开全文
  • 最近碰到虚函数,看了一遍后,以为自己明白了,后来发现并没有,网上的帖子给了一些...2、每个类的对象,都有一个虚函数表指针,一般存在于对象实例中最前面的位置(不同编译器不同),指向该类的虚函数表虚函数表属...

    最近碰到虚函数,看了一遍后,以为自己明白了,后来发现并没有,网上的帖子给了一些帮助,但大部分说的不太清楚,来来回回理了好几遍,谨以此记录。

    1、函数一旦声明成了虚函数,在所有子类中都是虚函数,如果子类要覆盖,那行参和返回类型必须和父类一致,virtual关键字可写可不写。

    2、每个类的对象,都有一个虚函数表指针,一般存在于对象实例中最前面的位置(不同编译器不同),指向该类的虚函数表,虚函数表属于类,存储所有虚函数地址(即虚函数指针,怎么存的见4),虚函数指针占用类的内存空间,虚函数表不占用。

    3、如果一个父类指针指向了子类对象:

          3.1 此子类对象会做裁剪,会没有子类中独有的成员函数(A中有f()、g(),B继承A,B中有f()、g()、h(),A指针指向B对象,即 B b; A *a = &b,此时a是调用不到h的,无论h是否为虚函数)

          3.2 对于从父类继承过来的非虚函数(如果不是从父类继承过来的,参数3.1),此子类对象会调用父类的

          3.3 对于从父类继承过来的虚函数(如果不是从父类继承过来的,参照3.1),如果有重写,那会调用重写的,如果没重写,那就会调用基类的

          3.4 父类的析构函数必须为虚函数,不然就会出现3.2的情况,可能导致子类没有被析构

    4、每个类的虚函数表中如何存放虚函数指针的

         4.1 按虚函数声明顺序依次存放,父类在前,子类在后,以”.”作为虚函数表结尾标识符,不同编译器下使用不同标识符

    A::f()

    A::g()

    B::f1()

    B:g1()

          4.2 若子类中有覆盖父类的虚函数,那在子类的函数表中,子类中覆盖的虚函数会放到原父类虚函数的位置,没有被覆盖的函数没有变化 

    B::f()

    A::g()

    B::f1()

    B::g1()

          4.3 若多重继承 且 没有覆盖,每个父类都有自己的虚函数表;子类的虚函数表被放到第一个父类的虚函数表中

     

      -->   

    (此图片来源网络)

          4.4 若多重继承 且 有覆盖,会结合4.3 和4.2(与4.2不同的是,所有父类虚函数表中被覆盖的虚函数都会替换成子类的虚函数)

     -->   

    (此图片来源网络)

    附大牛地址:

    https://www.cnblogs.com/yinbiao/p/10987640.html

    https://blog.csdn.net/JackZhang_123/article/details/80692420

    后续还有新的收获再更新。。。。

     

     

    展开全文

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 9,557
精华内容 3,822
关键字:

虚函数表