C++构造函数中抛出的异常

本文探讨了C++中构造函数抛出异常的几种情况及其影响,包括构造失败的通知方式、异常导致的析构行为及对象部分构造时的处理。

构造函数中抛出的异常

  1、标准C++中定义构造函数是一个对象构建自己,分配所需资源的地方,一旦构造函数执行完毕,则表明这个对象已经诞生了,有自己的行为和内部的运行状态,之后还有对象的消亡过程(析构函数的执行)。可谁能保证对象的构造过程一定能成功呢?说不定系统当前的某个资源不够,导致对象不能完全构建好自己(人都有畸形儿,更何况别的呢?朋友们!是吧!),因此通过什么方法来表明对象的构造失败了呢?C++程序员朋友们知道,C++中的构造函数是没有返回值的,所以不少关于C++编程方面的书上得出结论:“因为构造函数没有返回值,所以通知对象的构造失败的唯一方法那就是在构造函数中抛出异常”。主人公阿愚非常不同意这种说法,谁说的,便不信邪!虽然C++标准规定构造函数是没有返回值,可我们知道每个函数实际上都会有一个返回值的,这个值被保存在eax寄存器中,因此实际上是有办法通过编程来实现构造函数返回一个值给上层的对象创建者。当然即便是构造函数真的不能有返回值,我们也可以通过一个指针类型或引用类型的出参来获知对象的构造过程的状态。示例如下:

class MyTest_Base
{
public:
MyTest_Base (int& status)
{
//do other job

// 由于资源不够,对象构建失败
// 把status置0,通知对象的构建者
status = 0;
}

protected:
};

void main()
{
int status;
MyTest_Base obj1(status);

// 检查对象的构建是否成功
if(status ==0) cout << “对象构建失败” << endl;
}

  程序运行的结果是:
  对象构建失败
  是啊!上面我们不也得到了对象构造的成功与否的信息了吗?可大家有没有觉得这当中有点问题?主人公阿愚建议大家在此停留片刻,仔细想想它会有什么问题?OK!也许大家都知道了问题的所在,来验证一下吧!

class MyTest_Base
{
public:
MyTest_Base (int& status)
{
//do other job

// 由于资源不够,对象构建失败
// 把status置0,通知对象的构建者
status = 0;
}

virtual ~ MyTest_Base () 
{
cout << “销毁一个MyTest_Base类型的对象” << endl;
}


protected:
};

void main()
{
int status;
MyTest_Base obj1(status);

// 检查对象的构建是否成功
if(status ==0) cout << “对象构建失败” << endl;
}

  程序运行的结果是:
  对象构建失败
  销毁一个MyTest_Base类型的对象

  没错,对象的析构函数被运行了,这与C++标准中所规定的面向对象的一些特性是有冲突的。一个对象都没有完成自己的构造,又何来析构!好比一个夭折的畸形儿还没有出生,又何来死之言。因此这种方法是行不通的。那怎么办?那就是上面那个结论中的后一句话是对的,通知对象的构造失败的唯一方法那就是在构造函数中抛出异常,但原因却不是由于构造函数没有返回值而造成的。恰恰相反,C++标准中规定构造函数没有返回值正是由于担心很容易与面向对象的一些特性相冲突,因此干脆来个规定,构造函数不能有返回值(主人公阿愚的个人理解,有不同意见的朋友欢迎讨论)。

  2、构造函数中抛出异常将导致对象的析构函数不被执行。哈哈^-^,阿愚很开心,瞧瞧!如果没有C++的异常处理机制鼎立支持,C++中的面向对象特性都无法真正实现起来,C++标准总不能规定所有的对象都必须成功构造吧!这也太理想化了,也许只有等到共产主义社会实现的那一天(CPU可以随便拿,内存可以随便拿,所有的资源都是你的!)才说不定有可能·····,所以说C++的异常处理和面向对象确实是谁也离不开谁。当然示例还是要看一下,如下:

