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


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

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

    一、构造函数中抛出异常将导致对象的析构函数不被执行。

    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{}的作用域,会自动执行析构函数。运行上述代码,输出结果如下:

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

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

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

    二、构造函数抛出异常可能导致内存泄露

    #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出来的,且在异常抛出之前已经构造完全,所以它们会自动逆序析构。

    三、解决上述内存泄露的方法

    因为当对象在构造中抛出异常后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









  • 相关阅读:
    通用页面调用APP 互通
    HTML5[5]:在移动端禁用长按选中文本功能
    JAVA 中的基础
    手机访问PC网站自动跳转到手机网站代码
    自适应的设置字体的方式
    localStorage 与 sessionStorage
    《高级程序设计》3 基本慨念
    javascript基础
    jQuery技巧
    jQuery性能优化
  • 原文地址:https://www.cnblogs.com/songlee/p/5738092.html
Copyright © 2020-2023  润新知