[求鉴定]《虚函数表里边保存的不一定是虚函数的地址》

cswuyg 2010-09-05 07:21:02
说明:前段时间学习C++对象内存布局时,发现了一些以前理解错了的知识。于是写了篇总结的文章。这篇文章我朋友看了之后,批得一无是处..⊙﹏⊙b汗。可我觉得我是对的,但是他的批评还是让我有点担心,是否还有其他的错误呢?所以来这里求鉴定。我认为最大的悲剧就是把一个错误的观点当作对的。也许悲剧已经发生,但是不可以继续下去。。所以请各位来发现我的错误。先谢谢了。另外,若各位朋友有空的话,顺便看看我的其它几篇总结文章,看看是否有错误存在。
原文之前放在我的博客园博客:http://www.cnblogs.com/cswuyg/archive/2010/08/20/1804716.html
原文如下:
虚函数表里边保存的不一定是虚函数的地址

我一直以为虚函数表里边保存的就是虚函数的地址,前几天做测试的时候才发现这想法不一定是对的。
测试代码:
//虚函数表里边保存的不一定是虚函数的地址.cpp
//2010.8.19
//参考:http://topic.csdn.net/u/20091128/14/5a9ff412-560e-4214-8716-e269295f7028.html
/*分析:通过最后的输出结果可以发现,通过Derived类的虚函数表调用所有的虚函数,发现第一张虚函数表的输出1和第二张虚函数表的输出4它们是同一个函数的输出,
在虚函数表项上的值却是不同的。如果虚函数表上的项的值都是虚函数的地址,那么Derived的两张表里边用于调用show()函数的表项的值应该是相同的,但事实上它们不同。
这说明,虚函数表里边保存的未必就是虚函数的地址。这种情况在之前一直没有遇到过(或者没注意到),那么那两个不同的值哪一个才是Derived::show()函数的地址呢?
反汇编分析。。
//Code::Blocks VS2005/2008
*/
#include <iostream>
using namespace std;

class BaseA
{
public:
virtual void show()
{
cout << "BaseA::show()" << endl;
}
virtual void showAA()
{
cout << "BaseA::showAA()" << endl;
}
};


class BaseB
{
public:
virtual void show()
{
cout << "BaseB::show()" << endl;
}
virtual void showBB()
{
cout << "BaseB::showBB()" << endl;
}
};

class Derived : public BaseA, public BaseB
{
public:
/*重写*/
void show()
{
cout << "Derived::show()" << endl;
}
virtual void showD()
{
cout << "Derived::showD()" << endl;
}
};


int main()
{
typedef void (__thiscall *Fun)(void*pThis);//非常重要

BaseA aobj;
BaseB bobj;
Derived dobj;
/*BaseA对象*/
int** p = (int**)&aobj;
cout << "-----BaseA类的对象-----" << endl;
cout << "1 BaseA:\t" << (int*)p[0][0] << "\t"; ((Fun)p[0][0])(p);
cout << "2 BaseA:\t" << (int*)p[0][1] << "\t"; ((Fun)p[0][1])(p);
cout << endl;

/*BaseB对象*/
p = (int**)&bobj;
cout << "-----BaseB类的对象-----" << endl;
cout << "1 BaseB:\t" << (int*)p[0][0] << "\t"; ((Fun)p[0][0])(p);
cout << "2 BaseB:\t" << (int*)p[0][1] << "\t"; ((Fun)p[0][1])(p);
cout << endl;

/*Derived对象的第一个虚函数表指针所指向的虚函数表*/
p = (int**)&dobj;
cout << "-----Derived类的对象-----" << endl;
cout << "1 Derived:\t" << (int*)p[0][0] << "\t"; ((Fun)p[0][0])(p);
cout << "2 Derived:\t" << (int*)p[0][1] << "\t"; ((Fun)p[0][1])(p);
cout << "3 Derived:\t" << (int*)p[0][2] << "\t"; ((Fun)p[0][2])(p);
cout << endl;

/*Derived对象的第二个虚函数表指针所指向的虚函数表*/
p = (int**)((int*)(&dobj)+1);
cout << "4 Derived:\t" << (int*)p[0][0] << "\t"; ((Fun)p[0][0])(p);
cout << "5 Derived:\t" << (int*)p[0][1] << "\t"; ((Fun)p[0][1])(p);
system("pause");
return 0;
}
/*
-----BaseA类的对象-----
1 BaseA: 00401320 BaseA::show()
2 BaseA: 00401350 BaseA::showAA()

-----BaseB类的对象-----
1 BaseB: 004013A0 BaseB::show()
2 BaseB: 004013D0 BaseB::showBB()

-----Derived类的对象-----
1 Derived: 00401440 Derived::show()
2 Derived: 00401350 BaseA::showAA()
3 Derived: 00401470 Derived::showD()

4 Derived: 00405430 Derived::show()
5 Derived: 004013D0 BaseB::showBB()
*/

