在网上很多讨论Pure Virtual Function Called错误的文章,有的说了内存模型上的关系,有得则只是说了用例,以至于我当初只知道错误会发生,但不知道到底为何会发生.懵懂!现在让我们从汇编语言结合C++对象模型来看个究竟.
我从网上趴了两个例子代码,具体看原文:http://blog.csdn.net/Blue_Dream_/archive/2008/04/08/2259649.aspx
#include <iostream>
using namespace std;
class Parent
{
public:
Parent()
{ }
~Parent()
{
cout << "Parent ~~~~~" << endl;
ClearALL();
}
void ClearALL()
{
cout << "ClearALL ~~~~~" << endl;
ThePure(); //调用自身的纯虚函数,包装一下是因为直接调用编译器会识别出这样调用是有问题的!
}
virtual bool ThePure() = 0 ;
};
class Child : public Parent
{
public:
Child() { }
virtual bool ThePure()
{
cout << "哈哈" << endl;
return false;
}
~Child()
{
ThePure();
}
};
int main()
{
Child c;
return 0;
}
程序退出前会调用Child类的析构函数,
Code
mov DWORD PTR $T8853[ebp], 0
lea ecx, DWORD PTR _c$[ebp]
call ??1Child@@QAE@XZ ; Child::~Child
来看看析构函数像什么样子:
_TEXT SEGMENT
_this$ = -4
??1Child@@QAE@XZ PROC NEAR ; Child::~Child, COMDAT
; File test.cpp
; Line 40
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx
mov eax, DWORD PTR _this$[ebp]
mov DWORD PTR [eax], OFFSET FLAT:??_7Child@@6B@ ; Child::`vftable'
; Line 41
mov ecx, DWORD PTR _this$[ebp]
call ?ThePure@Child@@UAE_NXZ ; Child::ThePure
; Line 42
mov ecx, DWORD PTR _this$[ebp]
call ??1Parent@@QAE@XZ ; Parent::~Parent
mov esp, ebp
pop ebp
ret 0
??1Child@@QAE@XZ ENDP ; Child::~Child
_TEXT ENDS
这个函数不是重点,只是能看到析构函数里调用了基类的析构函数:
call ??1Parent@@QAE@XZ ; Parent::~Parent
再来看看Parent类的析构函数的样子:
_TEXT SEGMENT
_this$ = -4
??1Parent@@QAE@XZ PROC NEAR ; Parent::~Parent, COMDAT
; File test.cpp
; Line 12
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx
mov eax, DWORD PTR _this$[ebp]
mov DWORD PTR [eax], OFFSET FLAT:??_7Parent@@6B@ ; Parent::`vftable'
; Line 13
push OFFSET FLAT:?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z ; std::endl
push OFFSET FLAT:??_C@_0O@HGFA@Parent?5?5?$HO?$HO?$HO?$HO?$HO?$AA@ ; `string'
push OFFSET FLAT:?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
call ??6std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<
add esp, 8
mov ecx, eax
call ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
; Line 14
mov ecx, DWORD PTR _this$[ebp]
call ?ClearALL@Parent@@QAEXXZ ; Parent::ClearALL
; Line 16
mov esp, ebp
pop ebp
ret 0
??1Parent@@QAE@XZ ENDP ; Parent::~Parent
_TEXT ENDS
首先看到的是改写对象内存(现在没有Parent和Child之分)中vptr(偏移 0~4)的指向-->>Parent::Vftable(一个地址,模型为一个表,表中存放的是其他函数的地址)
DWORD PTR [eax], OFFSET FLAT:??_7Parent@@6B@ ; Parent::`vftable'
然后下面几行是打印字符串的,不看它哈,跳到:
mov ecx, DWORD PTR _this$[ebp]
call ?ClearALL@Parent@@QAEXXZ ; Parent::ClearALL
这个是当前对象内存的起始地址保存到ecx,然后调用Parent的成员函数::ClearAll..(这也是书上所说的成员函数的一个参数隐含为this指针,但它并不是push进去的哦~~这里到ecx中转),现在来看ClearAll的汇编代码:
Code
?ClearALL@Parent@@QAEXXZ PROC NEAR ; Parent::ClearALL, COMDAT
; File test.cpp
; Line 19
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx
; Line 20
push OFFSET FLAT:?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z ; std::endl
push OFFSET FLAT:??_C@_0BA@OHDP@ClearALL?5?5?$HO?$HO?$HO?$HO?$HO?$AA@ ; `string'
push OFFSET FLAT:?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
call ??6std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<
add esp, 8
mov ecx, eax
call ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
; Line 21
mov eax, DWORD PTR _this$[ebp]
mov edx, DWORD PTR [eax]
mov ecx, DWORD PTR _this$[ebp]
call DWORD PTR [edx]
; Line 23
mov esp, ebp
pop ebp
ret 0
?ClearALL@Parent@@QAEXXZ ENDP ; Parent::ClearALL
在C++源文件中能看到ClearAll函数调用了ThePure,汇编代码就是上面代码中的:
mov eax, DWORD PTR _this$[ebp]
mov edx, DWORD PTR [eax]
mov ecx, DWORD PTR _this$[ebp]
call DWORD PTR [edx]
上面是一个获取虚函数表的功能,然后把this指针放入ecx,然后调用 call DWORD PTR[edx]
[edx] 极为[edx+0],也极为虚函数表中第一个函数,现在该回去看看Parent::vftable了:
CONST SEGMENT
??_7Parent@@6B@ DD FLAT:__purecall ; Parent::`vftable'
CONST ENDS
就这样子的,表中只有一项(Parent类中只一个虚函数),而且后面写了 FLAT:__purecall (这个我不懂,应该是指向了一个"空"函数吧)
所以在ClearALl函数中调用ThePure,其实是调用了一个不存在的函数....所以出错了.....
总结1:在派生类中由于某种原因(比如调用析构函数)将内存中vptr指向的表更改指向了了基类的表,而基类中存在纯虚函数,并且在基类的某些地方存在调用纯虚函数,就出错.
至于最初那个链接文章中的第一个例子就更好理解了,我们来看看构造函数的汇编代码,先看Base的:
_TEXT SEGMENT
_this$ = -4
??0Parent@@QAE@XZ PROC NEAR ; Parent::Parent, COMDAT
; File test.cpp
; Line 8
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx
mov eax, DWORD PTR _this$[ebp]
mov DWORD PTR [eax], OFFSET FLAT:??_7Parent@@6B@ ; Parent::`vftable'
mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret 0
??0Parent@@QAE@XZ ENDP
很简单,这里仅仅是把Parent类的vftable地址分配到vptr(Parent对象内存起始的0~4偏移位置).
Child类的构造函数我就不贴了,其实主要一点先调用Parent类构造函数(如上,指定Parent类的vftable),然后就是如Parent一样,把自己(Child)的vftable地址指定给vptr.
那么很明显了,在Parent类中调用成员函数,然后在成员函数调用虚函数,根据当前vftable(Parent的)寻找出来的则肯定是纯虚函数.....如果等Child构造好之后,Child会改写虚函数表中的地址(哪个函数被改写就改写哪个),那么你调用Pure函数则不会出错,因为其实调用的是Child的改写版本,这是一个真实存在的函数.
--------------------------------------------------------------------------
在我这里没有深究一些C++对象模型的一些其他更多问题,这里更多的是一个简化,只为方便/简单的窥视Pure function called.
posted on 2008-11-08 00:20
duzhongwei 阅读(1556)
评论(0) 编辑 收藏 引用