很长时间都没写过博客了,主要是还没有养成思考总结的习惯,今天来一发。

  我是重度拖延症患者,本来这篇总结应该是早就应该写下来的。

一、虚函数表

  C++虚函数的机制想必大家都清楚了。不清楚的同学请参看各种C++入门书籍。这里,我要讨论一下这个虚函数机制究竟是怎么实现的。虚函数主要是靠一张VTABLE来实现的,先来看看这个VTABLE在哪里。

  首先我们看下面的代码:

 class ClassA
{
public:
int m_data1;
int m_data2;
void vfunc1(){cout << "i am A" << endl;}
};
class ClassB : public ClassA
{
public:
int m_data3;
void funcB(){}
void vfunc1(){cout << "i am B" << endl;}
};
class ClassC : public ClassB
{
public:
int m_data1;
int m_data4;
void funcC(){}
void vfunc1(){cout << "i am C" << endl;}
};
int main()
{
ClassA a;
ClassB b;
ClassC c;
cout << sizeof(int) << endl;
cout << sizeof(ClassA) << endl;
cout << sizeof(ClassB) << endl;
cout << sizeof(ClassC) << endl; cout << &(a) << endl;
cout << &(a.m_data1) <<endl;
cout << &(a.m_data2) <<endl;
cout << endl; cout << &(b) << endl;
cout << &(b.ClassA::m_data1) << endl;
cout << &(b.ClassA::m_data2) << endl;
cout << &(b.m_data3) <<endl;
cout << endl; cout << &(c) << endl;
cout << &(c.ClassA::m_data1) << endl;
cout << &(c.m_data2) << endl;
cout << &(c.m_data3) << endl;
cout << &(c.m_data1) <<endl;
cout << &(c.m_data4) <<endl; return ;
}

  我如果把上面的程序中ClassA的函数vfunc1声明成虚函数,即将第6行改为:

 virtual void vfunc1(){cout << "i am A" << endl;}

  程序运行的两个结果分别为:

  由上面的结果可以明显的看出,声明为虚函数的类比原来的类在大小上多了4个字节。没有虚函数的类的起始地址和第一个成员变量的地址保持一致,有虚函数的类的起始地址在第一个成员变量地址的前四个字节。这中间多出来的这四个字节就是隐藏起来的VPTR。VPTR是一个指向一个VTABLE的指针,换句话说,这多出来的四个字节里面存的是VTABLE的地址。

  而VTABLE里面就记录了这个类里面虚函数的地址。

再看下面的代码:

  ClassA *pa;
ClassB *pb;
ClassC *pc;
pa = &c;
pa->vfunc1();

  我们都知道如果是虚函数,上面的代码结果肯定为

  i am C

  如果没用虚函数,结果为

  i am A

  这是怎么做到的?

  首先,我们要知道,子类继承父类,子类拥有所有父类的成员变量跟成员函数,就是说:

 c.vfunc1();
c.ClassA::vfunc1()

  我们可以上面的方式显示地去访问被子类覆盖掉的函数和变量。可以理解为,虽然名字一样,其实子类里面有两个独立的vfunc1()函数,只不过子类调用的默认为ClassC::vfunc1()函数。

  当我们用父类的指针去指向一个子类的指针时,会有一个向上转换(我暂时这么叫)的过程。用pa指向对象c时,pa是一个ClassA类型的指针,pa只能访问ClassA类里面有的成员变量和成员函数地址,多余的,A类没有而C类有的成员变量和函数地址都被“upcasting”掉了。

  没有VTABLE时,只能找到ClassA类的vfunc1()函数的地址,找不到ClassC类的vfunc1()函数的地址。有虚函数表的存在时,对象c的虚函数表里面会记录ClassC::vfunc1()的地址,这样用pa指向对象c时,虚函数表不会被“upcasting”掉,于是,按照虚函数表里面的地址,就能够成功访问ClassC::vfunc1()。

  简言之,就是虚函数表里面存有正确的函数地址,这样就实现了动态绑定。用一张图来表示就是:

(如果有多个虚函数,VTABLE里面就有多个地址)

二、切片

  首先我们在三个类里面分别添加三个函数:

 virtual void funcSlicing(){cout << "slicing A" << endl;}
