C++异常处理机制详解



异常处理是一种允许两个独立开发的程序组件在程序执行期间遇到程序不正常的情况(异常exception)时相互通信的机制。本文总结了19个C++异常处理中的常见问题,基本涵盖了一般C++程序开发所需的关于异常处理部分的细节。

 

1. throw可以抛出哪些种类的异常对象?如何捕获?

1)异常对象通常是一个class对象, 通常用以下代码抛出:

// 调用的类的构造函数

throw popOnEmpty();

但是throw 表达式也可以抛出任何类型的对象, 例如(虽然很不常见)在下面的代码例子中,函数mathFunc()抛出一个枚举类型的异常对象

enum EHstate { noErr, zeroOp, negativeOp, severeError };

int mathFunc( int i )

{

    if ( i == 0 )

    throw zeroOp; // 枚举类型的异常

}

2)抛出异常的语句或其调用函数要在try块中才能被捕获。

 

2. catch子句的语法

一个catch 子句由三部分构成:

1)关键字catch

2)异常声明,在括号中的单个类型或单个对象声明被(称作异常声明,exception declaration)

3)复合语句中的一组语句。

// stackExcp.h

class popOnEmpty { };

class popOnFull { };

catch ( pushOnFull )

{

    cerr << "trying to push a value on a full stack\n";

    return errorCode88;

}

 

3. 异常声明可以只是一个类型声明而不是对象声明吗?

catch 子句的异常声明可以是一个类型声明或一个对象声明。当我们要获得throw 表达式的值或者要操纵throw 表达式所创建的异常对象时,我们应该声明一个对象。

catch ( pushOnFull eObj )

{

    cerr << "trying to push the value " << eObj.value() << " on a full stack\n";

}

 

4. 异常声明中异常对象的拷贝过程?

catch 子句异常声明的行为特别像参数声明。同理,也可以分出按值传递和引用传递(指针)。通常采用的是引用传递。

例1:按值传递。当进入catch 子句时,如果异常声明声明了一个对象,则用该异常对象的拷贝初始化这个对象。例中对象eObj 是用异常对象的值来初始化的,会调用拷贝构造函数。

void calculate( int op ) {

try {

mathFunc( op );

}

catch (pushOnFull eObj ) {

// eObj 是被抛出的异常对象的拷贝

}

}

例2:引用传递。catch子句可以直接引用由throw 表达式创建的异常对象,而不是创建一个局部拷贝。可以防止不必要地拷贝大型类对象。

void calculate( int op ) {

try {

mathFunc( op );

}

catch (pushOnFull &eObj ) {

// eObj 引用了被抛出的异常对象

}

}

 

5. 异常处理的栈展开过程是什么?

在查找用来处理被抛出异常的catch 子句时,因为异常而退出复合语句和函数定义,这个过程被称作栈展开(stack unwinding)。随着栈的展开,在退出的复合语句和函数定义中声明的局部变量的生命期也结束了。C++保证,随着栈的展开,尽管局部类对象的生命期是因为抛出异常而被结束,但是这些局部类对象的析构函数也会被调用。

 

6. 异常抛出没有在try块中或抛出的异常没有对应的catch语句来捕捉,结果如何?

异常不能够保持在未被处理的状态,异常对于一个程序非常重要,它表示程序不能够继续正常执行。如果没有找到处理代码,程序就调用C++标准库中定义的函数terminate()。terminate()的缺省行为是调用abort() ,指示从程序非正常退出。

 

7.为什么要重新抛出异常?怎么写?

在异常处理过程中也可能存在“单个catch 子句不能完全处理异常”的情况。在对异常对象进行修改或增加某些信息之后,catch 子句可能决定该异常必须由函数调用链中更上级的函数来处理。表达式的形式为:throw;

例子如下:

try

        {

            entryDescr->checkMandatoryData(beModel_);

        }

        catch (CatchableOAMexception & error) // 只能用引用声明

        {

            vector<string> paramList;

            paramList.push_back(currentDn);

            error.addFrameToEnd(6,paramList);  // 修改异常对象

            throw;  //重新抛出异常, 并由另一个catch 子句来处理

        }

