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

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

一、虚函数表

  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. ADO.NET Entity Framework(EF)

    ylbtech-Miscellaneos: ADO.NET Entity Framework(EF) A,返回顶部 1, ADO.NET Entity Framework 是微软以 ADO.NET 为 ...

  2. 对 HTTP 304 的理解(转-并增加自己的测试)

    作者:吴俊杰 性别:男 邮箱:sshroot@126.com 文章类型:原创 博客:http://www.cnblogs.com/voiphudong/ 转自: http://www.cnblogs. ...

  3. win10安装软件被阻止后

    以管理员身份运行CMD,然后在cmd里执行就可以了.

  4. EDE,DEDE网站搬家,DEDECMS搬家教程,一看就会

    无聊做个小教程:DEDE搬家,本人提供搬家 先来说下DEDE 大家会安装吧,不多说,看图 一.进入织梦DedeCms的后台备份数据库 步骤:系统 –> 数据库备份/还原 -> 全选有所织梦 ...

  5. Singleton 单例模板

    // singleton.h #ifndef SINGLETON_H #define SINGLETON_H // 单例基类模板 template <class T> class Sing ...

  6. 修改Tomcat的网站根目录

    想把Tomcat的默认网站根目录修改成自己指定的目录,比如:F:/MyWeb.这样以后把自己写的index.jsp放到该目录下,就能通过http://localhost:8080/index.jsp来 ...

  7. (medium)LeetCode 221.Maximal Square

    Given a 2D binary matrix filled with 0's and 1's, find the largest square containing all 1's and ret ...

  8. (medium)LeetCode 233.Number of Digit One

    Given an integer n, count the total number of digit 1 appearing in all non-negative integers less th ...

  9. iOS中使用FMDB事务批量更新数据库

    今天比较闲看到大家在群里讨论关于数据库操作的问题,其中谈到了"事务"这个词,坦白讲虽然作为计算机专业的学生,在上学的时候确实知道存储过程.触发器.事务等等这些名词的概念,但是由于毕 ...

  10. Unable to open serial port /dev/ttyUSB0

    ubuntu12.04使用USB转串口时出现权限不够问题,如下 Unable to open serial port /dev/ttyUSB0 权限不够 解决办法: 通过增加udev规则来实现.步骤如 ...