C++异常相关

使用异常处理,程序中独立开发的各部分能够就程序执行期间出现的问题相互通信,并处理这些问题。C++ 的异常处理中,需要由问题检测部分抛出一个对象给处理代码,通过这个对象的类型和内容,两个部分能够就出现了什么错误进行通信。

 

一:概述

1:在C++中的异常处理语句包括:

try:try语句块以try关键字开始,并且以一个或者多个catch子句结束。try语句块中执行正常的代码,并且可以抛出异常,在try块后面catch子句捕获异常,并处理之。

throw表达式:用来抛出异常。

 

try块的通用语法形式是:

try
{
    program-statements
}
catch(exception-specifier)
{
    handler-statements;
}
catch(…)
{
    …
}

 

2:catch后的圆括号内是单个类型或者单个对象的声明,称为异常说明符,如果catch捕获了异常,则执行相关的块语句,一旦这个catch子语句执行结束,则程序流程跳转到紧跟最后一个catch子句的语句。

throw表达式的类型决定了抛出异常的类型。在抛出异常之后,如果不存在处理该异常的catch子句,则程序的运行就跳转到名为terminate的标准库函数,一般该函数的执行导致程序的非正常退出。而且,如果程序不是在try中抛出的异常,则也会执行terminate函数,比如:

    int a = 3;
    try
    {
        if(a == 3)
        {               
            throw 3.0;
        }
    }
    catch(int b)
    {
        cout << b << endl;
    }

代码中抛出的类型为double型,但是catch捕获int类型的异常,所以,程序打印:

terminate called after throwing an instance of 'double'
Aborted (core dumped)

如果改为throw 3的话,则输出“3”

 

         下面的代码:

    int a = 3;
    throw 3.0;

同样打印:

terminate called after throwing an instance of 'double'
Aborted (core dumped)

 

3:标准库异常类的继承关系如下:

 

exception 类型所定义的唯一操作是一个名为 what 的虚成员,该函数返回const char* 对象,它一般返回用来在抛出位置构造异常对象的信息。

应用程序还经常通过从exception 类或者中间基类派生附加类型来扩充 exception 层次。

 

二:异常的引发

1:异常是通过抛出对象而引发的。该对象的类型决定应该激活哪个处理代码。被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那个。

 

2:异常以类似于将实参传递给函数的方式抛出和捕获。

 

3:传递数组或函数类型实参的时候,该实参自动转换为一个指针。被抛出的异常对象将发生同样的自动转换。

 

4:执行 throw 的时候,不会执行跟在 throw 后面的语句,而是将控制从 throw 转移到匹配的 catch,该 catch 可以是同一函数中局部的 catch,也可以在直接或间接调用发生异常的函数的另一个函数中。控制从一个地方传到另一地方,这意味着:

A:沿着调用链的函数提早退出;

B:一般而言,在处理异常的时候,抛出异常的块中的局部存储不存在了。

 

5:因为在处理异常的时候会释放局部存储,所以被抛出的对象就不能再局部存储,而是用 throw 表达式初始化一个称为异常对象的特殊对象。异常对象由编译器管理,而且保证驻留在可能被激活的任意 catch 都可以访问的空间。这个对象由 throw 创建,并被初始化为被抛出的表达式的副本。异常对象将传给对应的 catch,并且在完全处理了异常之后撤销。

因此,异常对象通过复制被抛出表达式的结果创建,该结果必须是可以复制的类型。当抛出一个表达式的时候,被抛出对象的静态编译时类型将决定异常对象的类型。

 

6:如果在抛出中需要对指针解引用,则指针解引用的结果是一个对象,其类型与指针的类型匹配。如果指针指向继承层次中的一种类型,指针所指对象的类型就有可能与指针的类型不同。无论对象的实际类型是什么,异常对象的类型都与指针的静态类型相匹配。如果该指针是一个指向派生类对象的基类类型指针,则那个对象将被分割,只抛出基类部分。

