本博客主要通过查看类的内容的变化,深入探讨有关虚指针和虚表的问题。

一、虚继承产生的虚基类表指针和虚基类表

如下代码:写一个棱形继承,父类Base,子类Son1和Son2虚继承Base,又来一个类Grandson继承Son1和Son2。
代码:

class Base
{public:int a;
protected:int b;
private:int c;
};class Son1 :virtual public Base
{public:int a;
protected:int b;
private:int c;
};class Son2 :virtual public Base
{public:int a;
protected:int b;
private:int c;
};class Grandson :public Son1,public Son2
{public:int a;
protected:int b;
private:int c;
};

首先对Base的内容是肯定的,三个整形变量,没有其它东西了,如下图:

再来看看类Son1和Son2的东西,由于Son1和Son2的类本身内容相同,且都是虚继承Base类,所以Son1和Son2类的内容相同。如下两个图:


上两个图是Son1类和Son2类的内容,这两个类创建的对象使用从Base类继承的内容就是委婉地通过vbptr到vbtale中找到继承的Base类内容的首地址访问的。
虚基类表中的16是相对于Son类vbptr地址的偏移量,即继承的Base类的内容在子类中存放的首地址相对于Son的vbptr的地址的偏移量。Son1和Son2都继承父类Base,本来类大小为sizeof(Son)+sizeof(Base)=12+12=24的,但是由于是虚继承,Son类内会多一个虚指针,所以类大小多了4字节,变成了28字节。

再来看看Grandson类的内容:

从Son1继承的虚基类表里面的44是指Base类的内容到Son1的vbptr的地址偏移量为44。同理,从Son2继承的虚基类表里面的28是指Base类的内容到Son1的vbptr地址偏移量为28。这里可以看出,本来Son1和Son2类里面都有从Base继承的内容,但是由于Son1和Son2都是虚继承,Grandson继承Son1和Son2的时候,Base类的内容只有一份,避免了继承两份Base内容(Son1含一份,Son2含一份)而造成的资源浪费。不管Grandson是通过Son1还是通过Son2访问Base的内容,其实Son1和Son2都是通过自己的vbptr访问同一份Base的内容。
那么,设想一下,如果Grandson也是以虚继承的方式继承Son1和Son2的呢?即把Grandson的继承代码改成以下方式:

class Grandson :virtual public Son1,virtual public Son2
{public:int a;
protected:int b;
private:int c;
};

Son1和Son2以虚继承的方式继承Base造成的结果是Son1和Son2都多了一个虚基类指针指向虚基类表,虚基类表中记录着从Base继承过来的内容的首地址对Son1(Son2)类的vbptr的地址的偏移量。类比一下,不难想出,Grandson如果也以虚继承的方式继承Son1和Son2的话,那么Grandson本身肯定也会多一个虚基类指针,指向一个虚基类表,这个虚基类表中存放着从Son1继承过来的内容的首地址相对于Grandson类的vbprtr地址的偏移量,也存放着从Son2继承过来的内容的首地址相对于Grandson类的首地址的偏移量。除此之外还存放着一个东西,Base的内容的首地址相对于Grandson类的vbptr的地址的偏移量。所以Grandson的虚基类指针指向虚基类表,虚基类表中存放着三个地址偏移量:

如上图(Son1和Son2的虚基类指针和虚基类表在上面分析过了,这里主要分析Grandson的虚基类指针和虚基类表),虚基类表中的16、28、44分别对应着包含的Base、Son1、Son2的内容的首地址相对于Grandson的vbptr地址的偏移量。

二、虚函数产生的虚函数指针和虚函数表

注: 在这里不讨论动态多态和静态多态,只讨论分析虚指针和虚表的问题。

首先,vbptr(virtual base pointer)虚基类指针和vfptr(virtual function pointer)虚函数指针是两个不同的东西,是可以共存的,上面已经说明vbptr是对于虚继承而言的产物,vfptr则是对于虚函数的产物。以下代码说明vbptr和vfptr可以共存:

