浅谈C++虚函数
很长时间都没写过博客了,主要是还没有养成思考总结的习惯,今天来一发。
我是重度拖延症患者,本来这篇总结应该是早就应该写下来的。
一、虚函数表
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++虚函数的更多相关文章
- 浅谈C++虚函数机制
0.前言 在后端面试中语言特性的掌握直接决定面试成败,C++语言一直在增加很多新特性来提高使用者的便利性,但是每种特性都有复杂的背后实现,充分理解实现原理和设计原因,才能更好地掌握这种新特性. 只要出 ...
- 浅谈 es6 箭头函数, reduce函数介绍
今天来谈一下箭头函数, es6的新特性 首先我们来看下箭头函数长什么样子, let result = (param1, param2) => param1+param2; 上述代码 按照以前书写 ...
- shell浅谈之十函数
转自:http://blog.csdn.net/taiyang1987912/article/details/39583179 一.简介 Linux Shell编 程中也会使用到函数,函数可以把大的命 ...
- 浅谈js回调函数
回调函数原理: 我现在出发,到了通知你”这是一个异步的流程,“我出发”这个过程中(函数执行),“你”可以去做任何事,“到了”(函数执行完毕)“通知你”(回调)进行之后的流程 例子 1.基本方法 ? 1 ...
- 浅谈JSON.stringify 函数与toJosn函数和Json.parse函数
JSON.stringify 函数 (JavaScript) 语法:JSON.stringify(value [, replacer] [, space]) 将 JavaScript 值转换为 Jav ...
- 浅谈JavaScript eval() 函数
用js的人都应该知道eval()函数吧,虽然该函数用的极少,但它却功能强大,那么问题来了,为什么不常用呢?原因很简单,因为eval()函数是动态的执行其中的字符串,里面有可能是脚本,那么这样的话就有可 ...
- 浅谈JavaScript匿名函数与闭包
一. 匿名函数 //普通函数定义: //单独的匿名函数是无法运行的.就算运行了,也无法调用,因为没有名称. 如: function(){ alert('123'); ...
- 浅谈javascript的函数节流
什么是函数节流? 介绍前,先说下背景.在前端开发中,有时会为页面绑定resize事件,或者为一个页面元素绑定拖拽事件(其核心就是绑定mousemove),这种事件有一个特点,就是用户不必特地捣乱,他在 ...
- 浅谈JavaScript的函数的call以及apply
我爱撸码,撸码使我感到快乐!大家好,我是Counter.今天就来谈谈js函数的call以及apply,具体以代码举例来讲解吧,例如有函数: function func(a, b) { return a ...
随机推荐
- 【转】C++ 单例模式
http://blog.csdn.net/hackbuteer1/article/details/7460019 单例的一般实现比较简单,下面是代码和UML图.由于构造函数是私有的,因此无法通过构造函 ...
- UCOS-消息队列(学习笔记)
消息队列的核心是一个消息的指针数组,UCOS系统初始化时根据OS_CONFI.h中的最大队列个数定义这么多个消息队列(队列的结构)并将他们串联成空的链表,创建消息队列时从空链表中抽出一个并用指针数组的 ...
- 编译器错误消息: CS0234: 命名空间“Purple”中不存在类型或命名空间名称“Model”(是否缺少程序集引用?)
编译错误 “/storeimg”应用程序中的服务器错误. 编译错误 说明: 在编译向该请求提供服务所需资源的过程中出现错误.请检查下列特定错误详细信息并适当地修改源代码. 编译器错误消息: CS023 ...
- 使用Git命令从Github下载代码仓库
Github作为最大的开源代码仓库,也是作为版本控制工具,简直就是程序员的天堂,是一座挖掘不尽的宝藏.现在讲解一下如何把一个Github上的项目下载到本地,也就是在本地也建立一个项目仓库,以后进行项目 ...
- Java注解实践--annotation学习三
注解对代码的语意没有直接影响, 他们只负责提供信息给相关的程序使用. 注解永远不会改变被注解代码的含义, 但可以通过工具对被注解的代码进行特殊处理. JDK 基本Annotation 注解 说明 @O ...
- javascript当文本框获得焦点设置边框
javascript当文本框获得焦点设置边框:本章节介绍一下当文本框获得焦点以后如何设置文本框的边框样式,本来是一个非常简单的问题,但是有可能前台美工人员对javascript并不是太了解,所以还是通 ...
- R如何检验类别变量(nominal variable)与其他变量之间的相关性
1.使用Pearson积差相关系性进行检验的话可以判断两个变量之间的相关性是否显著以及相关性的强度 显著性检验 (significant test) 连续变量 vs 类别变量 (continuous ...
- CentOS7 备忘录
//安装CentOS7 1.用LinuxLive USB Creator,LiveUSB Creator设置U盘安装不成功,UltraISO可以. 2.旧电脑(x8le)安装时报"/dev/ ...
- springmvc的3中路径风格
1.导入相应的jar包,文件放置情况 2.web.xml <?xml version="1.0" encoding="UTF-8"?> <we ...
- photoshop CS 调整选择区域的大小
网上看到说:矩形选框不能直接调整大小,如果你不想重新画一个可以利用转换路径,然后再调整.这是不对的,矩形选框是可以调整大小的,使用"变换选区"即可. 对应步骤截图如下: 1.画 ...