如果抛出指针本身,可能会引发比分割对象更严重的问题。具体而言,抛出指向局部对象的指针总是错误的,其理由与从函数返回指向局部对象的指针是错误的一样。

 

7:抛出异常的时候,将暂停当前函数的执行,开始查找匹配的 catch 子句。首先检查 throw 本身是否在 try 块内部,如果是,检查与该 catch 相关的catch 子句,看是否其中之一与抛出对象相匹配。如果找到匹配的 catch,就处理异常;如果找不到,就退出当前函数(释放当前函数的内在并撤销局部对象),并且继续在调用函数中查找。

如果对抛出异常的函数的调用是在 try 块中,则检查与该 try 相关的catch 子句。如果找到匹配的 catch,就处理异常;如果找不到匹配的 catch,调用函数也退出,并且继续在调用这个函数的函数中查找。

这个过程,称之为栈展开(stack unwinding),沿嵌套函数调用链继续向上,直到为异常找到一个 catch 子句。只要找到能够处理异常的 catch 子句,就进入该 catch 子句,并在该处理代码中继续执行。当 catch 结束的时候,在紧接在与该 try 块相关的最后一个 catch 子句之后的点继续执行。

栈展开期间,提早退出包含 throw 的函数和调用链中可能的其他函数。一般而言,这些函数已经创建了可以在退出函数时撤销的局部对象。因异常而退出函数时,编译器保证适当地撤销局部对象:

class test
{
public:
    ~ test()
    {
        cout << "this is test destructor" << endl;
    }
};

void fun3()
{
test tt;
    throw runtime_error("hehehe");
    cout << "this is fun3" << endl;
}

void fun2()
{
    fun3();
    cout << "this is fun2" << endl;
}

void fun1()
{
   try
    {
        fun2();
    }
    catch(...)
    {
        cout << "get the exception" << endl;
    }
    cout << "this is fun1" << endl;
}

int main()
{
    fun1();
    cout << "this is main" << endl;
}

结果是:

this is test destructor
get the exception
this is fun1
this is main

 

如果一个块通过 new 动态分配内存,而且在释放资源之前发生异常,在栈展开期间将不会释放该资源。

在为某个异常进行栈展开的时候,如果析构函数又抛出自己的未经处理的另一个异常,将会导致调用标准库 terminate 函数。一般而言,terminate函数将调用 abort 函数,强制从整个程序非正常退出。

不能不处理异常,如果找不到匹配的 catch,程序就调用库函数 terminate。

 

三:捕获异常

1:catch 子句中的异常说明符看起来像只包含一个形参的形参表。

说明符的类型决定了处理代码能够捕获的异常种类。当 catch 为了处理异常只需要了解异常的类型的时候,异常说明符可以省略形参名;如果处理代码需要已发生异常的类型之外的信息,则异常说明符就包含形参名,catch 使用这个名字访问异常对象。

在查找匹配的 catch 期间,找到的 catch 不必是与异常最匹配的那个catch,相反,将选中第一个找到的可以处理该异常的 catch。因此,在 catch 子句列表中,最特殊的 catch 必须最先出现。

 

异常与 catch 异常说明符匹配的规则比匹配实参和形参类型的规则更严格,大多数转换都不允许——除下面几种可能的区别之外,异常的类型与 catch 说明符的类型必须完全匹配:

• 允许从非 const 到 const 的转换。也就是说,非 const 对象的 throw可以与指定接受 const 引用的 catch 匹配。

• 允许从派生类型型到基类类型的转换。

• 将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针。

在查找匹配 catch 的时候,不允许其他转换。具体而言,既不允许标准算术转换,也不允许为类类型定义的转换。比如:

    short a = 3;
    try
    {
        throw a;
    }
    catch(int a)
    {
        cout << "catch int: " << a << endl;
    }
    catch(double a)
    {
        cout << "catch double: " << a << endl;
    }
    catch(short a)
    {
        cout << "catch short: " << a << endl;
    }

结果是:

catch short: 3

 

2:进入 catch 的时候,用异常对象初始化 catch 的形参。

如果异常说明符不是引用,就将异常对象复制到 catch 形参中,对形参所做的任何改变都只作用于副本,不会作用于异常对象本身。如果说明符是引用,则像引用形参一样,对 catch 形参所做的改变作用于异常对象。

 

像形参声明一样,基类的异常说明符可以用于捕获派生类型的异常对象,而且,异常说明符的静态类型决定 catch 子句可以执行的动作。如果被抛出的异常对象是派生类类型的,但由接受基类类型的 catch 处理,那么,catch 不能使用派生类特有的任何成员。

如果 catch 形参是引用类型,catch 对象就直接访问异常对象,catch 对象的静态类型可以与 catch 对象所引用的异常对象的动态类型不同。如果异常说明符不是引用,则 catch 对象是异常对象的副本,如果 catch 对象是基类类型对象而异常对象是派生类型的,就将异常对象分割为它的基类子对象。

 

3:有可能单个 catch 不能完全处理一个异常。在进行了一些校正行动之后,catch 可以通过重新抛出将异常传递函数调用链中更上层的函数。重新抛出是后面不跟类型或表达式的一个 throw;

空 throw 语句将重新抛出异常对象,它只能出现在 catch 或者从 catch调用的函数中。如果在处理代码不活动时碰到空 throw,就调用 terminate 函数。

重新抛出不指定自己的异常,仍然将异常对象沿链向上传递。被抛出的异常是原来的异常对象,而不是 catch 形参。当 catch 形参是基类类型的时候,我们不知道由重新抛出表达式抛出的实际类型,该类型取决于异常对象的动态类型,而不是 catch 形参的静态类型。例如,来自带基类类型形参 catch的重新抛出,可能实际抛出一个派生类型的对象。

一般而言,catch 可以改变它的形参。在改变它的形参之后,如果 catch 重新抛出异常,那么,只有当异常说明符是引用的时候,才会传播那些改变。

catch (my_error &eObj) { // specifier is a reference type
    eObj.status = severeErr; // modifies the exception object
    throw; // the status member of the exception object is severeErr
} 
catch (other_error eObj) { // specifier is a nonreference type
    eObj.status = badErr; // modifies local copy only
    throw; // the status member of the exception rethrown is unchanged
}

 

4:捕获所有异常的 catch 子句形式如下:

// matches any exception that might be thrown
catch (...) {
    // place our code here
}

捕获所有异常的 catch 子句与任意类型的异常都匹配。

 

catch(...) 经常与重新抛出表达式结合使用,catch 完成可做的所有局部工作,然后重新抛出异常:

void manip() 
{
    try {
        // actions that cause an exception to be thrown
    }

    catch (...) {
        // work to partially handle the exception
        throw;
    }
}

 

5:构造函数函数体内部的 catch 子句不能处理在构造函数初始化时发生的异常。为了处理这种异常,必须将构造函数编写为:函数 try 块。形式如下:

template <class T> Handle<T>::Handle(T *p)
try : ptr(p), use(new size_t(1))
{
    // empty function body
} 
catch(const std::bad_alloc &e)
{ handle_out_of_memory(e); }

关键字 try 出现在成员初始化列表之前,并且测试块的复合语句包围了构造函数的函数体。catch 子句既可以处理从成员初始化列表中抛出的异常,也可以处理从构造函数函数体中抛出的异常。

 

四:资源分配即初始化

1:因为异常的发生,可能导致使用new申请的内存无法得到释放:

void f()
{
    vector<string> v; // local vector
    string s;
    while (cin >> s)
    v.push_back(s); // populate the vector
    string *p = new string[v.size()]; // dynamic array
    // remaining processing
    // it is possible that an exception occurs in this code
    // function cleanup is bypassed if an exception occurs
    delete [] p;
} // v destroyed automatically when the function exits

