C++ 虚函数相关,从头至尾捋一遍

众所周知,C++虚函数是一大难点,也是面试过程当中必考部分。这次,从虚函数的相关概念、虚函数表、纯虚函数、再到虚继承等等跟虚函数相关部分,作一个比较细致的整理和复习。面试

  • 虚函数
    • OOP的核心思想是多态性(polymorphism)。把具备继承关系的多个类型称为多态类型。引用或指针的静态类型与动态类型不一样这一事实正是C++实现多态性的根本。
    • C++ 的多态实现便是经过虚函数。在C++中,基类将类型相关的函数与派生类不作改变直接继承的函数区别对待。对于某些函数,基类但愿它的派生类各自定义适合自身的版本,此时基类就将这些函数声明为虚函数(virtual function)。
    • C++在使用基类的引用或指针调用一个虚函数成员函数时会执行动态绑定。由于只有直到运行时才能知道调用了那个版本的虚函数,因此全部的虚函数必须有定义。
    • 动态绑定只有当经过指针或引用调用虚函数时才会发生。
    • 一旦某个函数被声明为虚函数,则在全部派生类中它都是虚函数。因此在派生类中能够再一次使用virtual指出,也能够不用。
    • 若是某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。换句话说,若是咱们经过基类的引用或指针调用函数,则使用基类中定义的默认实参,即便实际运行的是派生类的函数版本也是如此。此时,传入派生类函数的将是基类函数定义的默认实参。
    • 在某些状况下,咱们但愿对虚函数的调用不进行动态绑定,而是强迫其执行虚函数的某个特定版本。
      cpp //强行调用基类中定义的函数版本而无论baseP的动态类型究竟是什么 double price = basePtr->Base::net_price();
      一般状况下,只有成员函数(或友元)中的代码才须要使用做用域运算符来回避虚函数的机制。
  • 抽象基类
    • 纯虚函数:一个纯虚函数无须定义。经过在函数体的位置(即在声明语句的分号以前)书写 =0 将一个虚函数说明为纯虚函数。其中 =0 只能出如今类内部的虚函数声明语句处。
    • 值得注意的是,咱们也能够为纯虚函数提供定义,不过函数体必须定义在类的外部,不能在类的内部为一个 =0 的函数提供函数体。
    • 含有纯虚函数的类是抽象基类。
      • 含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,然后续的类能够覆盖接口。咱们不能(直接)建立一个抽象基类的对象。
        cpp //Base 声明了纯虚函数,而 Derive将覆盖该函数 Base b; //错误,不能定义Base的对象 Derive d; //正确,Derive中没有纯虚函数
  • 虚函数表指针和虚函数表
    • 对于每个定义了虚函数的类,编译器会为其建立一个虚函数表,该虚函数表被全部的类对象所共享,即它不是跟着对象走的,而是至关于静态成员变量,是跟着类走的。
    • 虚函数表指针vptr,每个类的对象都有一个虚函数表指针,该指针指向类的虚函数表的位置。为了实现多态,当一个对象调用某个虚函数时,其实是根据该虚函数指针vptr所指向的虚函数表vtable里找到相应的函数指针并调用之。
    • 关于vptr在对象内存布局中的存放位置,通常都是放在内存布局的最前面,固然,也可能有其余实现方式。
    • 基类定义以下所示:
      ```cpp
      class Base{
      public:
      Base()
      :a(0), b(0), c('\0'){}函数

      virtual void fun1(){
           cout << "Base::fun1()" << endl;
       }
      
       virtual void fun2(){
           cout << "Base::fun2()" << endl;
       }

      private:
      int a;
      double b;
      char c;
      };
      ```
      类Base对象其内存布局方式为:
      布局

    • 考虑继承的状况,以下所示
      ```cpp
      class Derive : public Base{
      public:
      Derive()
      :Base(),d(0), f(0){}学习

      virtual void fun1(){
            cout << "Derive::fun1()" << endl;
        }
      
        virtual void fun3(){
            cout << "Derive::fun3()" << endl;
        }

      private:
      int d;
      float f;
      };
      ```
      类Derive对象其内存布局以下所示:
      .net

      • 其实Derive对象的内存布局是能够这样理解,可是也不是很准确。
        如上所示,在Derive的定义中,我从新实现了Base的fun1(),直接继承了Base::fun2(),再新定义了 Derive::fun3()
        经过调试,即上面的右图发现,在Derive的对象中,可以看到的虚函数表是从Base继承而来的,其中里面覆写fun1(),继承了fun2(),可是并无fun3()的函数指针。因此按照上边的左图,给出内存布局的话,可能会有一些误导。指针

      • 当派生类继承基类时,若是覆写了基类中的虚函数,在基类的虚函数表中,会使用覆写的函数覆盖基类对应的虚函数,若是没有覆写,则直接继承基类的虚函数。如上图所示的fun1 和 fun2 则是这种状况。
      • 当派生类再定义新的虚函数时,此时在基类的虚函数表中是没法体现出来的。因此,此时编译器会为派生类维护不止一个属于派生类的虚函数表,其中的有从基类继承而来的虚函数表,可是跟基类的不一样,由于其中可能有函数覆写。另外则有一个用来记录当前派生类新定义的虚函数,函数 fun3即属于这种状况。固然,新维护的虚函数表的位置由编译器决定,也能够直接接到继承而来的虚函数表的后面,即也就只有一个表,可是这跟编译器的具体实现有关。因此,有那个意思就好了,不用太过深究具体实现细节。通常状况下,按照上面左图形式理解便可。
      • 由上可知,派生类若是没有定义新的虚函数,则直接继承虚类的虚函数表,并在其中作相应修改。若是定义了新的虚函数,不止要继承虚类的,还要维护本身的。
        因此上面的Derive的内存布局的另外一种状况多是:
        调试

    • 下面给出一个多重继承的讨论状况:
      ```cpp
      class Base1{
      public:
      Base1()
      {}code

      virtual void fun1(){
            cout << "Base1::fun1()" << endl;
        }
      
        virtual void fun2(){
            cout << "Base1::fun2()" << endl;
        }

      };对象

      class Base2{
      public:
      Base2(){}blog

      virtual void fun3(){
            cout << "Base2::fun3()" << endl;
        }
      
        virtual void fun4(){
            cout << "Base2::fun4()" << endl;
        }

      };

      class Derive : public Base1, public Base2(){
      public:
      Derive()
      :Base1(), Base2() {}

      virtual void fun2(){
            cout << "Derive::fun2()" << endl;
        }
      
        virtual void fun3(){
            cout << "Derive::fun3()" << endl;
        }
      
        virtual void fun5(){
            cout << "Derive::fun5()" << endl;
        }

      }
      ```
      Derive的对象内存布局以下:

      注意:
      • 注意派生类和基类的覆盖关系和继承关系
      • 关于字节对齐问题,虚函数表指针,做为隐藏成员加入到类对象中,而隐藏成员的加入不能影响其后成员的字节对齐,因此,虚函数表指针老是占有最大字节对齐数的内存。
  • 虚继承
    • 这是篇好文章C++ 多继承和虚继承的内存布局,虽然不是很懂,可是确实有帮助。下面在给出一些相关概念。

    • 概念:为了解决从不一样途径继承来的同名的数据成员在内存中有不一样的拷贝形成数据不一致的问题,将共同基类设置为虚基类。此时,从不一样途径继承过来的同名数据成员在内存中只有一个拷贝,同一个函数名也只有一个映射。解决了二义性问题,同时,也节省了内存,避免了数据不一致的问题。
    • C++ 对象的内存布局(下)关于虚拟继承的例子部从这篇文章学习,推荐。

    • 总结以下:
      • 不管是GCC仍是VC++,除了一些细节上的不一样,其大致上的对象布局是同样的。都是从Base1, 到Base2, 再到 Derive, 最后是虚基类 Base。
      • 关于虚函数表,尤为是第一个,GCC和VC++有很大的不同。
  • 讨论
    • 带有虚函数的类的sizeof问题
      ```cpp
      1. class Base{
        public:
        virtual void fun(){}
        private:
        int a;
        };

      很明显: sizeof(Base) = 8
      缘由:带有虚函数的类具备虚函数指针,而后再加上int

      1. class Base{
        public:
        virtual void fun(){}
        private:
        int a;
        double b;
        };

      乍一看 sizeof(Base) = 16, 其实应该是 sizeof(Base) = 24
      为何呢, 由于前面关于字节对齐中,提到过 类的隐藏对象不能影响其后的数据成员的对齐,因此通常隐藏对象都是最大对齐字节的整数倍。此时 最大对齐为8,因此 虚函数表指针占4个字节,但须要填充4个。而后 int 占 4 个,再填充 4 个,最后double占8个。一共24个。

      1. class A {
        int a;
        virtual ~A(){}
        };

        class B:virtual public A{
        virtual void funB(){}
        };

        class C:virtual public A{
        virtual void funC(){}
        };

        class D:public B,public C{
        virtual void funD(){}
        };

        sizeof(A) = 8
        sizeof(B) = 12
        sizeof(C) = 12
        sizeof(D) = 16

        A 中是虚函数指针 + int
        B、C 虚继承A,大小为 A + 指向虚基类的指针,B、C虽然新定义了虚函数,可是共享A中的虚函数指针。
        D 因为是普通继承 B、C,可是因为 B 、C是虚继承,因此D中保留A的一个副本。因此大小为 A + B指向虚基类的指针 + C指向虚基类的指针
        ```

    • 最后给出一个上面讨论 2 的具体实例。在VS2013下查看内存布局以下:


      上图中没有搞懂的部分,应该是随机数,系统随机的。不用管。

相关文章
相关标签/搜索