分析
一、反汇编分析
通过测试结果可以发现虚函数表里边保存的可能并非虚函数的地址,但是肯定跟虚函数有一点关联,因为最后通过虚函数表里的表项成功的调用了虚函数。通过反汇编分析,结果表明Derived类的第二张虚函数表里边保存的跟Derived::show()函数相关的表项,并非该函数的地址。分析过程如下:
1、 第一张虚函数表跟Derived::show()函数相关的表项保存的是该函数的地址。

图 1 通过第一张虚函数表调用show()函数

CALL EDX 按F7跟进之后见下图:

图 2 Derived::show()函数

2、 第二张虚函数表跟Derived::show()函数相关的表项保存的不是该函数的地址

图 3通过第二张虚函数表调用show()函数

CALL EDX 按F7跟进之后见下图:

图 4 跳转

可以发现,跳转的目标地址正是Derived::show()函数。
3、 总结:第一张虚函数表里边保存了Derived::show()函数的地址。第二张虚函数表里边保存的不是Derived::show()虚函数的地址,但是跟该虚函数地址间接关联了。
二、不直接保存函数地址的原因
现在已经明白,虚函数表里边保存的是什么东西了。还有另外一个问题,为什么第二张虚函数表里边不保存Derived::show()函数的地址,偏偏要保存跳转的地址,然后再跳过去,这样子有什么用途?
这个跟类的成员函数调用会传递this指针有关。
假如有这样的语句:
BaseB* pb = &dobj;
Pb->show();
如果没有中间的跳转,直接就去调用show()函数,那么传递的this指针是Derived对象中BaseB类实例的地址,也就是第二张虚函数表地址。这样的话,如果要访问dobj中的成员变量,通过这个this指针访问就会出错。可能Derived::show()会认为传递进来的是Derived对象的BaseA实例的地址。所以就需要图4中的代码,第二张虚函数表的表项保存的是那个sub的地址。在跳转之前先ecx减去4,在例子中可以发现,减去4使得this指针指向了dobj的地址(就是BaseA实例的地址)。
也就是说,之所以第二张虚函数表里边保存的不是函数地址,是为了保证this指针是正确。
原因猜测:可以通过A类指针去调用D::show(),也可以通过B类指针去调用D::show(),如果this指针不加调整,D::show()要访问成员变量的时候是this+偏移值来寻址的,这样就会有错误。所以必须调整。
三、这种情况什么时候出现
另一个问题, 什么时候虚函数表里边保存的不是函数地址?
如果要全面测试的话,那实在是件费力的事,所以猜测可能是这种情况:
派生类D有两个基类A和B,其中A定义了虚函数show(),B也定义了虚函数show(),且D类重写了虚函数show(),这样的D类中的第二张虚函数表(B类实例)里边保存的表项就不是D::show()虚函数地址。
也就是说通过B类指针调用D类对象的show()函数时,需要调整this指针。


特别说明:本文讨论跟跳转表无关。...之前没跟朋友说明,结果他老是以为我是忘了有跳转表。。
---
⊙﹏⊙b汗,只能100分以内。
...全文
528 38 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
38 条回复
切换为时间正序
请发表友善的回复…
发表回复
The_eagles 2010-09-09
  • 打赏
  • 举报
回复
收藏!
The_eagles 2010-09-09
  • 打赏
  • 举报
回复
好贴!
cswuyg 2010-09-09
  • 打赏
  • 举报
回复
告一段落,决定结贴,感谢各位的参与。
smartjeck 2010-09-08
  • 打赏
  • 举报
回复
学习了~
cswuyg 2010-09-07
  • 打赏
  • 举报
回复
[Quote=引用 30 楼 sumxx 的回复:]
学习了。。 我最近在也在.. 纠结虚函数表
[/Quote]
陈皓那三篇文章图文并茂,应该比较适合你。
tyzqqq 2010-09-07
  • 打赏
  • 举报
回复
cswuyg 2010-09-07
  • 打赏
  • 举报
回复
[Quote=引用 32 楼 crysleeper 的回复:]
我觉得你这只是概念游戏,你的编译器显然为函数show隐式得实现了两个版本,(其实应该是三个版本),难道只有那个不带偏移的才算是函数,而那个需要偏移,然后调用相同代码的就不是函数了?如果你认为虚函数的实现只能有一个版本,那只能是你一厢情愿,标准里对如何实现从未做规定,甚至都没有相应的暗示

值得称赞的是你整理的这么仔细,看了你的分析应该会对对虚函数的理解有很好的帮助.
[/Quote]
我对函数的理解:一堆指令之所以被称为函数,是因为它在进入函数时一般会有保护现场(如push指令)、分配局部空间(如果有需要,如sub esp,XX指令),恢复现场(如pop指令)、返回(如retn指令)……
那“调整this指针”的两行代码不能算是函数里头的代码。所以我认为派生类的show函数就是只有一个版本。至于标准,这个我没去看。我也只是分析VS下的。我想只需要一个函数空间总比需要两个函数空间要好,我猜编译器也是这样想的。
概念的东西一说起来就后怕...昨天就是跟朋友在纠缠概念.....
--
另外,谢谢你的夸奖,我也希望能给其他人带来帮助。
CrySleeper 2010-09-07
  • 打赏
  • 举报
