}
main.cpp:
typedef void* pDisplayFunc1(int i, CRect* rect);
TRACE_FUCTION_AND_LINE("test begin:");
pDisplayFunc1* pfunc1 = (pDisplayFunc1*)(*(pp+1));
(*pfunc1)(8888, &rect1);
TRACE_FUCTION_AND_LINE("test end..");
前面关于虚函数表还没有详细说明,再加一个虚函数,它的位置是位于第1个item表项上,可以通过取*(pp+1)获得,调用传递的参数为8888.
打印结果如下:
[ main: 53] test begin:
[ CRect::display1: 47] i=8888
不过我的vs2008随后报告了内存访问错误了,不知道为什么,但是可以看到函数还是调用了,证明预想不错。采用MINGW仿linux倒没有问题正常打印出了结果,应该两个环境的安全检查程度不一样。
3)再玩点C函数的感觉
函数的地址应该是编译期确定,不知道如何打印出来?
再看个更洋气的:既然pfunc1是display1的函数地址,那么我完全就不需要传入&rect1的参数了,传递一个NULL验证猜想。在(*pfunc1)(8888, &rect1);下面再加上一个调用(pfunc1)(8899, NULL); 如我所愿,还是正常打印出了8899,
打印结果(先注释掉前面那一句话,因为此方法引起内存访问错误):
[ main: 53] test begin:
[ CRect::display1: 47] i=8899
4)继续新花样,更改vtbl看看效果。
将vtbl的第0个item和第1个item的内容交换一下,那么再调用display会发生什么情况呢?迫不及待啊。
很不幸,由于内存保护不让写,很是郁闷。但是有什么东西能够阻挡我们的兴趣呢?你保护,我自己制造一个rect,自己制造一个vtbl。基本思想是创建一个rectbuf的char数组保存rect1的内存,创建一个rectvtbl保存vtbl的项目,而且将rectbuf数组的前四个字节变成一个vptr指针指向rectvtbl的item0地址,由于这个vbtl表是我们自己的内存,因此是可以访问的,然后交换一下rectvtbl表项的两个函数指针的值即可.
测试中发现将display和display1交换,vs2008总是说内存访问错误,猜想是因为两个函数一个有参数一个无参数导致的,于是又增加了一个虚函数display2,它和display1的内容一模一样。
Shape.cpp:
virtual void display2(int i){
TRACE_FUCTION_AND_LINE("i=%d", i);
}
main.cpp:
char rectbuf[sizeof(rect1)];
memcpy(rectbuf, &rect1, sizeof(rect1));//分配内存保存rect1的内存
char rectvtbl[sizeof(int*) * 4];
memcpy(rectvtbl, pp-1, sizeof(int*)*4);//分配内存报错rectvtbl的内存
int *prectbuf = (int*)rectbuf;
int *pvtbl = (int*)((int*)rectvtbl + 1);//将pvtbl指向虚表的item0项目,因为前四个字节是typeinfo指针
prectbuf[0] = (int)(pvtbl);
CRect* fakecharRect = (CRect*)rectbuf;
//fakecharRect->display();
TRACE_FUCTION_AND_LINE("before deal: fakecharRect.display.....");
fakecharRect->display1(100);
fakecharRect->display2(100);//打印,正常调用。先调用display1,再调用display2
long temp = *(pvtbl+1);
*(pvtbl+1) = *(pvtbl+2);
*(pvtbl+2) = temp;//交换虚表的item1和item2项目,也就是display1和display2
TRACE_FUCTION_AND_LINE("after deal: fakecharRect.display.....");
//fakecharRect->display();
fakecharRect->display1(100);
fakecharRect->display2(100);//打印,惊讶调用。先调用display2,再调用display1(交换表项目所致)
输出结果如下:
[ main: 61] before deal: fakecharRect.display....
[ CRect::display1: 54] i=100
[ CRect::display2: 57] i=100
[ main: 69] after deal: fakecharRect.display.....
[ CRect::display2: 57] i=100
[ CRect::display1: 54] i=100
[ CRect::~CRect: 34]
从日志打印的函数名字可以看到没有处理之前先调用diplay1,在调用display2。处理之后就变了。
上面的测试可以验证几个结论,不要把对象之类的东西看的过于神秘了,对象就是内存,你可以通过CRect rect1来创建对象,你也可以通过char数组来创建对象,完全取决于你。函数是什么?函数就是一个地址而已,那个地址是编译时确定的。由于我们知道了虚函数的一些机制,虚函数表项目中的内存实际代表的是哪一个函数,我们就可以通过交换表项目的内存,来进行一些匪夷所思的操作,将display1函数实际上调用的是display2,如果你想,可以自己随便写一个函数,然后将函数地址赋值到表的项目中,然后当你进行类的diplay函数调用的时候,你会惊讶的发现它竟然调用的是一个外部C函数,这个绝对会另你周围的人惊讶不已。当然玩这个可不是 为了哗众取宠,理解它可以让我们更懂得控制我们程序的行为,当程序出现一些百思不得解的诡异状况时,仔细思考一下程序的内部执行逻辑,你就会豁然开朗(相信我,你碰到的一般都不会涉及到如此的深入)。
想总结一下了:
OK,通过虚函数的描述,让我们知道了可以通过取虚函数表项目来进行函数调用,它可以如真实的C函数一样,我们可以通过更改虚函数的表项来操纵函数的调用,可以交换,可以给虚函数表项赋值。这样你将不再对虚函数感到很大的神秘性,它其实就是一个函数指针数组,里面存储着一堆当前类的虚函数而已。通过存储的虚函数可以