C++中的 throw详解

简介: 在 C++ 中,异常处理遵循"抛出-检测-捕获"的流程。异常必须通过 `throw` 显式抛出,例如 `throw 100;` 可以抛出整数类型的异常。动态数组可以通过自定义的 `Array` 类实现,当访问越界或空数组时,会抛出 `OutOfRange` 异常。`throw` 关键字也可用于函数异常规范,指定函数可能抛出的异常类型。然而,C++11 后不再推荐使用异常规范,因为它们可能导致编译器行为不一致。

在《C++异常处理》一节中,我们讲到了 C++ 异常处理的流程,具体为:

抛出(Throw)--> 检测(Try) --> 捕获(Catch)

异常必须显式地抛出,才能被检测和捕获到;如果没有显式的抛出,即使有异常也检测不到。在 C++ 中,我们使用 throw 关键字来显式地抛出异常,它的用法为:

throw exceptionData;

exceptionData 是“异常数据”的意思,它可以包含任意的信息,完全有程序员决定。exceptionData 可以是 int、float、bool 等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型,请看下面的例子:

char str[] = "http://c.biancheng.net";
    char *pstr = str;
    class Base{};
    Base obj;
    throw 100;  //int 类型
    throw str;  //数组类型
    throw pstr;  //指针类型
    throw obj;  //对象类型

一个动态数组的例子

C/C++ 规定,数组一旦定义后,它的长度就不能改变了;换句话说,数组容量不能动态地增大或者减小。这样的数组称为静态数组(Static array)。静态数组有时候会给编码代码不便,我们可以通过自定义的 Array 类来实现动态数组(Dynamic array)。所谓动态数组,是指数组容量能够在使用的过程中随时增大或减小。

下面这段代码虽然有点长,但它是一个典型的使用异常的场景,请大家耐心阅读。

#include <iostream>
    #include <cstdlib>
    using namespace std;
    //自定义的异常类型
    class OutOfRange{
    public:
        OutOfRange(): m_flag(1){ };
        OutOfRange(int len, int index): m_len(len), m_index(index), m_flag(2){ }
    public:
        void what() const;  //获取具体的错误信息
    private:
        int m_flag;  //不同的flag表示不同的错误
        int m_len;  //当前数组的长度
        int m_index;  //当前使用的数组下标
    };
    void OutOfRange::what() const {
        if(m_flag == 1){
            cout<<"Error: empty array, no elements to pop."<<endl;
        }else if(m_flag == 2){
            cout<<"Error: out of range( array length "<<m_len<<", access index "<<m_index<<" )"<<endl;
        }else{
            cout<<"Unknown exception."<<endl;
        }
    }
    //实现动态数组
    class Array{
    public:
        Array();
        ~Array(){ free(m_p); };
    public:
        int operator[](int i) const;  //获取数组元素
        int push(int ele);  //在末尾插入数组元素
        int pop();  //在末尾删除数组元素
        int length() const{ return m_len; };  //获取数组长度
    private:
        int m_len;  //数组长度
        int m_capacity;  //当前的内存能容纳多少个元素
        int *m_p;  //内存指针
    private:
        static const int m_stepSize = 50;  //每次扩容的步长
    };
    Array::Array(){
        m_p = (int*)malloc( sizeof(int) * m_stepSize );
        m_capacity = m_stepSize;
        m_len = 0;
    }
    int Array::operator[](int index) const {
        if( index<0 || index>=m_len ){  //判断是否越界
            throw OutOfRange(m_len, index);  //抛出异常(创建一个匿名对象)
        }
        return *(m_p + index);
    }
    int Array::push(int ele){
        if(m_len >= m_capacity){  //如果容量不足就扩容
            m_capacity += m_stepSize;
            m_p = (int*)realloc( m_p, sizeof(int) * m_capacity );  //扩容
        }
        *(m_p + m_len) = ele;
        m_len++;
        return m_len-1;
    }
    int Array::pop(){
        if(m_len == 0){
             throw OutOfRange();  //抛出异常(创建一个匿名对象)
        }
        m_len--;
        return *(m_p + m_len);
    }
    //打印数组元素
    void printArray(Array &arr){
        int len = arr.length();
        //判断数组是否为空
        if(len == 0){
            cout<<"Empty array! No elements to print."<<endl;
            return;
        }
        for(int i=0; i<len; i++){
            if(i == len-1){
                cout<<arr[i]<<endl;
            }else{
                cout<<arr[i]<<", ";
            }
        }
    }
    int main(){
        Array nums;
        //向数组中添加十个元素
        for(int i=0; i<10; i++){
            nums.push(i);
        }
        printArray(nums);
        //尝试访问第20个元素
        try{
            cout<<nums[20]<<endl;
        }catch(OutOfRange &e){
            e.what();
        }
        //尝试弹出20个元素
        try{
            for(int i=0; i<20; i++){
                nums.pop();
            }
        }catch(OutOfRange &e){
            e.what();
        }
        printArray(nums);
        return 0;
    }

运行结果:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9

Error: out of range( array length 10, access index 20 )

Error: empty array, no elements to pop.

Empty array! No elements to print.

