posts - 2,  comments - 0,  trackbacks - 0

在网上很多讨论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

来看看析构函数像什么样子:

 

_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

 

在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 阅读(1491) 评论(0)  编辑 收藏 引用

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理