C++学习之构造函数中的异常处理

news/2024/5/18 22:11:51 标签: c++, 异常处理, 构造函数, 内存泄露

构造函数中可不可以抛出异常?当然可以。从语法上来说,是可以的;从实际情况来看,现在的软件系统日渐庞大和复杂,很难保证 Constructor 在执行过程中完全不发生一点异常。

那么,如果构造函数中抛出异常,会发生什么情况呢?

构造函数中抛出异常将导致对象的析构函数不被执行。" style="clear:both; font-size:1.4em; margin:0.99em 0px 0.66em; color:rgb(34,34,34); font-family:'Roboto Condensed',Tauri,'Lucida Grande','Lucida Sans Unicode','Lucida Sans',AppleSDGothicNeo-Medium,'Segoe UI','Malgun Gothic',Verdana,Tahoma,sans-serif"> 构造函数中抛出异常将导致对象的析构函数不被执行。" style="text-decoration:none; vertical-align:baseline; color:rgb(50,105,160)">一、构造函数中抛出异常将导致对象的析构函数不被执行。

C++仅能 delete 被完全构造的对象(fully constructed objects),只有一个对象的构造函数完全运行完毕,这个对象才被完全地构造。所以如果在构造函数中抛出一个异常,这个异常将传递到创建对象的地方(程序控制权也会随之转移),这样对象就只是部分被构造,它的析构函数将不会被执行。

看下面的示例:

#pragma once
#include <iostream>
#include <string>
using namespace std;

/******************类定义**********************/
class person {
public:
	person(const string& str):name(str)
	{
		//throw exception("测试:在构造函数中抛出一个异常");
		cout << "构造一个对象!" << endl;
	};
	~person()
	{
		cout << "销毁一个对象!" << endl;
	};
private:
	string name;
};

/******************测试类**********************/
int main()
{
	try 
	{
		person me("songlee");
	} 
	catch(exception e) 
	{
		cout << e.what() << endl;
	};
	getchar();
	return 0;
}
注意上面的 me 是一个局部对象,所以离开 try{} 的作用域,会自动执行析构函数。运行上述代码,输出结果如下:

构造一个对象!
销毁一个对象!
如果在构造函数中抛出一个异常(去掉注释),输出结果如下:

测试:在构造函数中抛出一个异常

可以看出,析构函数没有被自动执行。为什么“构造一个对象!”也没有输出呢?因为程序控制权转移了,所以在异常点以后的语句都不会被执行。

构造函数抛出异常可能导致内存泄露" style="clear:both; font-size:1.4em; margin:0.99em 0px 0.66em; color:rgb(34,34,34); font-family:'Roboto Condensed',Tauri,'Lucida Grande','Lucida Sans Unicode','Lucida Sans',AppleSDGothicNeo-Medium,'Segoe UI','Malgun Gothic',Verdana,Tahoma,sans-serif"> 构造函数抛出异常可能导致内存泄露" style="text-decoration:none; vertical-align:baseline; color:rgb(50,105,160)">二、构造函数抛出异常可能导致内存泄露

#pragma once
#include <string>
#include <iostream>
using namespace std;

class A {
public:
	A(){};
};

class B {
public:
	B() { 
		//throw exception("测试:在B的构造函数中抛出一个异常");
		cout << "构造 B 对象!" << endl;
	};
	~B(){ cout << "销毁 B 对象!" << endl; };
};

class Tester {
public:
	Tester(const string& name, const string& address);
	~Tester();
private:
	string theName;
	string theAddress;
	A *a;
	B *b;
};
上面声明了三个类(A、B、Tester),其中Tester类的构造函数和析构函数定义如下:

Tester::Tester(const string& name, const string& address):
	theName(name),
	theAddress(address)
{
	a = new A();
	b = new B();  // <——
	cout << "构造 Tester 对象!" << endl;
}

Tester::~Tester()
{
	delete a;
	delete b;
	cout << "销毁 Tester 对象!" << endl;
}

构造函数中,动态的分配了内存空间给a、b两个指针。析构函数负责删除这些指针,确保Tester对象不会发生内存泄露(C++中delete一个空指针也是安全的)。

int main()
{
	Tester *tes = NULL;
	try 
	{
		tes = new Tester("songlee","201");
	} 
	catch(exception e) 
	{
		cout << e.what() << endl;
	};
	delete tes; // 删除NULL指针是安全的
	getchar();
	return 0;
}
运行输出结果:

构造 B 对象!
构造 Tester 对象!
销毁 B 对象!
销毁 Tester 对象!

看上去好像一切良好,在正常情况下确实没有错。但是在有异常的情况下,恐怕就不会良好了。

试想在 Tester 的构造函数执行时,b = new B()抛出了异常:可能是因为operator new不能给B对象分配足够的内存,也可能是因为 B 的构造函数自己抛出了一个异常。不论什么原因,在 Tester 构造函数内抛出异常,这个异常将传递到建立 Tester 对象的地方(程序控制权也会转移)。

在 B 的构造函数里抛出异常(去掉注释)时,程序运行结果如下:

测试:在B的构造函数中抛出一个异常

可以看出,C++拒绝为没有完成构造操作的对象调用析构函数,即使你使用了delete语句。由于 Tester 的析构函数不会执行,所以给A对象 a 动态分配(new)的空间无法释放,将造成内存泄露

