美文网首页
Day1:C++虚机制的实现

Day1:C++虚机制的实现

作者: Bystander_1996 | 来源:发表于2020-04-29 23:13 被阅读0次

    1. 虚析构函数

    话不多说,贴代码:

    #include <iostream>
    using namespace std;
    class Parent  {
      public:
        Parent() { }
        virtual ~Parent() {cout << "Destruct the Parent!"<<endl; }
    };
    
    class Son :public Parent {
      public:
        Son() {}
        ~Son () {cout << "Destruct the Son!"<<endl; }
    };
    
    int main () {
        Parent * base = new Son();
        delete base;
        return 0;
    }
    

    可以看到执行的结果是先调用了子类的析构函数,再调用了父类的析构函数;
    如果将virtual去掉的话,将Parent类改为如下代码所示(子类不用加virthal ,因为 C++会默认为子类也是virthal)
    本着眼见为实的思想尝试了一下如果只把子类的析构函数加上virthal 进行修饰,会发现加了和没加的效果一样。证实了一点 父类函数被定义为虚函数对应的子类中的构成覆盖的函数会直接被默认为虚函数。反过来不成立。对于没有构成覆盖的函数,在继承关系中父类和子类的同名不覆盖的函数不构成重载
    class Parent  {
      public:
        Parent() { }
        ~Parent() {cout << "Destruct the Parent!"<<endl; }
    };
    

    子类的析构函数没有被调用。

    现象

    1. 当父类的析构函数不声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,只调动父类的析构函数,而不调动子类的析构函数
    2. 当父类的析构函数声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,先调动子类的析构函数,再调动父类的析构函数

    分析原因:

    1. 首先说明一点父类的构造函数和析构函数是子类无法继承的,也就是说每一个类都有自己独有的构造函数和析构函数。
    2. 如果基类中析构函数是虚函数的话,派生类的析构函数自动成为虚函数。基类就有一张虚函数表,派生类继承基类的时候会把自己的析构函数覆盖到虚函数表中,delete基类指针的时候就发生动态绑定,调用的就是该派生类析构函数而该派生类析构函数会先释放派生类对象再释放基类对象。这样的话就不会造成派生类的资源没有释放的问题。
    3. 但是基类指针指向栈上的派生类地址就不会有问题,因为栈上的派生类是自动释放的,自动调用派生类析构函数的,也自然会调用基类的析构函数。

    2. 构造函数为什么不能写成虚函数:

    1. 虚函数的作用在于通过父类的指针或引用来调用子类的对象。而构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用。
    2. 虚函数的实现中涉及一个指向vtable虚函数表的指针,这个指向vtable的指针其实是存储在对象的内存空间的。如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,就没有为该对象分配内存空间,也就不存在虚函数表指针,进而无法定位到该虚函数表(确切的说虚函数表的构建都是在构造函数体执行之前创建的),所以构造函数不能是虚函数。

    3. 虚函数的实现原理

    1. 拥有虚函数的类会有一个虚表,而且这个虚表存储在进程的只读数据段。。模块的数据段通常存放定义在该模块的全局数据和静态数据,这样我们可以把虚表看作是模块的全局数据或者静态数据

    2. 类的虚表会被这个类的所有对象所共享。类的对象可以有很多,但是他们的虚表指针都指向同一个虚表,从这个意义上说,我们可以把虚表简单理解为类的静态数据成员。值得注意的是,虽然虚表是共享的,但是虚表指针并不是,类的每一个对象有一个属于它自己的虚表指针。

    3. 虚表中存放的是虚函数的地址。其中在虚表的前四个字节存的是RTTI指针,指向的是RTTI信息,也就是一个运行时类型识别功能就是通过RTTI(运行时类型识别)指针来是实现的。接下来的四个字节是一个偏移量,表示虚函数指针在对象中的偏移量,最后才是虚函数的地址!

    4. 虚表的地址被存放在对象的起始位置,即对象的第一个数据成员就是它的虚表指针。 虚表指针的初始化发生在构造函数的调用过程中, 但是在执行构造函数体之前,即进入到构造函数的"{"和"}"之前。

    5. 虚函数实现:通过动态绑定实现、动态绑定通过vfptr(虚函数表指针)和vftable(虚函数表)来实现的;

    6. 虚函数调用方式:通过基类指针或引用,执行时会根据指针指向的对象的类,决定调用哪个函数

    总结: 若在父类中定义了虚函数,在编译的时候会生成虚函数表保存在类模块定义的数据段中,所有该类的实例共享该虚函数表,同时每个类都会有一个虚表指针(虚函数表指针实在实例化的时候放到类对象的内存中的)指向该虚函数表(编译器保证虚函数表的指针存在于对象实例中最前面的位置);子类继承了该父类之后也会继承该虚函数表(在多继承的情况下,子类的实例中最前面的内存位置会存放多个虚函数表指针,每个虚函数表指针指向一个从父类继承过来的虚函数表),在实例化一个子类的时候,若子类定义的函数对其所继承父类构成了覆盖,该子类就会修改子类虚函数表中指向虚函数的地址,将原来父类的实现方法进行替换。

    总结2:

    1. 所有同一个类实例化对象用的是同一个虚函数表,也就是这些对象的虚函数指针指向同一区域;
    2. 即是子类修改了虚函数表中的数据,我们依然可以通过Base::function(类名+函数名)的方式进行访问,这时候实现是不通过虚函数表去找函数的地址了。所以说虚函数可以通过虚表调用,也可以通过访问普通成员函数的方式进行访问。
    3. 调用虚函数的前提是必须是用指针或引用调用,因为如果是通过对象调用虚函数,那么编译的时候就知道应该用哪个方法了,这是静态绑定,不是动态绑定。
    4. 子类会继承父类的虚函数表,子类如果重写了(重新定义了某个虚函数),那么子类的虚函数表中对应的位置的虚函数会发生替换。
    5. 对于多重继承,如果多个基类都有虚函数,则继承类中包含多个基类虚函数表,子类的虚函数地址放在声明的第一个有虚函数的基类的虚函数表后面
    6. 多态的实现时,无法通过父类的指针去找到子类独有的元素

    虚函数表在编译的时候就确定了,而类对象的虚函数指针是在运行阶段赋值根据虚函数表加载到内存中的地址进行赋值的。先调用基类构造函数把基类的虚函数表地址赋值到虚函数表指针上,但又在自身构造函数或初始化列表之前,再次让虚函数表指针指向派生类类型的虚函数表,对继承过来的基类的虚函数表进行函数的覆盖,这是实现多态的关键!


    参考:

    1. C++ 虚函数表 vfptr

    2. 多态实现

    3. C++之多态的原理及其分析

    4. 虚函数表是什么时候建立的

    https://www.cnblogs.com/laiqun/p/5887372.html

    相关文章

      网友评论

          本文标题:Day1:C++虚机制的实现

          本文链接:https://www.haomeiwen.com/subject/qxlnwhtx.html