9.6.7 虚析构函数(2)
最后,本程序的源文件Ex9_12.cpp应该如下所示:
- // Ex9_12.cpp
- // Destructor calls with derived classes
- // using objects via a base class pointer
- #include "Box.h" // For CBox and CContainer
- #include "Can.h" // For CCan (and CContainer)
- #include "GlassBox.h" // For CGlassBox (and CBox and CContainer)
- #include <iostream> // For stream I/O
- using std::cout;
- using std::endl;
-
- const double PI = 3.14159265; // Global definition for PI
-
- int main()
- {
- // Pointer to abstract base class initialized with CBox object address
- CContainer* pC1 = new CBox(2.0, 3.0, 4.0);
-
- CCan myCan(6.5, 3.0); // Define CCan object
- CGlassBox myGlassBox(2.0, 3.0, 4.0); // Define CGlassBox object
-
- pC1->ShowVolume(); // Output the volume of CBox
- cout << endl << "Delete CBox" << endl;
- delete pC1; // Now clean up the free store
-
- pC1 = new CGlassBox(4.0, 5.0, 6.0); // Create CGlassBox dynamically
- pC1->ShowVolume(); // ...output its volume...
- cout << endl << "Delete CGlassBox" << endl;
- delete pC1; // ...and delete it
-
- pC1 = &myCan; // Get myCan address in pointer
- pC1->ShowVolume(); // Output the volume of CCan
-
- pC1 = &myGlassBox; // Get myGlassBox address in pointer
- pC1->ShowVolume(); // Output the volume of CGlassBox
-
- cout << endl;
- return 0;
- }
示例说明
除了给每个类添加析构函数,以输出一条大意是"某某析构函数被调用"的消息以外,其他修改仅仅是在函数中添加了两处代码。其中一处增加了动态创建CGlassBox对象、输出该对象的体积值、然后将其删除的语句。另外一处显示一条消息,以指示动态创建的CBox对象被删除的时间。该示例生成的输出如下所示:
- CBox usable volume is 24
- Delete CBox
- CContainer destructor called
-
- CBox usable volume is 102
- Delete CGlassBox
- CContainer destructor called
-
- Volume is 45.9458
- CBox usable volume is 20.4
- CGlassBox destructor called
- CBox destructor called
- CContainer destructor called
- CCan destructor called
- CContainer destructor called
从输出可以看出,当我们删除pC1指向的CBox对象时,基类CContainer的析构函数被调用,但却没有调用CBox类的析构函数。同样,当我们删除添加的CGlassBox对象时,还是基类CContainer的析构函数被调用,但却没有调用CGlassBox或CBox类的析构函数。在另外两个对象的销毁过程中,析构函数的调用情况是正确的,都是先调用派生类的析构函数,再调用基类的析构函数。对于在声明中创建的第一个CGlassBox对象来说,被调用的析构函数有3个:首先是派生类的析构函数,然后是直接基类的析构函数,最后是间接基类的析构函数。
所有问题都源于对象是在自由存储器中创建的,程序在销毁那两个对象时都调用了错误的析构函数。出现这种情况的原因是,析构函数的连接是在编译时静态解析的。对自动对象而言,这样做没有任何问题,编译器知道它们是什么,也能够安排调用正确的析构函数。但对于动态创建并通过指针访问的对象来说,情况就不同了。当执行delete操作时,编译器知道的唯一信息是该指针的类型是"指向基类的指针"。编译器不知道该指针实际指向的对象的类型,因为这是在程序执行时确定的。因此,编译器只能确保使delete操作调用基类的析构函数。在实际的应用程序中,这一点可能造成大量的问题,可能有许多遗留的对象散布在自由存储器中,还可能造成更严重的问题,究竟如何取决于相关对象的本质。
解决方法很简单。我们需要在程序执行时动态解析对析构函数的调用。通过在类中使用虚析构函数,我们就可以安排这样的解析方式。前面初次讨论虚函数时曾经说过,将基类函数声明为virtual足以确保任何派生类中所有名称、形参列表和返回类型与其相同的函数也都是virtual。这条规则不仅适用于普通的成员函数,而且适用于析构函数。我们需要在Container.h文件包含的CContainer类定义中,给析构函数的定义添加virtual关键字,这样类定义将如下所示:
- class CContainer // Generic base class for containers
- {
- public:
- // Destructor
- virtual ~CContainer()
- { cout << "CContainer destructor called" << endl; }
-
- // Rest of the class as before
- };
现在,虽然我们没有显式指定,但所有派生类中的析构函数还是都自动变成了虚函数。当然,如果希望代码绝对清晰,那么也可以显式将它们指定为virtual。
如果重新运行修改过的示例,那么将得到下面的输出:
- CBox usable volume is 24
- Delete CBox
- CBox destructor called
- CContainer destructor called
-
- CBox usable volume is 102
- Delete CGlassBox
- CGlassBox destructor called
- CBox destructor called
- CContainer destructor called
-
- Volume is 45.9458
- CBox usable volume is 20.4
- CGlassBox destructor called
- CBox destructor called
- CContainer destructor called
- CCan destructor called
- CContainer destructor called
可以看出,所有对象现在都是以正确的析构函数调用次序销毁的。在该程序中,销毁动态对象时析构函数的调用次序与销毁同类型的自动对象时相同。
此刻我们可能想到一个问题,可以将构造函数声明为virtual吗?答案是不能-- 只有析构函数和其他成员函数才可以。
注意:
当使用继承时,总是照例将基类的析构函数声明为虚函数是个好主意。在类析构函数的执行过程中有少量的系统开销,但大多数情况下不用理会这一点。使用虚析构函数能够确保正确销毁对象,还能避免相反情形下可能出现的程序崩溃风险。