注意1:被重新抛出的异常就是原来的异常对象,所以异常声明一定要用引用。

注意2:在catch 语句里也可以抛出其它

 

8. 怎么捕捉全部异常或未知异常?

可以用catch ( ... ) { } 。

作用在于:1. 可以释放在前面获得的资源(如动态内存),因为异常退出,这些资源为释放。2. 捕获其余类型的未知异常。

catch 子句被检查的顺序与它们在try 块之后出现的顺序相同。一旦找到了一个匹配,则后续的catch 子句将不再检查。这意味着如果catch(...)与其他catch 子句联合使用,它必须总是被放在异常处理代码表的最后,否则就会产生一个编译时刻错误。例子如下:

catch ( pushOnFull ) {}

catch ( popOnEmpty ) { }

catch (...) { } // 必须是最后一个catch 子句

 

9. 为什么 catch 子句的异常声明通常被声明为引用?

1)可以避免由异常对象到 catch 子句中的对象的拷贝,特别是对象比较大时。

2)能确保catch子句对异常对象的修改能再次抛出。

3)确保能正确地调用与异常类型相关联的虚拟函数,避免对象切割。

具体参见4,7,17。

 

10. 异常对象的生命周期?

产生:throw className()时产生。

销毁:该异常的最后一个catch 子句退出时销毁

注意:因为异常可能在catch子句中被重新抛出,所以在到达最后一个处理该异常的catch 子句之前,异常对象是不能被销毁的。

 

11. const char *到char * 非法的异常类型转换。

我们注意到下面的代码在VC中可以正常运行(gcc不能)。

try { throw "exception";}

catch (char *) {cout << "exception catch!" <<endl;}

实际上throw的是一个const char *, catch的时候转型成char *。这是C++对C的向下兼容。

同样的问题存在于:

1. char *p =  “test”; // 也是一个const char * 到char *转型。

2. void func(char* p) { printf("%s\n", p); }

    func("abc"); // const char * 到char *

以上两例在编译时不警告,运行时不出错,是存在隐患的。

 

12. 异常规范(exception specification)的概念?

异常规范在函数声明是规定了函数可以抛出且只能抛出哪些异常。空的异常规范保证函数不会抛出任何异常。如果一个函数声明没有指定异常规范,则该函数可以抛出任何类型的异常。

例1:函数Pop若有异常,只能抛出popOnEmpty和string类型的异常对象

void pop( int &value ) throw(popOnEmpty, string);

例2:函数no_problem()保证不会抛出任何异常

extern void no_problem() throw();

例3:函数problem()可以抛出任何类型的异常

extern void problem();

 

13. 函数指针的异常规范?

我们也可以在函数指针的声明处给出一个异常规范。例如:

void (*pf) (int) throw(string);

当带有异常规范的函数指针被初始化或被赋值时,用作初始值或右值的指针异常规范必须与被初始化或赋值的指针异常规范一样或更严格。例如:

void recoup( int, int ) throw(exceptionType);

void no_problem() throw();

void doit( int, int ) throw(string, exceptionType);

// ok: recoup() 与 pf1 的异常规范一样严格

void (*pf1)( int, int ) throw(exceptionType) = &recoup;

// ok: no_problem() 比 pf2 更严格

void (*pf2)() throw(string) = &no_problem;

// 错误: doit()没有 pf3 严格

void (*pf3)( int, int ) throw(string) = &doit;

注:在VC和gcc上测试失败。

 

14. 派生类中虚函数的异常规范的声明?

基类中虚拟函数的异常规范,可以与派生类改写的成员函数的异常规范不同。但是派生类虚拟函数的异常规范必须与基类虚拟函数的异常规范一样或者更严格。

class Base {

public:

virtual double f1( double ) throw ();

virtual int f2( int ) throw ( int );

virtual string f3( ) throw ( int, string );

// ...

};

class Derived : public Base {

public:

// error: 异常规范没有 base::f1() 的严格

double f1( double ) throw ( string );

// ok: 与 base::f2() 相同的异常规范

int f2( int ) throw ( int );

// ok: 派生 f3() 更严格

string f3( ) throw ( int );

// ...

};

 