class MyTest_Base
{
public:
MyTest_Base (string name = “”) : m_name(name)
{
throw std::exception(“在构造函数中抛出一个异常,测试!”);
cout << “构造一个MyTest_Base类型的对象,对象名为:”<<m_name << endl;
}

virtual ~ MyTest_Base () 
{
cout << “销毁一个MyTest_Base类型的对象,对象名为:”<<m_name << endl;
}

void Func() throw()
{
throw std::exception(“故意抛出一个异常,测试!”);
}
void Other() {}

protected:
string m_name;
};

void main()
{
try
{
// 对象构造时将会抛出异常
MyTest_Base obj1(“obj1”);

obj1.Func();
obj1.Other();
}
catch(std::exception e)
{
cout << e.what() << endl;
}
catch(...)
{
cout << “unknow exception”<< endl;
}
}

  程序的运行结果将会验证:“构造函数中抛出异常将导致对象的析构函数不被执行”

  3、是不是到此,关于构造函数中抛出异常的处理的有关讨论就能结束了呢?非也!非也!主人公阿愚还有进一步的故事需要讲述!来看一个更复杂一点的例子吧!如下:

class MyTest_Base
{
public:
MyTest_Base (string name = "") : m_name(name)
{
cout << "构造一个MyTest_Base类型的对象,对象名为:"<<m_name << endl;
}

virtual ~ MyTest_Base () 
{
cout << "销毁一个MyTest_Base类型的对象,对象名为:"<<m_name << endl;
}

void Func() throw()
{
throw std::exception("故意抛出一个异常,测试!");
}
void Other() {}

protected:
string m_name;
};

class MyTest_Parts
{
public:
MyTest_Parts ()
{
cout << "构造一个MyTest_Parts类型的对象" << endl;
}

virtual ~ MyTest_Parts () 
{
cout << "销毁一个MyTest_Parts类型的对象"<< endl;
}
};

class MyTest_Derive : public MyTest_Base
{
public:
MyTest_Derive (string name = "") : m_component(), MyTest_Base(name)
{
throw std::exception("在MyTest_Derive对象的构造函数中抛出了一个异常!");

cout << "构造一个MyTest_Derive类型的对象,对象名为:"<<m_name << endl;
}

virtual ~ MyTest_Derive () 
{
cout << "销毁一个MyTest_Derive类型的对象,对象名为:"<<m_name << endl;
}

protected:
MyTest_Parts m_component;
};

void main()
{
try
{
// 对象构造时将会抛出异常
MyTest_Derive obj1("obj1");

obj1.Func();
obj1.Other();
}
catch(std::exception e)
{
cout << e.what() << endl;
}
catch(...)
{
cout << "unknow exception"<< endl;
}
}

  程序运行的结果是:
  构造一个MyTest_Base类型的对象,对象名为:obj1
  构造一个MyTest_Parts类型的对象
  销毁一个MyTest_Parts类型的对象
  销毁一个MyTest_Base类型的对象,对象名为:obj1
  在MyTest_Derive对象的构造函数中抛出了一个异常!

  上面这个例子中,MyTest_Derive从MyTest_Base继承,同时MyTest_Derive还有一个MyTest_Parts类型的成员变量。现在MyTest_Derive构造的时候,是在父类MyTest_Base已构造完毕和MyTest_Parts类型的成员变量m_component也已构造完毕之后,再抛出了一个异常,这种情况称为对象的部分构造。是的,这种情况很常见,对象总是由不断的继承或不断的聚合而来,对象的构造过程实际上是这些所有的子对象按规定顺序的构造过程,其中这些过程中的任何一个子对象在构造时发生异常,对象都不能说自己完成了全部的构造过程,因此这里就有一个棘手的问题,当发生对象的部分构造时,对象将析构吗?如果时,又将如何析构呢?

  从运行结果可以得出如下结论:
  (1) 对象的部分构造是很常见的,异常的发生点也完全是随机的,程序员要谨慎处理这种情况;
  (2) 当对象发生部分构造时,已经构造完毕的子对象将会逆序地被析构(即异常发生点前面的对象);而还没有开始构建的子对象将不会被构造了(即异常发生点后面的对象),当然它也就没有析构过程了;还有正在构建的子对象和对象自己本身将停止继续构建(即出现异常的对象),并且它的析构是不会被执行的。

  构造函数中抛出异常时概括性总结
  (1) C++中通知对象构造失败的唯一方法那就是在构造函数中抛出异常;
  (2) 构造函数中抛出异常将导致对象的析构函数不被执行;
  (3) 当对象发生部分构造时,已经构造完毕的子对象将会逆序地被析构;
  (4) 哈哈^-^,其是还是那句话, “C++的异常处理不会破坏任何一条面向对象的特性!”,因此主人公阿愚再次建议朋友们,牢牢记住这一条!