Array 类实现了动态数组,它的主要思路是:在创建对象时预先分配出一定长度的内存(通过 malloc() 分配),内存不够用时就再扩展内存(通过 realloc() 重新分配)。Array 数组只能在尾部一个一个地插入(通过 push() 插入)或删除(通过 pop() 删除)元素。我们通过重载过的[ ]运算符来访问数组元素,如果下标过小或过大,就会抛出异常(第53行代码);在抛出异常的同时,我们还记录了当前数组的长度和要访问的下标。在使用 pop() 删除数组元素时,如果当前数组为空,也会抛出错误。

throw 用作异常规范

throw 关键字除了可以用在函数体中抛出异常,还可以用在函数头和函数体之间,指明当前函数能够抛出的异常类型,这称为异常规范(Exception specification),有些教程也称为异常指示符异常列表。请看下面的例子:

double func (char param) throw (int);

这条语句声明了一个名为 func 的函数,它的返回值类型为 double,有一个 char 类型的参数,并且只能抛出 int 类型的异常。如果抛出其他类型的异常,try 将无法捕获,只能终止程序。如果函数会抛出多种类型的异常,那么可以用逗号隔开:

double func (char param) throw (int, char, exception);

如果函数不会抛出任何异常,那么( )中什么也不写:

double func (char param) throw ();

如此,func() 函数就不能抛出任何类型的异常了,即使抛出了,try 也检测不到。

1) 虚函数中的异常规范

C++ 规定,派生类虚函数的异常规范必须与基类虚函数的异常规范一样严格,或者更严格。只有这样,当通过基类指针(或者引用)调用派生类虚函数时,才能保证不违背基类成员函数的异常规范。请看下面的例子:

class Base{
    public:
        virtual int fun1(int) throw();
        virtual int fun2(int) throw(int);
        virtual string fun3() throw(int, string);
    };
    class Derived:public Base{
    public:
        int fun1(int) throw(int);   //错!异常规范不如 throw() 严格
        int fun2(int) throw(int);   //对!有相同的异常规范
        string fun3() throw(string);  //对!异常规范比 throw(int,string) 更严格
    }

2) 异常规范与函数定义和函数声明

C++ 规定,异常规范在函数声明和函数定义中必须同时指明,并且要严格保持一致,不能更加严格或者更加宽松。

请看下面的几组函数:

//错!定义中有异常规范,声明中没有
    void func1();
    void func1() throw(int) { }
    //错!定义和声明中的异常规范不一致
    void func2() throw(int);
    void func2() throw(int, bool) { }
    //对!定义和声明中的异常规范严格一致
    void func3() throw(float, char*);
    void func3() throw(float, char*) { }

请抛弃异常规范,不要再使用它

异常规范的初衷是好的,它希望让程序员看到函数的定义或声明后,立马就知道该函数会抛出什么类型的异常,这样程序员就可以使用 try-catch 来捕获了。如果没有异常规范,程序员必须阅读函数源码才能知道函数会抛出什么异常。

不过这有时候也不容易做到。例如,func_outer() 函数可能不会引发异常,但它调用了另外一个函数 func_inner(),这个函数可能会引发异常。再如,您编写的函数调用了老式的库函数,此时不会引发异常,但是库更新以后这个函数却引发了异常。总之,异常规范的初衷实现起来有点困难,所以大家达成的一致意见是,最好不要使用异常规范。

异常规范是 C++98 新增的一项功能,但是后来的 C++11 已经将它抛弃了,不再建议使用。

另外,各个编译器对异常规范的支持也不一样,请看下面的代码:

#include <iostream>
    #include <string>
    #include <exception>
    using namespace std;
    void func()throw(char*, exception){
        throw 100;
        cout<<"[1]This statement will not be executed."<<endl;
    }
    int main(){
        try{
            func();
        }catch(int){
            cout<<"Exception type: int"<<endl;
        }
        return 0;
    }

在 GCC 下,这段代码运行到第 7 行时程序会崩溃。虽然 func() 函数中发生了异常,但是由于 throw 限制了函数只能抛出 char*、exception 类型的异常,所以 try-catch 将捕获不到异常,只能交给系统处理,终止程序。在 Visual C++ 下,输出结果为Exception type: int,这说明异常被成功捕获了。

在 Visual C++ 中使用异常规范虽然没有语法错误,但是也没有任何效果,Visual C++ 会直接忽略异常规范的限制,函数可以抛出任何类型的异常。

相关文章
|
程序员 编译器 C语言
C++中的 throw详解
在《C++异常处理》一节中,我们讲到了 C++ 异常处理的流程,具体为: 抛出(Throw)--> 检测(Try) --> 捕获(Catch) 异常必须显式地抛出,才能被检测和捕获到;如果没有显式的抛出,即使有异常也检测不到。在 C++ 中,我们使用 throw 关键字来显式地抛出异常,它的用法为: throw exceptionData; exceptionData 是“异常数据”的意思,它可以包含任意的信息,完全有程序员决定。exceptionData 可以是 int、float、bool 等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型,请看下面的例子: c
119 0
|
7月前
|
C++
C++异常处理try和throw以及catch的使用
C++异常处理try和throw以及catch的使用
|
29天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
50 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
103 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
90 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
108 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
32 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
32 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
29 1