闲来无事 谈谈虚析构函数
何时调用析构函数
优秀的程序员常常把基类的析构函数定义为虚函数。因为,将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。
动态分配的对象只有在指向该对象的指针被删除时才撤销。如果没有删除指向动态对象的指针,则不会运行该对象的析构函数,对象就一直存在,从而导致内存泄露,而且对象内部使用的任何资源也不会释放。
当对象的引用或指针超出作用域时,不会运行析构函数。只有删除指向动态分配对象的指针或实际对象(而不是对象的引用)超出作用域时,才会运行析构函数。
这段话就是说:
撤销对象时会自动调用析构函数;但对于动态分配的指针对象,只有在该指针对象被删除时才会“自动”调用析构函数(delete *ptr;)。而对于一个真实的对象(not 对象的引用),在对象超出作用域时,析构函数也会被自动调用。
例如
class ClxBase
{
public:
ClxBase() {}
virtual ~ClxBase() { cout<<"Calling Base dis-construct\n";}
void print()
{
cout <<"Base class \n"; }
};
class ClxVird:public ClxBase
{
public:
ClxVird(){}
~ClxVird(){
cout<<"Calling Derived_1 dis-construct\n";
}
};
class ClxDerived : public ClxVird
{
public:
ClxDerived() {}
~ClxDerived() { cout << "Calling Derived_2 dis-construct\n"; }
};
1. 按如下调用:
ClxDerived ptest;
ptest.print();
ClxDerived &pr=ptest;
pr.print();
则打印的trace如下:
Base class
Base class
Calling Derived_2 dis-construct
Calling Derived_1 dis-construct
Calling Base dis-construct
由这个输出结果地顺序可以,在ptest对象超出作用域时,析构函数才被自动调用的。并且对象ptest的引用pr并没有被当做真实的对象对待,备受冷落啊。并且如果在 pr.print();之前加入ptest.~ClxDerived(); 则pr.print();将不可用。
但实验结果是:即便显示地调用析构函数ptest.~ClxDerived(); ,对象ptest仍然存在,并没有因为析构函数而消失,而是等待超出作用域时系统的调用。
或许这种情况不会发生在指针对象上。
【2】. 按如下调用:
ClxBase *pr=new ClxDerived;
pr->print();
delete pr;
则打印结果如下:
Base class
Calling Derived_2 dis-construct
Calling Derived_1 dis-construct
Calling Base dis-construct
动态分配的每个指针对象都需要显示地删除掉这个指针,否则它所指的对象所占的空间不会被释放。
So there is a more safe way to delete pointer:
Ex: char *p;
delete p;
p = NULL;
如果在ClxDerived类中增加一个函数void f(){cout<<"Calling Derived_2::f()...\n";}并再main中,用上面的pr调用,即pr->f()。
编译时会报错: error: 'class ClxBase' has no member named 'f'
由此可见,派生类赋给基类指针时还是被阉割了,但阉割的是那些在基类中不曾出现过的成员,。为什么那些虚函数则没有被阉割呢?
如果在基类base中再加入virtual void f(){cout<<"Calling Base::f()...\n";},
则再编译时不会报错,并且打印出:Calling Derived_2::f()...
为什么?可能是因为C++系统建立“虚函数表”的缘故。这个在继承中虚函数表的机制有关。
第一个想法是,派生类不会覆盖基类虚函数,而是在派生类中同时存在基类和派生类增加的成员。当派生对象赋给基类指针对象时,“阉割”这一动作时参照着基类的所有成员来的。即割掉那些派生类自己长出来的所有成员,留下那些从基类继承来的所有成员(如果派生类有overide覆盖基类成员的成员,则保留这些派生类成员。overload重载的成员被阉割,留下的是基类的)也就是说,ClxBase *pr=new ClxDerived;
pr仍然是派生类ClxDerived的指针对象,但调用的只是在基类ClxBase中出现过的属于ClxDerived的所有成员(直接继承的和override的)
派生类成员来源有:从基类继承来的(即override重写基类的普通成员,overide基类的virtual成员和原样照搬的)和自己长出来的。重载基类的函数成员属于自己长出来的。
证明:在基类ClxBase 增加函数成员:void g(){cout<<"Calling Base::g()...\n";}
在派生类ClxDerived中override改函数,即也增加: void g(){cout<<"Calling Drived_2::g()...\n";}
main函数中调用:
ClxBase *pr=new ClxDerived;
pr->g();
pr->ClxBase::g();
打印输出为:
Calling Base::g()...
Calling Base::g()...
由此可见上面两个调用g(),实际调用的还是基类的,纵然派生类重写了基类的g成员。所以ClxBase *pr=new ClxDerived;之后,pr调用的只有基类自己的和派生类override基类的virtual成员了(pr调用的是那些在基类ClxBase中注册过的派生类ClxDerived的成员)。virtual引来了虚函数表
下面我解释一下虚函数的背后是怎么实现的:
我们都知道,虚函数可以做到动态绑定,为了实现动态绑定,编译器是通过一个表格(虚拟函数表),在运行时间接的调用实际上绑定的函数来达到动态绑定,其中这个我刚所说的表格其实现就是一个“虚拟函数表”。这张表对我们程序来说是透明的。是编译器为我们的代码自动加上去的(更准确的讲,并不是为所有的代码都添加一张虚拟函数表,而是只针对那些包括虚函数的代码才加上这张表的)。
既然有了这么一张虚拟函数表,自然而然我们就会想到,这个虚拟函数表里到底是存放一些什么东西呢?很简单,即然叫做虚拟函数表,当然是存放虚拟函数了,呵呵,在c++中,该表每一行的元素应该就是我们代码中虚拟函数地址了,也就是一个指针。有了这个地址,我们可以调用实际代码中的虚拟函数了。
编译器既然为我们的代码加了一张虚拟函数表,那这张虚拟函数表怎么与我们的代码关联起来呢? 要实现动态绑定,我们应该利用这张虚拟函数表来调用虚拟函数,为了达到目的,编译器又会为我们的代码增加一个成员变量,这个成员变量就是一个指向该虚拟函数表的指针,该成员变量通常被命名为:vptr。
说到了这里,上面代码中的ClassA中的在内存中应该如下图所示:
每一个ClassA的实例,都会有一个虚拟函数表vptr,当我们在代码中通过这个实例来调用虚拟函数时,都是通过vptr先找到虚拟函数表,接着在虚拟函数表中再找出指向的某个真正的虚拟函数地址。虚拟函数表中的内容就是类中按顺序声明的虚拟函数组织起来的。在派生的时候,子类都会继承父类的虚拟函数表vptr,我们只在把这个vptr成员在继承体系中一般看待就成了。
有一点要说明一下,当子类在改写了父类中的虚拟函数时,同时子类的vptr成员也会作修改,此时,子类的vptr成员指向的虚拟函数表中的存放的虚拟函数指针不再是父类的虚拟函数地址了,而是子类所改写父类的虚拟函数地址。理解这一点就很容易想到了:原来多态体现在这里!
原文链接:http://soft.chinabyte.com/database/428/11720928.shtml
3 那如下调用:
ClxBase *pr=new ClxBase;
输出结果为:
Base class
Calling Base dis-construct
4.如果按如下调用:
ClxVird *pr=new ClxVird;
输出结果为
Base class
Calling Derived_1 dis-construct
Calling Base dis-construct
即便换成ClxVird *pr=new ClxVird;输出结果也是一样的。
5 但是如果把顶层heirachy的类ClxBase的虚析构函数的virtual去掉,
同样运行 ClxBase *pr=new ClxDerived;
输出为:Base class
Calling Base dis-construct
对比【2】发现,不使用虚析构函数派生类被完全切割但是却没有释放资源。
如果删除基类指针,则需要运行基类析构函数并清除基类的成员,如果对象实际是派生类类型的,则没有定义该行为(指的是派生类的析构行为)如【5】。要保证运行适当的析构函数,基类中的析构函数必须为虚函数如【2】。
5.1运行ClxDerived *pr=new ClxDerived;
输出为:
Base class
Calling Derived_2 dis-construct
Calling Derived_1 dis-construct
Calling Base dis-construct
5.2 运行ClxVird *pr=new ClxDerived;
输出结果为:
Base class
Calling Derived_1 dis-construct
Calling Base dis-construct
与【5】的说法相似。
通过以上对比可以知道:
虚析构函数也是动态绑定的形式。在c++中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定。
要触发动态绑定必须满足两个条件:
第一:只有指定为虚函数的成员函数才能进行动态绑定。
第二:必须通过基类类型的引用或指针进行函数调用。
派生类析构函数不负责撤销基类对象的成员。编译器总是显示调用派生类对象基类部分的析构函数。每个析构函数只负责清除自己的成员。
如果析构函数为虚,那么通过指针调用时,运行哪个析构函数将因指针所指对象类型的不同而不同。
像其他虚函数一样,析构函数的虚函数性质都将继承。
虚析构函数与普通虚函数一样,都是为了体现“动态绑定”的,只是调用的规则不同而已。这个规则是什么呢?有待总结