注:不用为 Tester 对象中的非指针数据成员操心,因为它们不是new出来的,且在异常抛出之前已经构造完全,所以它们会自动逆序析构。

内存泄露的方法" style="font-size:1.6em; color:rgb(34,34,34); clear:both; margin:1.125em 0px 0.75em; font-family:'Roboto Condensed',Tauri,'Lucida Grande','Lucida Sans Unicode','Lucida Sans',AppleSDGothicNeo-Medium,'Segoe UI','Malgun Gothic',Verdana,Tahoma,sans-serif"> 内存泄露的方法" style="text-decoration:none; vertical-align:baseline; color:rgb(50,105,160)">三、解决上述内存泄露的方法

因为当对象在构造中抛出异常后C++不负责清除(动态分配)的对象,所以你必须重新设计构造函数以让它们自己清除。常用的方法是捕获所有的异常,然后执行一些清除代码,最后再重新抛出异常让它继续传递。

示例代码如下:

Tester::Tester(const string& name, const string& address):
	theName(name),
	theAddress(address),
	a(NULL),   // 初始化为空指针是必须的
	b(NULL)
{
	try 
	{
		a = new A();
		b = new B();  
	} 
	catch(...)   // 捕获所有异常
	{
		delete a;
		delete b;
		throw;   // 继续传递异常
	}
}

另一种更好的方法是使用智能指针(smart pointer),不过关于智能指针的内容比较多,在这里就不说了。







总结:

  • 构造函数中抛出异常是C++中通知对象构造失败的唯一方法。

  • 构造函数中抛出异常,对象的析构函数将不会被执行。

  • 构造函数抛出异常时,本应该在析构函数中被delete的对象没有被delete,会导致内存泄露

  • 当对象发生部分构造时,已经构造完毕的子对象(非动态分配)将会逆序地被析构。



个人站点:http://songlee24.github.com










http://www.niftyadmin.cn/n/1092054.html

相关文章

1年时间业务量疯长40倍,谈人人车的平台架构演进之路

人人车业务平台从最初典型的LNMP单机单服务部署架构&#xff0c;发展到如今分布式服务化架构&#xff0c;五百多台虚拟机部署&#xff0c;一路走来&#xff0c;踩过不少坑&#xff0c;也遇到过不少挑战&#xff0c;特别是对于基于云服务进行业务开发的场景&#xff0c;以及从零…

sqlserver 中NOLOCK、HOLDLOCK、UPDLOCK、TABLOCK、TABLOCKX

SqlServer查询语句中用到的锁 作者: wokofo 前段时间**公司DBA来我们这培训。讲了一大堆MYSQL的优化。 QA环节一程序员问“SQL语句中的 with nolock 除了不锁表外,是否能读其他锁住的数据"。 讲课的人嘟嘟了半天没解释清楚&#xff08;有可能是MYSQL里没有这个机制&#x…

面试题之strcpy/strlen/strcat/strcmp的实现

阿里的电面要我用C/C实现一个字符串拷贝的函数&#xff0c;虽然以前写过 strcpy 的函数实现&#xff0c;但时间过去很久了&#xff0c;再加上有点紧张&#xff0c;突然就措手不及了。最后写是写出来了&#xff0c;但没考虑异常的情况&#xff0c;面试官好像很不满意。(T_T)&…

Java 并发 —— volatile 关键字

volatile 修饰变量等于向编译器传达如下两层含义&#xff1a; 保证了不同线程对这个变量进行操作时的可见性&#xff0c;即一个线程修改了某个变量的值&#xff0c;这新值对其他线程来说是立即可见的。禁止进行指令重排序。volatile 关键字定义了读写发生的次序&#xff1a; 对…

java 21 - 4 字符流的文件复制操作以及简化

既然字节流可以复制文件&#xff0c;那么字符流当然也有。 同样的思路&#xff1a;  数据源&#xff1a;    a.txt -- 读取数据 -- 字符转换流 -- InputStreamReader目的地&#xff1a;    b.txt -- 写出数据 -- 字符转换流 -- OutputStreamWriter 1 // 封装数…

TFTP服务器 NFS服务器 smb 、wireshark

在嵌入式开发过程中有宿主机和目标机的角色之分 宿主机是执行编译、链接嵌入式软件的计算机&#xff1b;目标机是运行嵌入式软件的硬件平台 宿主机一般通过网络、USB、JTAG等方式将软件下载到目标机。 网络下载&#xff1a;tftp服务器。 Tftp服务器作为工作于宿主机上的软件&a…

Cracking the Coding Interview 150题(一)

1、数组与字符串 1.1 实现一个算法&#xff0c;确定一个字符串的所有字符是否全都不同。假设不允许使用额外的数据结构&#xff0c;又该如何处理&#xff1f; 1.2 用C或C实现void reverse(char* str)函数&#xff0c;即反转一个null结尾的字符串。 1.3 给定两个字符串&#x…

【Asp.net入门3-02】使用jQuery-jQuery 入门

下面的几小节将介绍jQuery的基础知识。如前所述&#xff0c;不可能仅仅通过一章的内容详细介绍jQuery&#xff0c; 但可以向你说明如何对HTML文档中的内容执行简单的操作&#xff0c;以及更重要的&#xff0c;如何逐步实现本书其他 部分的示例。 jQuery功能通过一个名为jQuery的…