C/C++ 异常( std::exception)
发布日期:2021-06-30 15:33:44 浏览次数:5 分类:技术文章

本文共 7417 字,大约阅读时间需要 24 分钟。

C++提供异常主要是为了分割库异常与调用者之间的分割

平常当我们开发一个lib库时,一般lib库里的函数发生异常都会通过返回值来告诉用户,但是这样的开发属实有点过于麻烦了

如:

int test(){    //发生错误1    return 1;    //发生错误2    return 2}

上面一个名为test的函数发生了异常,通过num数字来表示异常类型,这样的情况下调用者还要去翻阅手册去查查你这个返回到底是什么意思,在开发期间极其耗费时间,如果能够很直观的报出错误会很方便,但是在lib库里我们不能调用printf之类的来告诉用户,因为这不符合lib库的规则,但是可以打印到stderr里,前提是用户的开发环境是CUI。

当然如果用char*作为每个函数的返回值显然不合理,因为每个函数都返回char* 在开发层面上来说很不合理,因为用户可能从函数原型上来看误以为是返回char字符,从开发角度来说这很不合理。

因为正常人看到一个函数原型为char*会误以为你返回的是一个可处理的字符数组,而不是你在发生异常时返回错误字符,在正常时返回NULL。

C++推出异常的目的就是为了解决这一问题,用户可以通过异常捕获发生的异常类型,并根据异常类型打印出对应的错误信息。

先看一个简单的捕获异常写法

int main(){    try{        //可能发生异常的表达式    }catch(抛出异常类型){        //处理异常    }}

从上面可以看出来,异常表达式非常简单

1.在try里写下可能发生异常的表达式

2.紧接着使用catch捕获抛出的异常类型

3.在catch作用域里处理异常

抛出的类型可以是自定义的类型,也可以是c/c++基本类型

如:

int main(){    try{        int *a = (int*)malloc(10);        if(a == NULL){            throw("Out of memory");        }    }catch(const char* err){        printf("%s\n",err);    }}

运行时如果a在分配内存时mallo因为一些异常返回NULL,则会抛出异常

同时会被catch捕获,并处理对应的异常事件

这里注意,当调用throw时,程序会中断,跳转到catch捕获中,并终止程序的运行,也就是说,当抛出异常try作用域里的代码就不会继续执行了。

如:

int main(){    try{        int *a = (int*)malloc(10);        if(a == NULL){            throw("Out of memory");        }        printf("这里是try作用域内的");    }catch(const char* err){        printf("%s\n",err);    }    printf("这里是try作用域外的");}

打印:

Out of memory

这里是try作用域外的

可以看到try作用域内的并没有被执行

这里顺便说一句被花括号括起来的"{}",代表作用域

也许这样写你会看的更清楚,上面的写法是我个人的一些编程小习惯:

这样写就显得正规一点了,你会看的更仔细,更加容易理解

int main(){    try    {        int *a = (int*)malloc(10);        if(a == NULL){            throw("Out of memory");        }        printf("这里是try作用域内的");    }    catch(const char* err)    {        printf("%s\n",err);    }    printf("这里是try作用域外的");}

 

同时一个try异常列表里可以拥有多个catch

int main(){    try    {        int *a = (int*)malloc(10);        if(a == NULL){            throw("Out of memory");        }           }    catch(const char* err)    {        printf("%s\n",err);    }    catch(int err)    {        printf("%d\n",err);    }    }

c++异常会根据类型做比较,优先选择类型匹配的异常,此外捕获列表里允许重名变量。

可以从上面看到 char类型和int都为err,但是编译器不会报错,但是作用域里不允许重名变量

打印结果:

Out of memory

如果改为这样:

int main(){    try    {        int *a = (int*)malloc(10);        if(a == NULL){            throw(123);        }           }    catch(const char* err)    {        printf("%s\n",err);    }    catch(int err)    {        printf("%d\n",err);    }    }

打印结果:

123

如果你抛出的类型不在捕获范围内,那么C++会杀死程序,并且会报出错误提示

这里我需要说一下double与float的捕获规则

double与float都是浮点数,一个是双精度一个是单精度

它们只是表示的小数点后的位不同而已

但是平常情况下:

1.23 你能区分出它是double还是float吗?

所以当我们使用throw抛出1.23的时候,编译器如何知道是double还是float呢?

答:

float后面加上f

如:

1.23f 代表float,不加f代表double

1.23f float

1.23 double

用下面的代码来验证:

int main(){    try    {        int *a = (int*)malloc(10);        if(a == NULL){            throw(12.3);        }           }    catch(const char* err)    {        printf("%s\n",err);    }    catch(int err)    {        printf("%d\n",err);    }    catch(float a)    {        printf("this is float\n");    }    catch(double a)    {        printf("this is double\n");    }    }

打印:

this is double

改成f

try    {        int *a = (int*)malloc(10);        if(a == NULL){            throw(12.3f);        }           }

打印:

this is float

 

std::exception类

std::exception是标准c++里所有的异常类的基类,你可以通过继承这个类来实现你自己的异常类

标准c++里许多类都继承了这个类实现了自己的bad类

如:

C++ 异常的层次结构

可以看到标准c++提供了很多异常类,用于捕获使用标准c++类发生的错误

如:

使用new分配内存时,可以捕获bad_alloc

同时每个类里重写了std::exception虚函数what,用于打印发生的错误信息

下面是继承基类提供的错误类描述

1.std::bad_alloc 

当程序调用new分配内存时,内存不够时会抛出此异常

2.std::bad_cast

类型转换失败时抛出的异常,由dynamic_cast(将基类指针转为继承类指针类型)抛出

3.std::bad_typeid

当获取类型失败时会抛出此异常

4.std::bad_exception

一些未知的异常,可以通过此类抛出来

5.std::logic_error

逻辑错误类,可以通过继承此类来抛出一些逻辑的错误,用于自定义逻辑错误,当然也可以不继承它,这只是c++提供的标准

以下是c++继承std::logic_error实现的几个异常类

5.1 std::domain_error

自定义异常类型,用户可以通过抛出此异常来描述自己的自定义异常,抛出的类型只能是const char*,主要用于标识告诉调用者在主或核心函数里发生了异常,这是c++帮我们定义的异常类型

5.2 std::invalid_argument

无效参数类型,如果检测到无效参数,可以通过此异常抛出去

5.3 std::length_error

当长度超出规定值时可以通过此异常抛出

5.4 std::out_of_range

发生越界时可以通过此异常类型抛出

以上几个都是c++为我们提供的一些基本异常类型,我们也可以自定义

如当发生无效参数时:

int main(){    try    {        int *a = (int*)malloc(10);        if(a == NULL){            throw std::invalid_argument("无效参数");        }           }    catch(std::invalid_argument err)    {        cout << err.what() << endl;    }       }

6.std:runtime_error

运行时的异常类型,当运行时发生了异常,可以通过继承此类来抛出异常

以下是c++继承此类实现的几个标准类

6.1 std::overflow_error

当运行时进行数学计算时,输入的数据太长,超出了缓冲区,并填充到缓冲区上一级的内存空间里去时会抛出此异常

6.2 std::underflow_error

当运行时进行数学计算时,输入的数据太长,超出了缓冲区,并填充到缓冲区下一级的内存空间里去时会抛出此异常

以上两个简称上溢与下溢

6.3 std::range_error

计算时所产生的结果不符合预期值,或预期范围值时会抛出此异常

以上这些标准被很多数学库和其它的一些类库所采用,我们调用时可以根据函数行为来捕获对应的异常。

一个合格的c++程序员,理应在对应的场景下使用这些异常类,以符合通用标准。

 

使用std::exception实现自己的异常类

下面看一段代码

class zserr_null : public std::exception{        public:        const char* what() const throw(){                return "pointer passed in is null!";        }};

这里声明了一个zserr_null的类,并且继承std::exception异常类

并在类里重写exception的虚函数“what”

这里可以看到what后面跟了个const throw()

这个是c++的规范,用于告诉编译器这个函数可能会抛出异常

这是c++的一种规范,就是用来表示此函数专门用来处理异常方面的问题。

如果你的函数专门用来处理异常方面的,那么理应加上这个关键字,这让其它开发者们看到也很明确这个函数和普通函数的不同。

当然你也可以不加,然后同样去处理异常方面的问题,但是这样对于那些比较看重c++标准的人来说,会比较排斥你的这种写法,因为不符合c++规范,有时候他们可能理解不了你的写法,所以建议跟着标准来写。

写好了之后直接抛出来就可以了

int main(){        try{               zserr_null null;               throw null;        }catch(zserr_null& err){                cout << err.what() << endl;        }}

打印:

pointer passed in is null!

这里在提一下,如果你抛出变量,是不需要加"()"的,同时在抛出时不要让你的代码发生隐式转换或强制转换这种情况,因为最后匹配规则可能会错乱掉,如char最后因为隐式转换这种情况匹配到int上去了。

同时在提一下,在匹配规则里如果同时有int和short,无论怎样编译器都会优先选择int,这是因为int更符合cpu的寻址位宽对齐,如果你硬要它去short匹配规则里可以使用强转

有细心的同学肯定发现我用了&,引用描述符,因为这里建议大家使用引用,因为这样可以节省很多内存。

当然,我们也可以使用基类来接收你的异常类

int main(){        try{               zserr_null null;               throw null;        }catch(std::exception& err){                cout << err.what() << endl;        }}

打印:

pointer passed in is null!

注意这里如果你把引用去掉了,就会发生很神奇的事情:

打印居然不是我们的异常类,居然是std::exception,调用了父类的虚函数,并没有调用我们自己的实现

这是为什么呢?

答:因为引用即使是父类类型,引用子类一样是可行的,因为地址空间指向子类,你调用同样的方法编译器会在编译器期间将其编译为子类的地址空间,只是指向的类型不同而已

就像char*可以指向int*一样

但是显然这样不合理,也不符合规范,只是利用了编译器的某些特性,戏耍了编译器。

如果不用指针或者引用的话,那么编译器会实例化一个父类出来,不会实例化子类。

当你把引用去掉以后,它就是父类,你这样抛出异常就等于:

zserr_null null

std::exception err

err = null

显然父类并不支持这样的做法,所以这次的异常虽然被捕获了,但是并没有被正确赋值

但是 如果这样就是正确的:

能够被正确的赋值,因为这种做转换是安全的,c++编译器会实例化一个副本出来并将传递的结构体内存拷贝到副本中去

int main(){        try{               zserr_null null;               throw null;        }catch(zserr_null err){                cout << err.what() << endl;        }}

引用指向它是允许的,这是因为向上类型转换,有想了解的朋友可以参考我这篇文章:

以下我把我之前写的文章里对其解释的部分拿了过来

同时如果是虚函数的情况下,会先从子类的虚函数表里寻找地址,如果找到了则执行,没找到则到父类的内存空间里去寻找符号

同理,当遇到重名函数的情况下也会优先从子类的内存空间里去寻找符号,在到父类。

那么如何实现这样的效果呢:

通过构造函数来抛出,这样就很方便了,不需要在定义变量

throw std::invalid_argument("无效参数");

如果使用这个方法需要继承std::runtime_err或std::logic_err,因为这两个类自己实现了一个构造函数,我们可以通过继承它们来完成我们的实现:

class my : public logic_error {   public:      my(const char* sda) : logic_error(sda) {}};int main(){        try{               throw my("asdas");        }catch(my& err){                cout << err.what() << endl;        }}

结果:

asdas

当然我们也可以不继承任何类,自己去实现这些东西

class my{   public:      my(const char* value){	    _temp = value;      }      const char* what(){      	return _temp.data();      }   private:     string _temp; };int main(){        try{               throw my("test");        }catch(my& err){                cout << err.what() << endl;        }}

输出:

test

原理也非常简单

1.我们在my类里声明一个构造函数用于存储抛出异常时的消息

my(const char* value){	    _temp = value;      }

用string类型的变量存储起来,用于what函数输出

当我们调用throw时,c++编译器会帮我们实例化一个对象,并传递给捕获列表。

throw my("test");等价于my test("test");throw test;

 

转载地址:https://jrhar.blog.csdn.net/article/details/110231205 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Linux内核开发_将Linux内核打包成img文件
下一篇:C/C++_虚函数

发表评论

最新留言

表示我来过!
[***.240.166.169]2024年04月16日 07时14分54秒