class Base
{public:int a;
protected:int b;
private:int c;
};class Son1 :virtual public Base //虚继承(产生虚基类指针vbptr)
{virtual void fun() {}//虚函数(产生虚函数指针vfptr)
public:int a;
protected:int b;
private:int c;
};

看一下Son1的结构:

可以看出vbptr和vfptr是两个不同的指针,而且可以共存。
vbptr指向的是虚基类表,虚基类表中放着Base内容的首地址相对于Son1vbptr指针的地址的偏移量。
vfbptr指向的是虚函数表,虚函数表中放着Son1的虚函数的地址。
由于虚基类指针已经分析过,下面只针对虚函数指针展开分析。如下代码,父类函数虚函数,子类继承父类。

class Base
{public:virtual void fun1() {}virtual void fun2() {}
public:int a;
protected:int b;
private:int c;
};class Son1 :public Base
{public:int a;
protected:int b;
private:int c;
};

看一下Base类的结构:

可以看出,由于Base类内函数虚函数,所以Base类会多一个虚函数指针,虚函数指针指向虚函数表,虚函数表中存放着虚函数的地址。
再来看看Son1类里面的东西:

可以看见Base的虚函数指针被Son1继承下来了,虚函数指针指向一个虚函数表,虚函数表中放着的是Base的两个虚函数的地址。那么如果Son1对Base的虚函数进行重写呢?比如对fun1函数进行重写:

class Base
{public:virtual void fun1() {}virtual void fun2() {}
public:int a;
protected:int b;
private:int c;
};class Son1 :public Base
{public:void fun1() {}//对Base的虚函数fun1函数进行重写
public:int a;
protected:int b;
private:int c;
};


可以发现,虚函数表中本来存放Base的虚函数fun1()的地址,Son1对Base的fun1函数进行重写后,虚函数表中原本存放Base的虚函数fun1的地址,现在存放了被Son1重写后的fun1函数的地址。这也就是我们说的当子类重写父类的某一个虚函数时,父类的这个虚函数会被重写后的函数覆盖。
再想一下,如果Son1本身也有虚函数呢?和vbptr类比一下,会觉得如果Son1本身有虚函数的话,那么Son1本身也会有一个虚函数指针,指向一个虚函数表,虚函数表中放着Son1的虚函数的地址。但是事实并非如此!
如下所示:

class Base
{public:virtual void fun1() {}//对Base的虚函数fun1函数进行重写virtual void fun2() {}//Son1自己的虚函数
public:int a;
protected:int b;
private:int c;
};class Son1 :public Base
{public:void fun1() {}virtual void fun3() {}
public:int a;
protected:int b;
private:int c;
};

看一下Son1的结构:

可以看见,Son1本身还是没有vfptr,而是仍旧用从Base继承过来的vfptr,而且函数表也是只有一个,Son1的虚函数fun3的地址直接添加在虚函数表里面(在其他函数地址下面)。由此可见,一个类如果有虚函数,并且继承的有函数指针,那么这个类会继续用继承的vfptr,而且自己的虚函数地址会继续存放在同一个虚函数表中。但是不意味着每个类都只有一个虚函数指针和一个虚函数表,比如,两个类都有虚函数指针和虚函数表,又来一个"孙子"类继承这两个类,那么"孙子"类里面就会有两个虚函数指针,两个虚函数表。
如下代码:

class Base
{public:virtual void fun1() {}virtual void fun2() {}
public:int a;
protected:int b;
private:int c;
};class Son1 :public Base
{public:virtual void fun3() {}
public:int a;
protected:int b;
private:int c;
};class Son2 :public Base
{public:virtual void fun4() {}
public:int a;
protected:int b;
private:int c;
};class Grandson :public Son1,public Son2
{public:void fun1(){}virtual void fun5() {}
public:int a;
protected:int b;
private:int c;
};