virtual void funcSlicing(){cout << "slicing B" << endl;}
virtual void funcSlicing(){cout << "slicing C" << endl;}

  再看如下的代码:

 ClassA *pa = &c;
pa->funcSlicing();
c.funcSlicing();
((ClassA*)(&c))->funcSlicing();
((ClassA)c).funcSlicing();

  如果你能一眼看出上面程序的运行结果,那接下来你就可以不用再看了。正确的结果是:

  

  前面三个应该很好理解,就是前面的虚函数机制。最后一句((ClassA)c).funcSlicing()的结果为什么是“slicing A”呢。这就是传说中的对象切片了。(ClassA)c这个操作意味着什么?这个操作意味着调用ClassA::默认拷贝构造函数将对象c中继承自ClassA类的成员进行copy,这个过程包含把对象c的VPTR(原来指向ClassC::VTABLE)修改为指向ClassA::VTABLE,而对象c中多余的东西则被“切割”掉了。

  这个时候(ClassA)c已经完完全全是一个ClassA了,这就是对象切片。

  也就是说,假如我有如下的一个函数:

 void TEST(ClassA a)
{
a.funcSlicing();
}

  这个时候,无论我调用TEST(b)或者TEST(c),结果都应该是调用ClassA::funcSlicing(),因为发生了对象切片。

  在多态的机制里面,我们总是应该是传对象的地址或者引用,不应该以对象本身作为参数传递。

  这里,我再简单说一下纯虚函数,我们都知道有纯虚函数的抽象类是不能实例化的。为什么不能实例化?因为纯虚函数强制性的给VTABLE里面留了一个空位置,这个位置里面没有留任何函数地址,为空。而我们在创建一个包含虚函数的对象时,编译器首先要做的事情就是初始化VPTR和VTABLE。只要有一个纯虚函数存在,那么VTABLE就是不完整的,为这样的类(抽象类)创建对象,编译器会返回错误信息。

  同理,在上面的例子中,假如我们把ClassA::funcSlicing()改为纯虚函数:

 virtual void funcSlicing() = ;

  TEST()函数就会编译错误,纯虚函数重要作用之一就是防止对象切片的发生。

  PS:C++果然是超级复杂,要兼顾效率和设计,完全取决于使用者的需要。我也只能是,用到哪里就好好把哪部分学一下。

  2014-1-11更新:

  偶然间看到一篇大牛的文章,C++ 虚函数表解析 ,又深刻体会到自己与别人的差距,你对一个东西理解有多深,你就可以给别人讲多清楚。由这篇文章的启发,可以用函数指针来访问虚函数表里面的函数。

  首先,声明这样一个函数指针的类型:

 typedef void(*Fun)(void);

  然后用下面的代码去访问虚函数表里面的函数(在陈的那篇文章第一个例子里面会有一些细节错误):

  Fun pFun = NULL;
pFun = (Fun)*((int *)*((int *)&c + )+);
pFun();
pFun = (Fun)*((int *)*((int *)&c + )+);
pFun();
//虚函数表(VPTR)的地址(对象c起始四字节里面的内容):*((int *)&c + 0)
//(int *)的作用是强制转换成四字节的int型指针,这样指针偏移是以四字节为单位。
//虚函数表里面第一个虚函数地址 *((int *)*((int *)&c + 0)+0) int **pVtable = (int **)&c;//这样就直观多了,两次寻址。
pFun = (Fun)pVtable[][];
pFun();

  我想,聪明的你肯定清楚上面代码的执行结果。这样,就不是空口无凭了。