15. 被抛出的异常的类型和异常规范中指定的类型能进行类型转换吗?

int convert( int parm ) throw(string)

{

if ( somethingRather )

// 程序错误:

// convert() 不允许 const char* 型的异常

throw "help!";

}

throw 表达式抛出一个C 风格的字符串,由这个throw 表达式创建的异常对象的类型为const char*。通常,const char*型的表达式可以被转换成string 类型。但是,异常规范不允许从被抛出的异常类型到异常规范指定的类型之问的转换。

注意:

当异常规范指定一个类类型(类类型的指针)时,如果一个异常规范指定了一个类,则该函数可以抛出“从该类公有派生的类类型”的异常对象。类指针同理。

例如:

class popOnEmpty : public stackExcp { };

void stackManip() throw( stackExcp )  // 异常规范是stackExcp类型

{

    throw stackExcp();            // 与异常规范一样

    throw popOnEmpty ();      // ok. 是stackExcp的派生类

}

 

16. 公有基类的catch子句可以捕捉到其派生类的异常对象。

int main( ) {

try {

// 抛出pushOnFull异常

}

catch ( Excp ) {

// 处理 popOnEmpty 和 pushOnFull 异常

throw;

}

catch ( pushOnFull ) {

// 处理 pushOnFull 异常

}

}

在上例中,进入catch ( Excp )子句,重新抛出的异常任然是pushOnFull类型的异常对象,而不会是其基类对象Excp。

 

17. 异常对象中怎么运用虚拟函数来完成多态?

1)异常申明是对象(不是引用或指针),类似于普通的函数调用,发生对象切割。

// 定义了虚拟函数的新类定义

class Excp {

public:

virtual void print() {

cerr << "An exception has occurred"

<< endl;

}

};

class stackExcp : public Excp { };

class pushOnFull : public stackExcp {

public:

virtual void print() {

cerr << "trying to push the value " << _value

<< " on a full stack\n";

}

// ...

};

int main( ) {

try {

// iStack::push() throws a pushOnFull exception

} catch ( Excp eObj ) {

eobj.print(); // 调用虚拟函数

// 喔! 调用基类实例

}

}

对象切割过程:eObj 以“异常对象的基类子对象Excp 的一个拷贝”作为初始值,eobj 是Excp 类型的对象,而不是pushOnFull 类型的对象。

输出结果:

An exception has occurred

2)异常声明是一个指针或引用

int main( ) {

try {

// iStack::push() 抛出一个 pushOnFull 异常

}

catch ( Excp &eObj ) {

eobj.print(); // 调用虚拟函数 pushOnFull::print()

}

}

输出结果:

trying to push the value 879 on a full stack

 

18. function try block(函数try块)

把整个函数体包含在一个try块中

int main()

try {

// main() 的函数体

}

catch ( pushOnFull ) {

// ...

}

catch ( popOnEmpty ) {

// ...

}

 

19. 为什么类的构造函数需要函数try块?

如下例,普通的try块

inline Account::无法处理成员初始化表中的异常,若serviceCharge抛出异常,则这个异常无法被捕捉到。

Account( const char* name, double opening_bal )

: _balance( opening_bal - serviceCharge() )

{

try {

_name = new char[ strlen(name)+1 ];

strcpy( _name, name );

_acct_nmbr = get_unique_acct_nmbr();

}

catch ( ...) {

// 特殊处理

// 不能捕获来自成员初始化表的异常

}

}

改进后如下,使用函数try 块是保证“在构造函数中捕获所有在对象构造期间抛出的异常”的惟一解决方案。关键字try 应该被放在成员初始化表之前,try 块的复合语句包围了构造函数体。

inline Account::

Account( const char* name, double opening_bal )

try

: _balance( opening_bal - serviceCharge() )

{

_name = new char[ strlen(name)+1 ];

strcpy( _name, name );

_acct_nmbr = get_unique_acct_nmbr();

}

catch ( ... )

{

// 特殊处理

// 现在能够捕获来自 ServiceCharge() 的异常了

}

 

参考文献:

C++ Primer第三版

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值