转载:http://blog.youkuaiyun.com/smking/article/details/5286900



<think> 我们被要求回答关于C++构造函数中手动抛出异常的方法。 根据引用内容,我们可以总结以下几点: 1. 在构造函数中,如果遇到错误情况,可以直接使用throw语句抛出异常(如引用[3]和引用[4]中的示例)。 2. 当构造函数抛出异常时,由于对象尚未完全构造,因此不会调用该对象的析构函数(引用[3]中提到“析构函数不会被调用”)。 3. 但是,在构造函数中已经初始化的成员变量(包括基类子对象)会被正确地销毁(即它们的析构函数会被调用)。 4. 因此,在构造函数抛出异常时,需要注意资源泄露问题。通常建议使用智能指针或RAII技术来管理资源,以避免泄露(引用[3]提到“两段初始化和智能指针”)。 具体方法: 在构造函数体内,我们可以像普通函数一样使用throw关键字抛出异常。例如: throw std::runtime_error("Error message"); 另外,在构造函数的初始化列表(成员初始化列表)中也可以抛出异常,但通常我们需要通过函数try块(function-try-block)来捕获初始化列表中可能抛出异常(引用[2]提到委托构造函数异常处理)。 但是,请注意:在初始化列表中抛出异常时,如果使用函数try块捕获,则捕获后必须重新抛出或终止构造(引用[2]选项B)。因为构造函数初始化列表中的异常表示构造失败,对象不会被完全构造,所以我们必须确保异常被重新抛出,让调用者知道构造失败。 示例1:在构造函数体内抛出异常 class MyClass { public: MyClass(int value) { if (value < 0) { throw std::invalid_argument("Negative value not allowed"); } // ... 其他初始化 } }; 示例2:在初始化列表中抛出异常(通过函数try块) class MyClass { public: MyClass(int value) try : someResource(value) { // 构造函数体 } catch (const std::exception& e) { // 这里可以处理异常,但必须重新抛出(或者抛出另一个异常,或者终止程序) // 注意:此时对象没有构造完成,所以只能清理在进入构造函数体之前发生的资源分配(如基类或成员子对象已经构造,它们的析构函数会被调用,但本类的析构函数不会执行) throw; // 重新抛出当前异常 } private: SomeType someResource; }; 注意:函数try块主要用于构造函数,可以捕获初始化列表和构造函数体中的异常。但是,在构造函数中捕获异常后,如果没有重新抛出,编译器会自动重新抛出(即不允许吞掉异常)。 另外,引用[4]中的示例展示了在构造函数抛出异常的情况,并且捕获了异常,但注意该示例中析构函数没有被调用。 因此,手动抛出异常的方法就是使用throw语句,位置可以在初始化列表(通过函数try块捕获)或构造函数体内。 但是,需要特别注意资源管理: - 在构造函数中,如果先申请了资源(比如new了内存),然后在抛出异常之前,那么这些资源需要手动释放,否则会造成泄露。 - 使用RAII对象(如智能指针)来管理资源,可以避免这个问题。例如,使用std::unique_ptr来管理动态分配的内存,这样当构造函数抛出异常时,已经构造的成员(即智能指针)会被销毁,从而自动释放内存。 引用[3]中的BaseManager例子就展示了资源泄露的情况:在构造函数中new了Base对象,然后抛出异常,此时由于构造函数没有完成,BaseManager的析构函数不会被调用,导致_base指向的内存泄露。解决方案是使用智能指针: class BaseManager { private: std::unique_ptr<Base> _base; public: BaseManager() : _base(std::make_unique<Base>(2)) { throw "-1"; // 此时即使抛出异常,_base这个unique_ptr成员已经被构造,在异常发生时会自动销毁,从而释放Base对象 } // 不需要手动写析构函数,因为unique_ptr会自动释放 }; 所以,总结手动抛出异常的方法: 1. 在构造函数的初始化列表中抛出:直接在初始化列表中调用可能抛出异常的函数(包括构造函数),然后通过函数try块捕获(可选)。 2. 在构造函数体内抛出:使用throw语句。 但无论哪种方式,都要确保资源被正确释放,推荐使用RAII。 因此,回答如下: </think> 在C++构造函数中手动抛出异常是一种处理对象初始化失败的标准机制,具体实现方法和注意事项如下: ### 一、手动抛出异常的基本方法 在构造函数中使用 `throw` 语句即可主动抛出异常: ```cpp class MyClass { public: MyClass(int value) { if (value < 0) { // 验证参数合法性 throw std::invalid_argument("Value must be non-negative"); } // 其他初始化代码... } }; ``` ### 二、关键特性与注意事项 1. **对象构造终止** 构造函数抛出异常时,对象被视为**未完全构造**,析构函数不会自动调用[^3]。 ```cpp class Test { public: Test() { throw 1; } ~Test() {} // 不会被执行 }; ``` 2. **成员资源清理** - 已构造的成员子对象会自动调用其析构函数(栈上对象) - 原始指针资源需手动释放: ```cpp class ResourceHolder { int* ptr; public: ResourceHolder() : ptr(new int[100]) { throw std::runtime_error("Fail"); // 内存泄漏! } ~ResourceHolder() { delete[] ptr; } }; ``` 3. **推荐RAII解决方案** 使用智能指针自动管理资源: ```cpp class SafeHolder { std::unique_ptr<int[]> ptr; // RAII包装 public: SafeHolder() : ptr(std::make_unique<int[]>(100)) { throw std::runtime_error("Fail"); // 无泄漏,ptr自动释放 } // 无需显式析构函数 }; ``` 4. **初始化列表中的异常** 需通过函数try块捕获: ```cpp class Complex { std::vector<int> data; public: Complex(size_t size) try : data(size) { // 函数try块 if (size > 1000) throw std::bad_alloc(); } catch (...) { // 必须重新抛出或终止 throw; } }; ``` ### 三、继承场景下的特殊处理 ```mermaid classDiagram class Base { +Base() 可能抛出异常 } class Derived { +Derived() 异常时已构造的基类子对象自动销毁 } Base <|-- Derived ``` - 基类构造函数抛出异常 → 派生类构造终止 - 派生类构造函数抛出异常 → 基类子对象自动销毁[^1] ### 四、最佳实践 1. **优先使用RAII** 用 `std::unique_ptr`/`std::shared_ptr` 管理动态资源 2. **避免在析构函数抛出** C++11后析构函数默认 `noexcept`[^1] 3. **委托构造函数异常** 使用函数try块捕获并重新抛出[^2] 4. **提供强异常保证** 确保操作失败时对象状态可回滚 > 示例:安全构造函数模板 > ```cpp > class SafeObject { > std::unique_ptr<Resource> res; > public: > SafeObject(const Config& cfg) > : res(std::make_unique<Resource>()) // 先初始化资源 > { > if (!res->load(cfg)) { // 后验证 > throw InitFailed("Load failed"); > } > } // 异常时res自动释放 > }; > ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值