众所周知,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. phpcms v9更改后台文章排序的方法

    后台文章排序怎么才可以按自己输入的数字排列?如按4,3,2,1,从大到小排列?实现方法如下: 修改文件: phpcms\modules\content 中的 content.php 代码如下: $da ...

  2. BZOJ 2142: 礼物

    模非素数下的排列组合,简直凶残 调着调着就过了= = 都不知道怎么过的= = 直接上链接http://hi.baidu.com/aekdycoin/blog/item/147620832b567eb4 ...

  3. ASP.NET MVC 项目直接预览PDF文件

    背景及需求 项目使用的是MVC4框架,其中有一个功能是根据设置生成PDF文件,并在点击时直接预览. 实现过程 1.第一版实现代码: HTML内容 @{ Layout = null; } <!DO ...

  4. Angular.js!(附:聊聊非原生框架项目)

    最近,为了项目接触了一个很火的前端框架Angular.js,下面就Angular做一个简介吧(大牛请绕步,只针对没有接触过angular的人). Angular.js是一款精简的前端框架,如果要追溯它 ...

  5. 【openstack N版】——计算服务nova

    一.openstack计算服务nova 1.1nova介绍 Nova是openstack最早的两块模块之一,另一个是对象存储swift.在openstack体系中一个叫做计算节点,一个叫做控制节点.这 ...

  6. 前端总结·基础篇·CSS(三)补充

    前端总结系列 前端总结·基础篇·CSS(一)布局 前端总结·基础篇·CSS(二)视觉 前端总结·基础篇·CSS(三)补充 目录 一.移动端 1.1 视口(viewport) 1.2 媒体查询(medi ...

  7. Cloudera Manager安装_搭建CDH集群

    2017年2月22日, 星期三 Cloudera Manager安装_搭建CDH集群 cpu   内存16G 内存12G 内存8G 默认单核单线 CDH1_node9 Server  || Agent ...

  8. C#基础——类

    第一部分:String类 系统内置的处理字符串类型的函数方法类.方便我们对字符串类型进行一系列的处理. +++++String类+++++黑色小扳手 - 属性紫色立方体 - 方法 1.***字符串.L ...

  9. Java(基础)的类与变量

    Java的类与成员变量 在我们学习编程语言中,需要灵活自用,那么怎么来灵活的将所有的函数属性来调用来实现完整的工程呢? 所以我们需要认识到类和变量的定义 1.类是什么? 类是抽象的概念,而对象就是类的 ...

  10. ERP项目案例:澳科利辊业科技有限公司

    企业简介: 上海澳科利公司成立于1995年,在主要股东LASERLIFE的支持下,创始人归霆先生带领他的精英团队--一支陶瓷网纹辊专业制造队伍和资深专业的柔版印刷服务机构,致力于发展中国包装印刷业,服 ...