回复
我觉得你这只是概念游戏,你的编译器显然为函数show隐式得实现了两个版本,(其实应该是三个版本),难道只有那个不带偏移的才算是函数,而那个需要偏移,然后调用相同代码的就不是函数了?如果你认为虚函数的实现只能有一个版本,那只能是你一厢情愿,标准里对如何实现从未做规定,甚至都没有相应的暗示

值得称赞的是你整理的这么仔细,看了你的分析应该会对对虚函数的理解有很好的帮助.
cd2108006026 2010-09-06
  • 打赏
  • 举报
回复
虚函数表还要保存rtti对象的地址!就是那个typeinfo
cswuyg 2010-09-06
  • 打赏
  • 举报
回复
貌似没什么错。
再顶出来。
sumxx 2010-09-06
  • 打赏
  • 举报
回复
学习了。。 我最近在也在.. 纠结虚函数表
liutengfeigo 2010-09-06
  • 打赏
  • 举报
回复
学习了~
cswuyg 2010-09-06
  • 打赏
  • 举报
回复
[Quote=引用 27 楼 luoqi 的回复:]
其实我觉得C++的继承,是很失败的,当然它只是表达一种编程思想!
用C也能表达这种思想,但C++把它弄复杂了!
[/Quote]
这个跟主题无关,就不说了。而且你的观点..见仁见智。
luoqi 2010-09-06
  • 打赏
  • 举报
回复
其实我觉得C++的继承,是很失败的,当然它只是表达一种编程思想!
用C也能表达这种思想,但C++把它弄复杂了!
cswuyg 2010-09-06
  • 打赏
  • 举报
回复
[Quote=引用 24 楼 luoqi 的回复:]
对不起,LZ,是我误会了,
再仔细看了一下文章!
我想楼主是想说,为什么虚函数表中为什么不都是函数地址?
我记得有本书<<深度探索C++对象模型>>(记不太清了)

多重继承,纯虚继承,,都是保存不同继承的函数表地址,而不是拷贝过来!

这种思想很常见的,
1,全局只有一份同类虚函数表,避免混乱,及减少代码,
2,思路简单.
3,藕合度少
[/Quote]
呵呵,没关系。其实我猜测了原因,就在文章的后部分。来这里是想让各位看看是否猜错了,或者有更新的解释。这个原因现在看了应该猜对了。《深度……》它避开了编译器,我觉得不是很好,毕竟我们总是在编译器下工作,所以只看了一点点。

接触面比较少,也没碰到其它方式的实现(GCC也是这样做),所以还没升华到思想的层面。减少代码空间、减少模块耦合这个本来就是C++应该有的。
  • 打赏
  • 举报
回复
luoqi 2010-09-06
  • 打赏
  • 举报
回复
对不起,LZ,是我误会了,
再仔细看了一下文章!
我想楼主是想说,为什么虚函数表中为什么不都是函数地址?
我记得有本书<<深度探索C++对象模型>>(记不太清了)

多重继承,纯虚继承,,都是保存不同继承的函数表地址,而不是拷贝过来!

这种思想很常见的,
1,全局只有一份同类虚函数表,避免混乱,及减少代码,
2,思路简单.
3,藕合度少
cswuyg 2010-09-06
  • 打赏
  • 举报
回复
[Quote=引用 22 楼 luoqi 的回复:]
你看我,18楼的回复
[/Quote]
关于你回复的18楼:
从Watch里边拷出来的吧,应该挺辛苦的。
“两码事”这三个字我不知道你想表达什么。我推测你是想表达你在11楼提出的观点,关于p是什么,不是本文的重点,而且你的疑惑,我也已经跟你说了是你误会了(见17楼回复)。
最后提醒:
你的回复,很多都是自认为的想法。那些错误的观点我没有这么说,你就给我扣上这个帽子,然后说我错了。我实在不好回复你。所以才叫你先看看我的文章在讲什么。而且回复的时候,最好说得明白点,这样我才知道你在说什么,方便交流。。希望你不要介意。
luoqi 2010-09-06
  • 打赏
  • 举报
回复
你看我,18楼的回复
cswuyg 2010-09-06
  • 打赏
  • 举报
回复
[Quote=引用 20 楼 luoqi 的回复:]
这种找法不对,需要了解一下编译器,才知道!
而且不同的编译器,都不一样,可能放在前面,也可能放在后面!
[/Quote]
哎,老兄,你先看懂我的文章再说。
另外,我文章里明确的指出了编译环境 :Code::Blocks VS2005/2008
加载更多回复(17)

65,187

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