避免在析构函数中编写代码

上篇文章中,我们介绍了为什么应该彻底避免编写拷贝构造函数和赋值操作符。今天这篇我们讨论下为什么应该避免在析构函数中编写代码。即让析构函数为空。

例如:

virtual ~MyClass()
{
}

我们用空析构函数这个术语表示花括号内没有代码的析构函数。

需要编写析构函数可能有如下几个原因:

  • 在基类中,可能需要声明虚拟析构函数,这样就可以使用一个指向基类的指针指向一个派生类的实例。
  • 在派生类中,并不需要把析构函数声明为虚拟函数,但是为了增强可读性,也可以这样做。
  • 可能需要声明析构函数并不抛出任何异常。

对于最后一种情况,我们将详细讨论。在C++中,从析构函数中抛出异常被认为是不好的思路。这是因为析构函数常常是在一个异常已经被抛出的情况下被调用的,在这个过程中再次抛出异常将导致程序终止(或崩溃),这很可能违背程序员的初衷。因此,在有些类中,析构函数被声明为如下:

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,这是自动完成的。因此,我们看到从构造函数抛出异常是一种潜在的危险行为:对应的析构函数将不会被调用,因此可能会存在问题,除非析构函数是空函数。

总结:

从构造函数中抛出异常时为了避免内存泄露,在设计类的时候,使析构函数保持为空函数。

时间: 2024-10-31 13:35:28

避免在析构函数中编写代码的相关文章

Xcode概览:在源码编辑器中编写代码 --【转载】

转自:http://www.cocoachina.com/ios/20141204/10394.html 本章节由CocoaChina翻译组成员星夜暮晨(博客)翻译自Xcode Overview:Write Code in the Source Editor,CocoaChina校对,敬请勘误. 您将大部分开发时间花在了编写.编辑以及调试代码上.Xcode源码编辑器的语法修正.代码补全以及静态代码分析等特性可以帮您快速准确地键入代码.而诸如分拆窗口.快捷键.syntax-aware字体以及文本颜

eclipse中编写代码时如何自动提示变量名?

打开 Eclipse  -> Window -> Perferences -> Java -> Editor -> Content Assist,在右边最下面一栏找到 auto-Activation ,下面有三个选项,找到第二个“Auto activation triggers for Java:”选项 在其后的文本框中会看到一个“.”存在.这表示:只有输入“.”之后才会有代码提示和自动补全,我们要修改的地方就是这里.把该文本框中的“.”换掉,换成“.abcdefghijkl

在vs中编写代码常用的快捷键

作为一个程序员,能够熟悉使用各种快捷键,可以增加我们编写和调试代码的速度,下面我就对常使用的快捷键做一些总结,下面这些快捷键基本适用于所有版本的vs: 最给力: Ctrl+K+F   快速整理代码格式 快捷键              功能 Ctrl+Space    直接完成类或函数(替代的快捷键是Alt+Right) Shift+Delete    整行删除,并且将这一行放到剪贴板(这时候不能选中一段内容) Ctrl+Up,Ctrl+Down 滚动编辑器,但尽量不移动光标,光标保证在可见范围

Xilinx Vivado的使用详细介绍(1):创建工程、编写代码、行为仿真、Testbench

新建工程 打开Vivado软件,直接在欢迎界面点击Create New Project,或在开始菜单中选择File - New Project即可新建工程. 点击Next 输入工程名称和路径. 选择RTL Project,勾选Do not specify......(这样可以跳过添加源文件的步骤,源文件可以后面再添加). 根据自己的开发板选择器件型号,可以直接通过型号进行搜索,例如Basys3开发板上的芯片型号为xc7a35tcpg236-1.如果不了解或者暂时不写进开发板,可以随便选一个型号,

[转]Xilinx Vivado的使用详细介绍(1):创建工程、编写代码、行为仿真、Testbench

新建工程 打开Vivado软件,直接在欢迎界面点击Create New Project,或在开始菜单中选择File - New Project即可新建工程. 点击Next 输入工程名称和路径. 选择RTL Project,勾选Do not specify......(这样可以跳过添加源文件的步骤,源文件可以后面再添加). 根据自己的开发板选择器件型号,可以直接通过型号进行搜索,例如Basys3开发板上的芯片型号为xc7a35tcpg236-1.如果不了解或者暂时不写进开发板,可以随便选一个型号,

C#和JAVA中编写事务代码

C#  DAL层代码,执行多条增删改,使用事务操作: /// <summary> /// 执行 多条增删改 (非查询语句) /// </summary> /// <param name="strSql"></param> /// <param name="paras"></param> /// <returns></returns> public static int E

C#中的快捷键,可以更方便的编写代码 (转载)

C#中的快捷键,可以更方便的编写代码 CTRL + SHIFT + B 生成解决方案 CTRL + F7 生成编译 CTRL + O 打开文件 CTRL + SHIFT + O 打开项目 CTRL + SHIFT + C 显示类视图窗口 F4 显示属性窗口 SHIFT + F4 显示项目属性窗口 CTRL + SHIFT + E 显示资源视图 F12 转到定义 CTRL + F12 转到声明 CTRL + ALT + J 对象浏览 CTRL + ALT + F1 帮助目录 CTRL + F1 动

Android win7系统,Eclipse编写代码时,在sourceInsight中查看出现对不齐的问题

win7系统,Eclipse编写代码时,在sourceInsight中查看出现对不齐的问题: 用空格键代替tab键,步骤如下: Step 1: Windows->Preferences->General->Editors->Text Editors,选中"Insert spaces for tabs". Step 2: Windows->Preferences->Java->Code Styles->Formatter,点击Acitivi

在存储过程中编写正确的事务处理代码

在 SQL Server 中数据库事务处理是个重要的概念,也稍微有些不容易理解,很多 SQL 初学者编写的事务处理代码存往往存在漏洞, 本文介绍了三种不同的方法,举例说明了如何在存储过程事务处理中编写正确的代码. 在编写 SQL Server 事务相关的存储过程代码时,经常看到下面这样的写法: begin tran update statement 1 ... update statement 2 ... delete statement 3 ... commit tran 这样编写的SQL存在