Son1和Son2的结构不用看了,上面已经分析过了,用继承的base的vfptr,虚函数地址和base的虚函数地址也存放在同一个虚函数表中。看一下Grandson的结构:


可以看见,Grandson从Son1和Son2分别d都继承了一份虚函数表,和分别都继承了一个虚函数指针。总共两个虚函数指针和两个虚函数表。而Grandson重写后的fun1函数将从Son1继承过来的虚函数表中的fun1覆盖了,而Grandson自己的虚函数fun5()也存放在从Son1继承过来的虚函数表中。在这里有一个规则:
1.虚函数地址按照其声明顺序放于虚函数表中。
2.父类的虚函数在子类的虚函数前面。
3.被重写的函数放到了虚函数表中原来父类虚函数的位置。即原来的虚函数的地址直接被覆盖。

发现从Son2继承过来的虚函数表中的fun1位置被
&thunk: this-=28;goto GrandSon::fun1替代
thunk我查了一下,说是一种thunk技术。thunk是一组动态生成的ASM指令,它记录了窗口类对象的this指针,这组指令既可以当作函数,也可以是窗口过程来使用。(如果真是这个知识的话,那么这里已经超出本人理解范围了)。猜测就是Son2的函数表里面的fun1并没有被覆盖,而是用了另一种技术调用Grand::fun1。this -= 28这句代码中,this应该是Son2的vfptr,而Son2的vfptr -= 28,那么就指向了Son1的vfptr,goto Grandson::fun1就是转到Grandson::fun1函数。
所以本人猜测,是通过回转到Son1的vfptr指向的虚函数表中找到Grandson::fun1函数的地址,然后调用。对于thunk,在其中不知道发挥着怎样的作用,已经超出本人知识范围了,网上搜了一下,也看的迷迷糊糊的。
总之,是通过某种方式调用Grand::fun1,而并不是简单的覆盖Son2的fun1就是了。

总结:
1.如果基类有虚函数表,那么子类直接使用基类的虚函数表。且实例中虚函数表地址值存储顺序就是基类继承顺序。
2.继承类新增的虚函数排在第一个虚函数表中,且在基类虚函数后面。
3.子类重写父类的虚函数,只有按顺序第一个包含被重写的虚函数的地址的虚函数表中的原虚函数地址被覆盖,后面的虚函数表中如果也包含这个虚函数地址,那么就不是简简单单的覆盖,而是用更像是回调的方式调用重写后的函数。
注: 本博客中如有错误,欢迎指出一起讨论学习。本博客主要以查看类的内容的方式理解分析虚指针和虚表。查看通过代码讲解验证的博客,请点击此处查看优秀博客。