这个函数定义了一个局部 vector 并动态分配了一个数组。在正常执行的情况下,数组和 vector 都在退出函数之前被撤销,函数中最后一个语句释放数组,在函数结束时自动撤销 vector。

不管何时发生异常,都保证运行 vector 析构函数。但是,如果在 new 之后但在 delete 之前发生了异常,则使得数组无法被撤销。

 

2:通过定义一个类来封闭资源的分配和释放,可以保证正确释放资源。这一技术常称为“资源分配即初始化”,简称 RAII。

应该设计资源管理类,以便构造函数分配资源而析构函数释放资源。想要分配资源的时候,就定义该类类型的对象。如果不发生异常,就在获得资源的对象超出作用域的进修释放资源。更为重要的是,如果在创建了对象之后但在它超出作用域之前发生异常,那么,编译器保证撤销该对象。

 

3:标准库的 auto_ptr 类是“资源分配即初始化”技术的例子。auto_ptr 类是接受一个类型形参的模板,auto_ptr 类在头文件 memory 中定义。

每个 auto_ptr 对象指向一个对象。当 auto_ptr 对象指向一个对象的时候,可以说它“拥有”该对象。当 auto_ptr 对象超出作用域或者另外撤销的时候,就自动回收 auto_ptr 所指向的动态分配对象。

auto_ptr 只能用于管理从 new 返回的一个对象,它不能管理动态分配的数组。auto_ptr 被复制或赋值的时候,有不寻常的行为,因此,不能将 auto_ptrs 存储在标准库容器类型中。

 

使用常规指针的形式如下:

void f()
{
    int *ip = new int(42); // dynamically allocate a new object
    // code that throws an exception that is not caught inside f
    delete ip; // return the memory before exiting
}

如果使用auto_ptr类,则形式如下:

void f()
{
    auto_ptr<int> ap(new int(42)); // allocate a new object
    // code that throws an exception that is not caught inside f
}// auto_ptr freed automatically when function ends

编译器保证在展开栈越过 f 之前运行 ap 的析构函数。

 

希望访问 string 操作。用普通 string 指针,像下面这样做:

string *pstr_type = new string("Brontosaurus");
if (pstr_type->empty())
    // oops, something wrong

auto_ptr 类定义了解引用操作符(*)和箭头操作符(->)的重载版本,所以可以用类似于使用内置指针的方式使用 auto_ptr 对象:

auto_ptr<string> ap1(new string("Brontosaurus"));
// normal pointer operations for dereference and arrow
*ap1 = "TRex"; // assigns a new value to the object to which ap1 points
string s = *ap1; // initializes s as a copy of the object to which ap1 points
if (ap1->empty()) 
    // runs empty on the string to which ap1 points

auto_ptr 的主要目的,在保证自动删除 auto_ptr 对象引用的对象的同时,支持普通指针式行为。

 

如果不给定初始式,auto_ptr 对象是未绑定的,它不指向对象。对未绑定的 auto_ptr 对象解引用,其效果与对未绑定的指针解引用相同——程序出错。

 

为了检查指针是否未绑定,可以在条件中直接测试指针,效果是确定指针是否为 0。相反,不能直接测试 auto_ptr 对象:

// error: cannot use an auto_ptr as a condition
if (p_auto)
    *p_auto = 1024;

auto_ptr 类型没有定义到可用作条件的类型的转换,相反,要测试auto_ptr 对象,必须使用它的 get 成员,该成员返回包含在 auto_ptr 对象中的基础指针:

// revised test to guarantee p_auto refers to an object
if (p_auto.get())
    *p_auto = 1024;

应该只用 get 询问 auto_ptr 对象或者使用返回的指针值,不能用 get 作为创建其他 auto_ptr 对象的实参。使用 get 成员初始化其他 auto_ptr 对象违反 auto_ptr 类设计原则。

 

auto_ptr 对象与内置指针的另一个区别是,不能直接将一个地址(或者其他指针)赋给 auto_ptr 对象:

p_auto = new int(1024); // error: cannot assign a pointer to an auto_ptr

相反,必须调用 reset 函数来改变指针:

// revised test to guarantee p_auto refers to an object
if (p_auto.get())
    *p_auto = 1024;
else
    // reset p_auto to a new object
    p_auto.reset(new int(1024));

调用 auto_ptr 对象的 reset 函数时,在将 auto_ptr 对象绑定到其他对象之前,会删除 auto_ptr 对象所指向的对象(如果存在)。但是,正如自身赋值是没有效果的一样,如果调用该 auto_ptr 对象已经保存的同一指针的 reset 函数,也没有效果,不会删除对象。

 

auto_ptr 和内置指针对待复制和赋值有非常关键的重要区别。当复制 auto_ptr 对象或者将它的值赋给其他 auto_ptr 对象的时候,将基础对象的所有权从原来的 auto_ptr 对象转给副本,原来的 auto_ptr 对象重置为未绑定状态。

在复制(或者赋值)auto_ptrs 对象之后,原来的 auto_ptr 对象不指向对象而新的 auto_ptr(左边的 auto_ptr 对象)拥有基础对象:

auto_ptr<string> ap1(new string("Stegosaurus"));
// after the copy ap1 is unbound
auto_ptr<string> ap2(ap1); // ownership transferred from ap1 to ap2

当复制 auto_ptr 对象或者对 auto_ptr 对象赋值的时候,右边的auto_ptr 对象让出对基础对象的所有职责并重置为未绑定的 auto_ptr 对象之后,在上例中,删除 string 对象的是 ap2 而不是 ap1,在复制之后,ap1 不再指向任何对象。

class test
{
public:
    test(int i = 0):index(i) {}
    ~ test()
    {
        printf("this is test[%d] destructor
", index);
    }

private:
    int index;
};

int main()
{
    auto_ptr<test> ptr1;
    cout << "ptr1.get() is " << ptr1.get() << endl;

    ptr1.reset(new test(1));
    ptr1.reset(new test(0));
    cout << "ptr1.get() is " << ptr1.get() << endl;
    auto_ptr<test> ptr2(ptr1);
    cout << "ptr1.get() is " << ptr1.get() << endl;
    cout << "ptr2.get() is " << ptr2.get() << endl;
}

结果如下:

ptr1.get() is 0
this is test[1] destructor
ptr1.get() is 0xd79030
ptr1.get() is 0
ptr2.get() is 0xd79030
this is test[0] destructor

 

除了将所有权从右操作数转给左操作数之外,赋值还删除左操作数原来指向的对象——假如两个对象不同。通常自身赋值没有效果。

    auto_ptr<test> ptr1(new test(0));
    auto_ptr<test> ptr2(new test(1));
    cout << "ptr1.get() is " << ptr1.get() << endl;
    cout << "ptr2.get() is " << ptr2.get() << endl;

