首页 > 代码库 > 避免在析构函数中编写代码
避免在析构函数中编写代码
上篇文章中,我们介绍了为什么应该彻底避免编写拷贝构造函数和赋值操作符。今天这篇我们讨论下为什么应该避免在析构函数中编写代码。即让析构函数为空。
例如:
virtual ~MyClass() { }
我们用空析构函数这个术语表示花括号内没有代码的析构函数。
需要编写析构函数可能有如下几个原因:
- 在基类中,可能需要声明虚拟析构函数,这样就可以使用一个指向基类的指针指向一个派生类的实例。
- 在派生类中,并不需要把析构函数声明为虚拟函数,但是为了增强可读性,也可以这样做。
- 可能需要声明析构函数并不抛出任何异常。
virtual ~ScppAssertFailedException() throw () { }
这意味着我们保证不会从这个析构函数中抛出异常。因此,我们可以看到有时候需要编写析构函数。现在我们可以讨论析构函数为什么应该为空。何时需要在析构函数中出现实质性的代码呢?只有在析构函数或类的其他方法中获取了一些资源,并且在这个类的对象被销毁时应该释放这些资源时才应该这样,例如:
class PersonDescription { public: PersonDescription(const char* first_name, const char* last_name) : first_name_(NULL), last_name_(NULL) { if(first_name != NULL) first_name_ = new string(first_name); if(last_name != NULL) last_name_ = new string(last_name); } ~PersonDescription() { delete first_name_; delete last_name_; } private: PersonDescription(const PersonDescription&); PersonDescription& operator = (const PersonDescription&); string* first_name_; string* last_name_; };
这个类的设计违背了我们在前几篇文章中所讨论的原则。首先,我们看到每次添加一个表述个人描述的新元素时,都需要在析构函数中 添加对应的清理代码,这就违背了“不要迫使程序记住某些事情”的原则。以下是改进的设计代码:
class PersonDescription { public: PersonDescription(const char* first_name, const char* last_name) : first_name_(NULL), last_name_(NULL) { if(first_name != NULL) first_name_ = new string(first_name); if(last_name != NULL) last_name_ = new string(last_name); } private: PersonDescription(const PersonDescription&); PersonDescription& operator = (const PersonDescription&); string* first_name_; string* last_name_; };
在这个例子中,我们根本不需要编写析构函数,因为编译器会为我们自动生成一个析构函数完成这些任务,在减少工作量的同时,也减少了出现脆弱代码的可能性。但是,这并不是选择第二种设计的主要原因。在第一个例子当中,存在一种更为严重的潜在危害。
假设我们决定增加安全检查,检查调用者是否提供了名字和姓氏:
class PersonDescription { public: PersonDescription(const char* first_name, const char* last_name) : first_name_(NULL), last_name_(NULL) { <span style="color:#ff0000;">SCPP_ASSERT(first_name != NULL ,"First name must be provided"); first_name_ = new string(first_name); SCPP_ASSERT(last_name != NULL ,"Last name must be provided"); last_name_ = new string(last_name);</span> } ~PersonDescription() { delete first_name_; delete last_name_; } private: PersonDescription(const PersonDescription&); PersonDescription& operator = (const PersonDescription&); string* first_name_; string* last_name_; };
正如我们之前讨论的那样,程序中的错误可能会终止程序,但也有可能抛出一个异常。现在我们就陷入这种麻烦之中:从构造函数抛出异常是一种不好的思路。为何呢?如果我们试图在堆栈上创建一个对象,并且构造函数正常的完成了它的任务(不抛出异常),那么当这个对象离开作用域之后,它的析构函数会被调用。但是,如果构造函数并没有完成它的任务,而是抛出了一个异常,析构函数将不会被调用。
因此,在前面的例子当中,如果我们假设提供了名字却没有提供姓氏,表示名字的字符串将被分配内存,但永远不会被删除,因此导致了内存泄露。但是,情况还不至于无可挽回。更进一步观察,如果我们有一个包含了其他对象的对象,一个重要的问题是:哪些析构函数将被调用?哪些析构函数将不被调用?
以下是用一个小试验来说明:
class A { public: A() { cout<<"Creating A"<<endl; } ~A() { cout<<"Destroying A"<<endl; } }; class B { public: B() { cout<<"Creating B"<<endl; } ~B() { cout<<"Destroying B"<<endl; } }; class C : public A { public: C() { cout<<"Creating C"<<endl; Throw "Don't like C"; } ~C() { cout<<"Destroying C"<<endl; } private: B b_; };
注意,C类通过合成(即C类拥有一个B类型的数据成员)包含了B类。它还通过继承包含了A类型的对象(即在C类型的对象内部有一个A类型的对象)。现在,如果C的构造函数抛出一个异常,会发生什么情况呢?看以下的代码:
int main() { cout<<"Testing throwing from constructor."<<endl; try{ C c; }catch(...) { cout<<"Caught an exception."<<endl; } return 0; }
运行后将产生下面的输出:
Testing throwing from constuctor. Creating A Creating B Creating C Destroying B Destroying A Caught an exception.
注意,只有C的析构函数没有被执行,A和B的析构函数都被调用。因此上面问题的答案既简单又符合逻辑:对于允许构造函数正常结束的对象,析构函数将会被调用,即使这些对象是一个更大对象的一部分,而后者的构造函数并没有正常结束。因此,让我们用智能指针重写上面的示例代码,引入安全检查:
class PersonDescription { public: PersonDescription(const char* first_name, const char* last_name) : first_name_(NULL), last_name_(NULL) { SCPP_ASSERT(first_name != NULL ,"First name must be provided"); first_name_ = new string(first_name); SCPP_ASSERT(last_name != NULL ,"Last name must be provided"); last_name_ = new string(last_name); } ~PersonDescription() { delete first_name_; delete last_name_; } private: PersonDescription(const PersonDescription&); PersonDescription& operator = (const PersonDescription&); <span style="color:#ff0000;">scpp::ScopedPtr<string> first_name_; scpp::ScopedPtr<string> last_name_;</span> };
即使第2个安全检查抛出一个异常,指向first_name_的智能指针的析构函数仍然会被调用,并执行它的清理工作。另一个附带的好处是,我们并不需要操心把这些智能指针初始化为NULL,这是自动完成的。因此,我们看到从构造函数抛出异常是一种潜在的危险行为:对应的析构函数将不会被调用,因此可能会存在问题,除非析构函数是空函数。
总结:
从构造函数中抛出异常时为了避免内存泄露,在设计类的时候,使析构函数保持为空函数。
避免在析构函数中编写代码
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。