浅谈C++虚函数的更多相关文章

  1. 浅谈C++虚函数机制

    0.前言 在后端面试中语言特性的掌握直接决定面试成败,C++语言一直在增加很多新特性来提高使用者的便利性,但是每种特性都有复杂的背后实现,充分理解实现原理和设计原因,才能更好地掌握这种新特性. 只要出 ...

  2. 浅谈 es6 箭头函数, reduce函数介绍

    今天来谈一下箭头函数, es6的新特性 首先我们来看下箭头函数长什么样子, let result = (param1, param2) => param1+param2; 上述代码 按照以前书写 ...

  3. shell浅谈之十函数

    转自:http://blog.csdn.net/taiyang1987912/article/details/39583179 一.简介 Linux Shell编 程中也会使用到函数,函数可以把大的命 ...

  4. 浅谈js回调函数

    回调函数原理: 我现在出发,到了通知你”这是一个异步的流程,“我出发”这个过程中(函数执行),“你”可以去做任何事,“到了”(函数执行完毕)“通知你”(回调)进行之后的流程 例子 1.基本方法 ? 1 ...

  5. 浅谈JSON.stringify 函数与toJosn函数和Json.parse函数

    JSON.stringify 函数 (JavaScript) 语法:JSON.stringify(value [, replacer] [, space]) 将 JavaScript 值转换为 Jav ...

  6. 浅谈JavaScript eval() 函数

    用js的人都应该知道eval()函数吧,虽然该函数用的极少,但它却功能强大,那么问题来了,为什么不常用呢?原因很简单,因为eval()函数是动态的执行其中的字符串,里面有可能是脚本,那么这样的话就有可 ...

  7. 浅谈JavaScript匿名函数与闭包

    一. 匿名函数   //普通函数定义: //单独的匿名函数是无法运行的.就算运行了,也无法调用,因为没有名称. 如: function(){             alert('123');    ...

  8. 浅谈javascript的函数节流

    什么是函数节流? 介绍前,先说下背景.在前端开发中,有时会为页面绑定resize事件,或者为一个页面元素绑定拖拽事件(其核心就是绑定mousemove),这种事件有一个特点,就是用户不必特地捣乱,他在 ...

  9. 浅谈JavaScript的函数的call以及apply

    我爱撸码,撸码使我感到快乐!大家好,我是Counter.今天就来谈谈js函数的call以及apply,具体以代码举例来讲解吧,例如有函数: function func(a, b) { return a ...

随机推荐

  1. (转)linux中fork()函数详解

    一.fork入门知识 一个进程,包括代码.数据和分配给进程的资源.fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同, ...

  2. Google是如何做代码审查的?

    Google是一个非常优秀的公司.他们做出了很多令人称赞的东西—既是公司外部,人们可以看到的东西,也是公司内部.有一些在公司内部并不属于保密的事情,在外部并没有给予足够广泛的讨论.这就是我今天要说的. ...

  3. 【转】深入理解DIP、IoC、DI以及IoC容器

    原文链接:http://www.cnblogs.com/liuhaorain/p/3747470.html 前言 对于大部分小菜来说,当听到大牛们高谈DIP.IoC.DI以及IoC容器等名词时,有没有 ...

  4. java GUI之事件处理

    常见事件及相应事件源类型 事件源 用户操作 事件 JButton 点击按钮 ActionEvent JTextField 在文本域按回车键 ActionEvent JCheckBox 点击复选框 Ac ...

  5. esriSRProjCS2Type Constants

    ArcGIS Developer Help  (Geometry)   esriSRProjCS2Type Constants See Also esriSRProjCSType Constants ...

  6. fatal: Not a git repository (or any of the parent directories): .git

    $ git remote add origin https://github.com/heyuanchao/YouxibiClient.gitfatal: Not a git repository ( ...

  7. Android——inflate 将一个xml中定义的布局找出来

    通俗的说,inflate就相当于将一个xml中定义的布局找出来. 因为在一个Activity里如果直接用findViewById()的话,对应的是setConentView()的那个layout里的组 ...

  8. 你不知道的JavaScript 二

    词法作用域 上次说到作用域,将其定义为一套规则,这套规则用来管理引擎如何在当前作用 域以及嵌套的子作用域中根据标识符名称进行变量查找. 作用域共有两种主要的工作模型.第一种是最为普遍的,被大多数编程语 ...

  9. 在解决方案中所使用 NuGet 管理软件包依赖

    使用程序包恢复功能可以在提交源代码时, 不需要将代码库提交到源代码管理中,大幅减少项目的尺寸.所有NuGet程序包都存储在解决方案的Packages文件夹中. 要启用程序包恢复功能,可右键单击解决方案 ...

  10. C#将C++动态库的回调函数封装成事件

    关于C#调用C++动态库的文章很多,调用动态库中回调函数的方法也不在少数.但大多数调用回调函数的方法依然保留了C++的语法特点. 比如有一段C++的回调函数代码,为了表达它的意思,我把注释也粘贴了进来 ...