    ptr1 = ptr2;
    cout << "ptr1.get() is " << ptr1.get() << endl;
    cout << "ptr2.get() is " << ptr2.get() << endl;

结果是:

ptr1.get() is 0x2185010
ptr2.get() is 0x2185030
this is test[0] destructor
ptr1.get() is 0x2185030
ptr2.get() is 0
this is test[1] destructor

将 ptr2 赋给 ptr1 之后:删除了 ptr1 指向的对象;将 ptr1置为指向 ptr2 所指的对象;ptr2 是未绑定的 auto_ptr 对象。

 

注意:

a. 不要使用 auto_ptr 对象保存指向静态分配对象的指针;

b. 永远不要使用两个 auto_ptrs 对象指向同一对象,导致这个错误的一种明显方式是,使用同一指针来初始化或者 reset 两个不同的 auto_ptr 对象。另一种导致这个错误的微妙方式可能是,使用一个 auto_ptr 对象的 get 函数的结果来初始化或者 reset另一个 auto_ptr 对象。

c. 不要使用 auto_ptr 对象保存指向动态分配数组的指针。当auto_ptr 对象被删除的时候,它使用普通delete 操作符,而不用数组的 delete [] 操作符。

d. 不要将 auto_ptr 对象存储在容器中。容器要求所保存的类型在复制(或者赋值)之后,两个对象必须具有相同值,auto_ptr 类不满足这个要求。

 

五:异常说明

1:查看普通函数声明的时候,不可能确定该函数会抛出什么异常,但是,为了编写适当的 catch 子句,了解函数是否抛出异常以及会抛出哪种异常是很有用的。

异常说明指定,如果函数抛出异常,被抛出的异常将是包含在该说明中的一种,或者是从列出的异常中派生的类型。

异常说明跟在函数形参表之后。一个异常说明在关键字 throw 之后跟着一个(可能为空的)由圆括号括住的异常类型列表:

void recoup(int) throw(runtime_error);

空说明列表指出函数不抛出任何异常:

void no_problem() throw();

如果一个函数声明没有指定异常说明,则该函数可以抛出任意类型的异常。

 

异常说明是函数接口的一部分,函数定义以及该函数的任意声明必须具有相同的异常说明。

如果函数抛出了没有在其异常说明中列出的异常,就调用标准库函数unexpected。默认情况下,unexpected 函数调用 terminate 函数,terminate 函数一般会终止程序。

 

像非成员函数一样,成员函数声明的异常说明跟在函数形参表之后。

class bad_alloc : public exception {
public:
    ...
    virtual const char* what() const throw();
};

在 const 成员函数声明中,异常说明跟在 const 限定符之后

 

基类中虚函数的异常说明,可以与派生类中对应虚函数的异常说明不同。但是,派生类虚函数的异常说明必须与对应基类虚函数的异常说明同样严格,或者比后者更严格。所谓严格就是指,如果A会抛出a、b、c三种异常,而B会抛出a、b两种异常,则B就比A严格。

这个限制保证,当使用指向基类类型的指针调用派生类虚函数的时候,派生类的异常说明不会增加新的可抛出异常。例如:

class Base {
public:
    virtual double f1(double) throw ();
    virtual int f2(int) throw (std::logic_error);
    virtual std::string f3() throw (std::logic_error, std::runtime_error);
};

class Derived : public Base {
public:
    // error: exception specification is less restrictive than Base::f1's
    double f1(double) throw (std::underflow_error);
    // ok: same exception specification as Base::f2
    int f2(int) throw (std::logic_error);
    // ok: Derived f3 is more restrictive
    std::string f3() throw ();
};

派生类中 f1 的声明是错误的,因为它的异常说明在基类 f1 版本列出的异常中增加了一个异常。派生类不能在异常说明列表中增加异常,通过派生类抛出的异常限制为由基类所列出的那些,在编写代码时就可以知道必须处理哪些异常。

 

异常说明是函数类型的一部分。这样,也可以在函数指针的定义中提供异常说明:

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

在用另一指针初始化带异常说明的函数的指针,或者将后者赋值给函数地址的时候,两个指针的异常说明不必相同,但是,源指针的异常说明必须至少与目标指针的一样严格。

void recoup(int) throw(runtime_error);

// ok: recoup is as restrictive as pf1
void (*pf1)(int) throw(runtime_error) = recoup;
// ok: recoup is more restrictive than pf2
void (*pf2)(int) throw(runtime_error, logic_error) = recoup;
// error: recoup is less restrictive than pf3
void (*pf3)(int) throw() = recoup;
// ok: recoup is more restrictive than pf4
void (*pf4)(int) = recoup;

第三个初始化是错误的。指针声明指出,pf3 指向不抛出任何异常的函数,但是recoup 函数指出它能抛出 runtime_error 类型的异常,recoup 函数抛出的异常类型超出了 pf3 所指定的,对 pf3 而言,recoup 函数不是有效的初始化式,并且会引发一个编译时错误。

原文地址:https://www.cnblogs.com/gqtcgq/p/7230971.html