#include<iostream>
using namespace std;
class Base
{
public:
int i;
virtual void f(){cout<<"Base::f()"<<endl;}
~Base(){}
};
class Derived:public Base
{
public:
int j;
void f(){cout<<"Derived::f()"<<endl;}
~Derived(){};
};
int main()
{
Derived *p;
Derived d;
p=&d;
p->f();
d.Base::~Base();
p->f();
return 0;
}
输出:
Derived::f()
Base::f()
为什么调用了基类的析构函数,虚函数表就改变了?
这样的问题, 有一个最实用的方法, 就是去跟一下汇编的代码, 当然了这需要你懂一点点的汇编语言.
问题的原因就是: 当你调用基类的析构函数d.Base::~Base()时, C++会先把d对象中的虚函数表改变一下, 原来指向Derived类的虚函数表, 现在指向Base类的虚函数表. 汇编代码为:
004116E3 mov eax,dword ptr [this]
004116E6 mov dword ptr [eax],offset Base::`vftable'
这里就把Base的vtable给对象d了.
至于为什么C++要这么处理, 一楼的基本上说出来了, 就是在构造函数和析构函数里多态性是被冻结了的, 在这两个函数中没有多多态性.
具体说明如下:在构造或者析构函数中调用没有实现的纯虚函数是很恐怖的,但是一般这个动作编译器都能检查出来;但是如果中间隔了一层(构造函数调用非纯虚函数A,而A调用了纯虚函数),编译器就无能为力了,然后就等着运行时错误出现吧~~~
而且,在构造函数或析构函数的上下文中,虚函数是没有意义的,它只调用本类的那个函数。因为,就构造函数来说,基类的构造函数总是先调用,而根据前面所说的,基类的构造函数中会被编译器插入一些填写本类型虚函数表的代码,也就是在执行基类构造函数中的用户代码时,此时虚函数表中的保存的仅是本类型的虚函数地址。也就是在构造时,多态是被freeze的。对应析构函数则是相反的过程,原理是一样的
至于析构函数还执行其它什么任务了, 我帖出来这段的汇编代码你就知道了:
~Base()
{
00411D80 push ebp
00411D81 mov ebp,esp
00411D83 sub esp,0CCh
00411D89 push ebx
00411D8A push esi
00411D8B push edi
00411D8C push ecx
00411D8D lea edi,[ebp-0CCh]
00411D93 mov ecx,33h
00411D98 mov eax,0CCCCCCCCh
00411D9D rep stos dword ptr es:[edi]
00411D9F pop ecx
00411DA0 mov dword ptr [ebp-8],ecx
// 注意下面这两句仅对于含有虚函数的类才有
004116E3 mov eax,dword ptr [this]
004116E6 mov dword ptr [eax],offset Base::`vftable'
// 就是这里把虚函数指针改变了的
}
00411DB4 pop edi
00411DB5 pop esi
00411DB6 pop ebx
00411DB7 add esp,0CCh
00411DBD cmp ebp,esp
00411DBF call @ILT+460(__RTC_CheckEsp) (4111D1h)
00411DC4 mov esp,ebp
00411DC6 pop ebp
00411DC7 ret
说实话, 除了你碰到的这个问题, 还真是没有其它什么值得你注意的问题了.
测试代码1:
#include "stdafx.h" using namespace std; class ClassA { public: ClassA(){ cout<<"ClassA::ClassA() begin"<<endl; Print(); cout<<"ClassA::ClassA() end"<<endl;; } virtual void Print(){ cout<<"ClassA::Print()"<<endl; } virtual ~ClassA(){ cout<<"ClassA::~ClassA() begin"<<endl; Print(); cout<<"ClassA::~ClassA() end"<<endl;; } }; class ClassB : public ClassA { public: ClassB(){ cout<<"ClassB::ClassB() begin"<<endl; Print(); cout<<"ClassB::ClassB() end"<<endl;; } void Print(){ cout<<"ClassB::Print()"<<endl; } ~ClassB(){ cout<<"ClassB::~ClassB() begin"<<endl; Print(); cout<<"ClassB::~ClassB() end"<<endl;; } }; int _tmain(int argc, _TCHAR* argv[]) { ClassB* pClassB = new ClassB; cout<<"------------"<<endl; ClassA* pClassA = pClassB; pClassA->Print(); cout<<"------------"<<endl; delete pClassB; getchar(); return 0; }
在vs2003运行结果如下
ClassA::ClassA() begin
ClassA::Print()
ClassA::ClassA() end
ClassB::ClassB() begin
ClassB::Print()
ClassB::ClassB() end
------------
ClassB::Print()
------------
ClassB::~ClassB() begin
ClassB::Print()
ClassB::~ClassB() end
ClassA::~ClassA() begin
ClassA::Print()
ClassA::~ClassA() end
(对代码稍作修改,在gcc中的编译结果也是如此)
可以看到,在new ClassB时,虽然在父类ClassA的构造函数调了的是被ClassB覆盖的虚函数Print(),但是实际上还是调用的ClassA的Print。这是因为
继承类在构造的时候总是首先调用其基类的构造函数来对属于其基类的部分进行构造,在这个时候,整个类被当作基类来处理(此时虚指针指向基类的虚函数表),继承类的部分对整个类来说好像不存在一样,直到基类的构造函数退出并进入继承类的构造函数,该类才被当作继承类来出来处理(此时虚指针指向派生类的虚函数表)。对析构也一样,只是析构的顺序正好相反。
进一步分析,如果在析构函数中调用纯虚函数呢?将ClassA中的Print()改为纯虚函数
测试代码2:
#include "stdafx.h" using namespace std; class ClassA { public: ClassA(){ cout<<"ClassA::ClassA() begin"<<endl; Print(); cout<<"ClassA::ClassA() end"<<endl;; } virtual void Print() = 0; virtual ~ClassA(){ cout<<"ClassA::~ClassA() begin"<<endl; Print(); cout<<"ClassA::~ClassA() end"<<endl;; } }; class ClassB : public ClassA { public: ClassB(){ cout<<"ClassB::ClassB() begin"<<endl; Print(); cout<<"ClassB::ClassB() end"<<endl;; } void Print(){ cout<<"ClassB::Print()"<<endl; } ~ClassB(){ cout<<"ClassB::~ClassB() begin"<<endl; Print(); cout<<"ClassB::~ClassB() end"<<endl;; } }; int _tmain(int argc, _TCHAR* argv[]) { ClassB* pClassB = new ClassB; cout<<"------------"<<endl; ClassA* pClassA = pClassB; pClassA->Print(); cout<<"------------"<<endl; delete pClassB; getchar(); return 0; }
编译出错:
之所以出错注意看上面的红字,此时在基类ClassA中构造析构函数调用的Print()是本类的,未实现。故报错。因为已经强调,在析构函数构造函数中就是当作本类(范围,虚指针都与本类搭配)。如果不是构造析构函数,若是其他函数,那么就可以调用纯虚函数。因为有纯虚函数的类是抽象类并不会创建实例。
Test error LNK2019: 无法解析的外部符号 "public: virtual void __thiscall ClassA::Print(void)" (?Print@ClassA@@UAEXXZ) ,该符号在函数 "public: __thiscall ClassA::ClassA(void)" (??0ClassA@@QAE@XZ) 中被引用
gcc中也有类似提示
稍加改动,继续测试:
测试代码3:
#include "stdafx.h" using namespace std; class ClassA { public: ClassA(){ cout<<"ClassA::ClassA() begin"<<endl; Print(); cout<<"ClassA::ClassA() end"<<endl;; } virtual void Print(){ Print2(); } virtual void Print2() = 0; virtual ~ClassA(){ cout<<"ClassA::~ClassA() begin"<<endl; Print(); cout<<"ClassA::~ClassA() end"<<endl;; } }; class ClassB : public ClassA { public: ClassB(){ cout<<"ClassB::ClassB() begin"<<endl; Print(); cout<<"ClassB::ClassB() end"<<endl;; } void Print(){ cout<<"ClassB::Print()"<<endl; } void Print2(){ cout<<"ClassB::Print2()"<<endl; } ~ClassB(){ cout<<"ClassB::~ClassB() begin"<<endl; Print(); cout<<"ClassB::~ClassB() end"<<endl;; } }; int _tmain(int argc, _TCHAR* argv[]) { ClassB* pClassB = new ClassB; cout<<"------------"<<endl; ClassA* pClassA = pClassB; pClassA->Print(); cout<<"------------"<<endl; delete pClassB; getchar(); return 0; }
成功编译,但是运行时出现runtime error的错误。编译器不会绕弯啊。
最后,建议大家在构造函数中别做太复杂的事,最好只是对成员变量的初始化工作。复杂点的操作另写一个初始化函数。
http://pan.baidu.com/s/1bpFward