讲解之前,了解下什么是虚函数,什么是虚表指针,了解下语法,(也算复习了)
开发知识为了不码字了,找了一篇介绍比较好的,这里我扣过来了,当然也可以看原博客链接: http://blog.csdn.net/hackbuteer1/article/details/7558868
一丶虚函数讲解(复习开发,熟悉内存模型)
1.复习开发知识
首先:强调一个概念
定义一个函数为虚函数,不代表函数为不被实现的函数。
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现。
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
1、简介
假设我们有下面的类层次:
class A { public: virtual void foo() { cout<<"A::foo() is called"<<endl; } }; class B:public A { public: void foo() { cout<<"B::foo() is called"<<endl; } }; int main(void) { A *a = new B(); a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的! return 0; }
这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。
虚函数只能借助于指针或者引用来达到多态的效果。
如果看明白上面的开发知识,则我们可以从内存角度看一下虚函数是怎么样存在的.
2.从内存角度看虚函数
首先我们学习C++的时候,自学或者老师教学的时候,都有谈过一个虚表指针的概念.
那我们要知道什么是虚表指针.
2.1
不带虚表指针的高级代码:
class MyTest { public: MyTest(); ~MyTest(); void ShowHelloWorld(); int m_Number; }; MyTest::MyTest() { printf("MyTest::MyTest() "); } MyTest::~MyTest() { printf("MyTest::~MyTest() "); } void MyTest::ShowHelloWorld() { printf("Hello World! "); } //类声明在上 int main(int argc, char* argv[]) { MyTest obj; //构造obj对象 obj.ShowHelloWorld();//调用成员函数 obj.m_Number = 1; //成员变量赋值 return 0; }
首先看上图高级代码,为什么我们说它没有虚表指针.我们调试查看.
首先经过我们调试
1.obj在监视窗口中只有一个成员变量,且初始化为CCCCC (Debug下)
2.看对象的所在的地址中,发现只申请了4个字节空间,用来存放成员变量.
2.2带虚表指针的高级代码
高级代码还是其高级代码,唯一不同的则是在类中给成员函数加了一个关键字, virtual,让此成员函数变为一个虚函数.
内存模型:
我们发现加了之后会额外多出4个字节空间,而且监视窗口中加了一项虚表指针变量.
构造一下继续观看内存模型.
构造之后发现已经初始化了虚表指针,那么我们进去这个地址后查看有什么内容.
其内容是一个函数指针表,里面存放了虚函数的地址.不相信的话我们打开反汇编窗口,跟进去则可以看到.
总结:
1.没有虚表指针
1.1没有虚函数的情况下没有虚表指针
2.有虚表指针
2.1虚表指针的产生是看你有没有 virtual这个关键字
2.2虚表指针存储的是虚表的首地址,虚表可以看做是一个数组
2.3虚表中存储的是虚函数的地址.
二丶熟悉反汇编中虚表指针,以及还原
既然上面我们熟悉了内存模型,也熟悉了虚函数的原理,那么我们从反汇编的角度下看一下.
例子是我们加了虚函数的例子
Debug下的反汇编
在我们构造的时候,会填写虚表指针,然后核心代码就在上面
1.将对象存入一个局部变量保存
2.局部变量中转给eax
3.对eax取内容填写虚表地址.
总结就是一句话: 取出对象的首4个字节,填写虚表.
那么现在好办了,既然找到了虚表,则可以找到构造,析构,以及虚表中存储的所有虚函数了.
PS: 此图和上图的反汇编一样,只不过高版本的图表没法看,所以用低版本,低版本可以打开.
对其位置下一个引用图表,谁引用了我,则可以看到调用它的所有构造以及析构了,
1.构造的时候会填写虚表
2.析构的时候会填写虚表
图表:
可以看出分别有个构造和析构.那个是构造那个是析构,我们需要跟过去看一下.
根据以前所讲的认识构造和析构的方法,可以很简单的判别出来.
识别虚函数
既然我们找到了虚表指针,则可以双击过去,可以找到虚函数了.
有一个虚函数,确实只有一个,我们跳转过去看看是不是我们定义的
Debug下有跳转表
里面的跳转则是我们的虚函数
总结:
1.识别虚表指针可以在构造中或者析构中查看
2.虚表指针双击过去则可以看到所有的虚函数的地址
3.对虚表指针来个引用,(谁引用我)可以看到所有的构造和析构
三丶识别虚函数的调用
熟悉了虚表指针, 通过虚表指针找构造,析构,以及虚表指针指向的虚表找虚函数,那么我们看一下普通成员函数调用和虚函数调用有什么区别.
PS:类声明不截图,其普通成员函数不加关键字 virtual,其虚函数加virtual
高级代码:
int main(int argc, char* argv[]) { MyTest test; MyTest &obj = test; //可以虚调用 obj.print(); //普通成员函数 obj.ShowHelloWorld(); //虚函数调用 return 0; }
Debug下的反汇编观察.
认真观察可以看出
1.普通成员函数调用,直接Call
2.虚函数调用
2.1 首先获得虚表指针
2.2 间接调用虚表指针指向的虚表的内容(虚成员函数地址)
总结:
识别调用普通成员函数和虚函数的特征则是
1.普通成员函数直接调用Call
2.虚函数会通过虚表指针指向的虚表来间接调用.
转载于:
作者:IBinary
出处:http://www.cnblogs.com/iBinary/