C++中虚继承产生的虚基类指针和虚基类表,虚函数产生的虚函数指针和虚函数表相关推荐

  1. C++中的各种“虚“-- 虚函数、纯虚函数、虚继承、虚基类、虚析构、纯虚析构、抽象类讲解

    C++中的各种"虚" 1. 菱形继承 1.1 虚继承 && 虚基类 1.2 虚基类指针(vbptr)&& 虚基类表(vbtable) 2. 多态 2 ...

  2. C++虚继承和虚基类详解(二)

    虚继承(Virtual Inheritance) 为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员. 在继承方式前面加上 virtual 关键字就 ...

  3. C++虚继承和虚基类;虚函数与继承

    ref http://blog.csdn.net/owen7500/article/details/52432970?locationNum=4&fps=1 http://blog.csdn. ...

  4. C++学习笔记day47-----C++98-继承中的构造函数,析构函数,拷贝构造函数,拷贝赋值函数,多重继承,虚继承

    继承中的构造函数 当通过一个子类创建一个新的对象时,编译器会根据子类在初始化表中指明的基类的初始化方式去调用基类相应的构造函数.如果子类的初始化表中,并没有指明基类的初始化方式,编译器将会调用基类的无 ...

  5. C++虚继承和虚基类

    多继承(Multiple Inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员.尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名 ...

  6. 【C++】继承和派生、虚继承和虚基类、虚基类表和虚基类指针

    继承和派生.虚继承和虚基类.虚基类表和虚基类指针 继承和派生 继承概述 继承基本概念 派生类中的成员 继承的内容 派生类定义 派生类访问控制 对象构造和析构 对象构造和析构的调用顺序 继承中的构造和析 ...

  7. C++ 面向对象(一)继承:继承、对象切割、菱形继承、虚继承、继承与组合

    目录 继承 继承的概念 继承方式 基类与派生类的赋值转换 作用域与隐藏 派生类的默认成员函数 友元与静态成员 友元 静态成员 多继承 菱形继承 虚继承 继承和组合 什么是组合 如何选择组合和继承 继承 ...

  8. C++基本概念复习之二:多重继承、虚继承、纯虚函数(抽象类)

    一.多重继承: #include <iostream> using namespace std; class Horse { public: Horse(){cout<<&qu ...

  9. C++ 继承 | 对象切割、菱形继承、虚继承、对象组合

    文章目录 继承 继承的概念 继承方式及权限 using改变成员的访问权限 基类与派生类的赋值转换 回避虚函数机制 派生类的默认成员函数 友元与静态成员 多继承 菱形继承 虚继承 组合 继承 继承的概念 ...

  10. C++之菱形继承与虚继承(含虚函数)

    面向对象的三大特征:封装,多态,继承 前面我们已经讲了继承的一些知识点,在这基础上,我们讲的时候再涉猎一些多态的只是. 下面我们先接着上次讲有虚函数的菱形虚继承 首先什么是虚函数.? 虚函数:在类里面 ...

最新文章

  1. Linux_PXE服务器_RHEL7
  2. 如何在 Windows 上安装 Python | Linux 中国
  3. 除了Open Day,Nibiru与DigiArtist来CJ 搞事情了
  4. 《c陷阱与缺陷》笔记--注意边界值
  5. 制图折断线_CAD制图初学入门之CAD标注时必须要区分的两个概念
  6. IBM打造云访问量子计算机 规模仅相当于D-Wave系统的四百分之一
  7. 安装Greenplum-perfmon-web监控软件遇到的问题及解决
  8. 《孙哥说Spring5》学习笔记
  9. Mybatis-Spring扫描路径有重叠导致Invalid bound statement(not found)问题
  10. C++程序注册Dll
  11. dss中文含义_DSS(中文译名:决策支持系统),这是什么系统?有多少个种类?...
  12. 如何稳定eBay店铺评分
  13. 模态逻辑(1)——从命题逻辑开始
  14. 软件工程应用与实践(2)——application.properties配置文件分析
  15. 小程序与后台 api接口数据交互详解(微信报修小程序源码讲解七)
  16. 记录一个删库跑路的技巧(如何快速删除数据库下面的所有表)
  17. python爬取王者皮肤_Python爬取王者荣耀所有英雄以及高清大图
  18. 国内外计算机联锁系统的发展,车站计算机联锁系统的现状与发展
  19. 关于C语言的图像光标处理
  20. HTML界面多语言切换

热门文章

  1. 使用Tortoisegit,修改账户名密码
  2. clickhouse分析:zookeeper数据存储
  3. 在全系1000学生中,征集慈善捐募,当总数达到十万时就结束,统计捐款人数和平均捐款数目
  4. 剩余内存无法满足申请时,系统会怎么做?
  5. 点选式图片验证码的一种实现效果
  6. 求一元二次方程求根公式与韦达定理.
  7. 安卓扁平化之路专题(一)Android 4.4新特性
  8. 墨羽卿画第二章第6节:修行路,修心途
  9. tightvnc for linux,tightvnc linux编译
  10. TextMate使用心得