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

  • 虚函数

    • OOP的核心思想是多态性(polymorphism)。把具有继承关系的多个类型称为多态类型。引用或指针的静态类型与动态类型不同这一事实正是C++实现多态性的根本。
    • C++ 的多态实现即是通过虚函数。在C++中,基类将类型相关的函数与派生类不做改变直接继承的函数区别对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明为虚函数(virtual function)。
    • C++在使用基类的引用或指针调用一个虚函数成员函数时会执行动态绑定。因为只有直到运行时才能知道调用了那个版本的虚函数,所以所有的虚函数必须有定义。
    • 动态绑定只有当通过指针或引用调用虚函数时才会发生。
    • 一旦某个函数被声明为虚函数,则在所有派生类中它都是虚函数。所以在派生类中可以再一次使用virtual指出,也可以不用。
    • 如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。换句话说,如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使实际运行的是派生类的函数版本也是如此。此时,传入派生类函数的将是基类函数定义的默认实参。
    • 在某些情况下,我们希望对虚函数的调用不进行动态绑定,而是强迫其执行虚函数的某个特定版本。
      //强行调用基类中定义的函数版本而不管baseP的动态类型到底是什么
      double price = basePtr->Base::net_price();

      通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数的机制。

  • 抽象基类

    • 纯虚函数:一个纯虚函数无须定义。通过在函数体的位置(即在声明语句的分号之前)书写 =0 将一个虚函数说明为纯虚函数。其中 =0 只能出现在类内部的虚函数声明语句处。
    • 值得注意的是,我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部,不能在类的内部为一个 =0 的函数提供函数体。
    • 含有纯虚函数的类是抽象基类。
      • 含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类负责定义接口,而后续的类可以覆盖接口。我们不能(直接)创建一个抽象基类的对象。
        //Base 声明了纯虚函数,而 Derive将覆盖该函数
        Base b; //错误,不能定义Base的对象
        Derive d; //正确,Derive中没有纯虚函数
  • 虚函数表指针和虚函数表

    • 对于每一个定义了虚函数的类,编译器会为其创建一个虚函数表,该虚函数表被所有的类对象所共享,即它不是跟着对象走的,而是相当于静态成员变量,是跟着类走的。

    • 虚函数表指针vptr,每一个类的对象都有一个虚函数表指针,该指针指向类的虚函数表的位置。为了实现多态,当一个对象调用某个虚函数时,实际上是根据该虚函数指针vptr所指向的虚函数表vtable里找到相应的函数指针并调用之。

    • 关于vptr在对象内存布局中的存放位置,一般都是放在内存布局的最前面,当然,也可能有其他实现方式。

    • 基类定义如下所示:

      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对象其内存布局方式为:

    • 考虑继承的情况,如下所示

      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对象其内存布局如下所示:

      • 其实Derive对象的内存布局是可以这样理解,但是也不是很准确。

        如上所示,在Derive的定义中,我重新实现了Base的fun1(),直接继承了Base::fun2(),再新定义了 Derive::fun3()

        通过调试,即上面的右图发现,在Derive的对象中,能够看到的虚函数表是从Base继承而来的,其中里面覆写fun1(),继承了fun2(),但是并没有fun3()的函数指针。所以按照上边的左图,给出内存布局的话,可能会有一些误导。

      • 当派生类继承基类时,如果覆写了基类中的虚函数,在基类的虚函数表中,会使用覆写的函数覆盖基类对应的虚函数,如果没有覆写,则直接继承基类的虚函数。如上图所示的fun1 和 fun2 则是这种情况。

      • 当派生类再定义新的虚函数时,此时在基类的虚函数表中是无法体现出来的。所以,此时编译器会为派生类维护不止一个属于派生类的虚函数表,其中的有从基类继承而来的虚函数表,但是跟基类的不同,因为其中可能有函数覆写。另外则有一个用来记录当前派生类新定义的虚函数,函数 fun3即属于这种情况。当然,新维护的虚函数表的位置由编译器决定,也可以直接接到继承而来的虚函数表的后面,即也就只有一个表,但是这跟编译器的具体实现有关。所以,有那个意思就行了,不用太过深究具体实现细节。一般情况下,按照上面左图形式理解即可。

      • 由上可知,派生类如果没有定义新的虚函数,则直接继承虚类的虚函数表,并在其中做相应修改。如果定义了新的虚函数,不止要继承虚类的,还要维护自己的。

        所以上面的Derive的内存布局的另一种情况可能是:

    • 下面给出一个多重继承的讨论情况:

      class Base1{
      public:
      Base1()
      {} virtual void fun1(){
      cout << "Base1::fun1()" << endl;
      } virtual void fun2(){
      cout << "Base1::fun2()" << endl;
      }
      }; class Base2{
      public:
      Base2(){} 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问题

      1.  class Base{
      public:
      virtual void fun(){}
      private:
      int a;
      }; 很明显: sizeof(Base) = 8
      原因:带有虚函数的类具有虚函数指针,然后再加上int 2. 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个。 3. 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下查看内存布局如下:



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

C++ 虚函数相关,从头到尾捋一遍的更多相关文章

  1. destoon代码从头到尾捋一遍

    destoon® B2B网站管理系统(以下简称destoon)由西安嘉客信息科技有限责任公司独立研发并推出,对其拥有完全知识产权,中国国家版权局计算机软件著作权登记号:2009SR037570. 系统 ...

  2. 索引很难么?带你从头到尾捋一遍MySQL索引结构,不信你学不会!

    前言 Hello我又来了,快年底了,作为一个有抱负的码农,我想给自己攒一个年终总结.自上上篇写了手动搭建Redis集群和MySQL主从同步(非Docker)和上篇写了动手实现MySQL读写分离and故 ...

  3. 带你从头到尾捋一遍MySQL索引结构(1)

    从一个简单的表开始 create table user( id int primary key, age int, height int, weight int, name varchar(32) ) ...

  4. 带你从头到尾捋一遍MySQL索引结构(2)

    前言 Hello我又来了,快年底了,作为一个有抱负的码农,我想给自己攒一个年终总结.索性这次把数据库中最核心的也是最难搞懂的内容,也就是索引,分享给大家. 这篇博客我会谈谈对于索引结构我自己的看法,以 ...

  5. 带你从头到尾捋一遍MySQL索引结构

    索性这次把数据库中最核心的也是最难搞懂的内容,也就是索引,分享给大家. 这篇博客我会谈谈对于索引结构我自己的看法,以及分享如何从零开始一层一层向上最终理解索引结构. 从一个简单的表开始 create ...

  6. iOS:从头捋一遍VC的生命周期

    一.介绍 UIViewController是iOS开发中的核心控件,没有它那基本上任何功能都无法实现,虽然系统已经做了所有控件的生命维护,但是,了解它的生命周期是如何管理还是非常有必要的.网上有很多教 ...

  7. iOS:捋一遍View的生命周期

    一.介绍 前面介绍了VC的生命周期,闲着没事也来捋一捋View的生命周期,简单用两个类型的View来监测.一个View纯代码创建,另一个View使用Xib创建. 二 .代码 MyCodeView:  ...

  8. C++ 虚函数相关

    多态 C++的封装.继承和多态三大特性,封装没什么好说的,就是把事务属性和操作抽象成为类,在用类去实例化对象,从而对象可以使用操作/管理使用它的属性. 至于继承,和多态密不可分.基类可以进行派生,而派 ...

  9. C++虚函数相关内容

    样例代码 class Base{public: Base(){}; virtual ~Base(){    //若没有设置为虚函数:如果有这样的指针Base *p=new Derived();声明,则 ...

随机推荐

  1. path和classpath的用途

    1 path很明显是unix shell的环境变量,比如bash shell,输入一个命令,它会先去path指定的目录下查找是不是有该命令的可执行文件. 2 -classpath 只是用在下面这种不发 ...

  2. devexpress chartcontrol实现非连续点数据的显示

    1.先上图: 其中暗红的曲线中数据不连续的,在实际开发中可能也会遇到这种情况,由于断电或其他原因导致部分日期数据无法采集,如果按一般情况来显示可能会显示如下图所示: 图中可以看出非连续曲线中,3月5号 ...

  3. idea常用设置

    Idea删除当前行的快捷键是Ctrl+y,复制当前行的快捷键是Ctrl+d,和eclipse的习惯不一样.虽然可以一键把idea的快捷键映射成eclipse,但是这样做代价太大,如果这样,idea的官 ...

  4. 基于Blod的ajax进度条下载实现

    普通的浏览器下载 在web开发中,如果要实现下载功能,往往都是使用新开web页面或者是使用iframe的形式.实现起来其实很简单: <a target="_blank" hr ...

  5. 关注云端搜索技术:elasticsearch,nutch,hadoop,nosql,mongodb,hbase,cassandra 及Hadoop优化

    http://www.searchtech.pro/ Hadoop添加或调整的参数: 一.hadoop-env.sh1.hadoop的heapsize的设置,默认1000 # The maximum ...

  6. console深入理解

    ["$$", "$x", "dir", "dirxml", "keys", "values ...

  7. shell 获取指定目录下文件名

    有两个目录a.b,两个文件夹目录里有一些文件的文件名是一样,不过后缀名不同,我想把a文件夹下跟b文件夹里相同文件名的文件覆盖到b去,并删除b里同名而不同后缀的文件,文件很多    #!/bin/bas ...

  8. Regular Expression(正则表达式)之邮箱验证

    正则表达式(regular expression, 常常缩写为RegExp) 是一种用特殊符号编写的模式,描述一个或多个文本字符串.使用正则表达式匹配文本的模式,这样脚本就可以轻松的识别和操作文本.其 ...

  9. VUE2.0实现购物车和地址选配功能学习第七节

    第七节 卡片选中,设置默认 1.卡片选中html:<li v-for="(item,index) in filterAddress" v-bind:class="{ ...

  10. C#计算表达式(仿计算器功能)

    一.用MSScriptControl在C#中执行JavaScript代码javascript中有个eval方法用过的人都知道他的方便和强大之处.在C#中,我们也可以通过Com组件来执行一段javasc ...