目录
写在前面
-
拖更了几天,但是其实博主一直都在学习的,只是没有之前学习时间那么长,在解决完博主的汇报之后马上会重新投入cpp的学习中,这一周准备把cpp基础学完整理完然后下周正式开始数据结构的学习,争取日更,望大家共同监督共同进步,共勉之。
-
今天写的内容是三天前学习的内容,关于cpp中的异常处理机制,包含了基本处理,一些已经废弃的写法,异常类型的生命周期,异常抛出的性能优化,一个简单的项目实现以及标准库中提供的异常类的使用方法。异常处理还是很重要的,即使是短时间内看了第二遍还是收获了很多新的东西。
-
最近弄了一台新的笔记本,还没有改成linux,听说笔记本改linux很多坑,之前帮同学装过一台r9000p,运行还可以,之后我有空在来跳一跳这个坑,先用这个win弄一弄,还有就是在Windows上面写代码时发现的clion内存占用过多的问题,我直接贴一个回答吧,就不自己写了:缓解CLion因内存不足卡顿的问题 - 简书
为什么需要异常处理机制?
-
函数是一种以栈结构展开的系统,函数的上下衔接通过返回值逐层传递且不可以跳跃。
-
异常是另外一种程序控制的机制,可以在出现意外时中断当前的函数,并以某种机制将当前的异常信息回馈给隔代的调用者。
-
在c语言中,人们常用返回值来实现异常的反馈。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#define BUFFSIZE 1024
int copyfile(char* dest, char*src){
FILE *fp1 = NULL;
FILE *fp2 = NULL;
//rb: open a binary file using read-only authority
fopen_s(&fp1, dest, "rb");
if(fp1 == NULL){
//if failed in open file return -1
return -1;
}
//wb: open a binary file using write-only authority
fopen_s(&fp2, src, "wb");
if(fp2==NULL){
//if failed in open file return -2
return -2;
}
char buffer[BUFFSIZE];
int readlen, writelen;
//fread() returns the length of reading contents
while((readlen = fread(buffer, 1, BUFFSIZE, fp1)) > 0){
writelen = fwrite(buffer, 1, readlen, fp2);
if(writelen!=readlen){
//if the reading contents is not same length as that of writing, return -3
return -3;
}
}
fclose(fp1);
fclose(fp2);
//if successful, return 0
return 0;
}
int main() {
int ret = 0;
ret = copyfile("dest.txt", "src.txt");
if(ret!=0){
switch (ret) {
case -1:
std::cout << "failed in open source file!" << std::endl;
break;
case -2:
std::cout << "failed in open the target file" << std::endl;
break;
case -3:
std::cout << "failed in copy contents" << std::endl;
break;
default:
std::cout << "unknown error" << std::endl;
break;
}
}
return 0;
}
异常处理机制的方法:
-
使用关键字
throw
在处理异常的程序进行判断和抛出异常。 -
可以通过不同的数据类型来判断不同的异常类型(支持自定义类型如类和结构体类型),且数据类型不支持隐式类型转换。
-
关键字
try
和catch
在调用目标函数时将错误抛出,其中try
语句包含的内容叫做保护段将可能出现异常的代码置于此处。 -
在保护段中抛出异常的代码后面的部分将不会被执行,即抛出异常后,异常处理将会立即中断保护段中的代码,并运行catch中的语句(如果被成功捕捉)。
-
如果异常被正常接收则之后的程序也会正常地运行,如果异常没有被接收,就会进入默认处理方式调用
abort()
,此时程序会终止。 -
为了防止程序被终止一般会使用通配符
...
。 -
如果遇到当前无法处理的异常,可以将其再次抛出,交由下面一层异常处理函数对其处理,一般此操作在catch的最后一个分支中进行。
-
一个简单的例子(有两种方式类型分别被测试即(int类型和string*类型)注意抛出一个堆内存开辟的异常在异常处理时要记得回收内存,示例代码如下:
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <string>
#define BUFFSIZE 1024
int copyfile(char* dest, char*src){
FILE *fp1 = NULL;
FILE *fp2 = NULL;
//rb: open a binary file using read-only authority
fopen_s(&fp1, dest, "rb");
if(fp1 == NULL){
//if failed in open file return -1
//throw -1;
throw new std::string("there is no target file!");
}
//wb: open a binary file using write-only authority
fopen_s(&fp2, src, "wb");
if(fp2==NULL){
//if failed in open file return -2
throw -2;
}
char buffer[BUFFSIZE];
int readlen, writelen;
//fread() returns the length of reading contents
while((readlen = fread(buffer, 1, BUFFSIZE, fp1)) > 0){
writelen = fwrite(buffer, 1, readlen, fp2);
if(writelen!=readlen){
//if the reading contents is not same length as that of writing, return -3
throw -3;
}
}
fclose(fp1);
fclose(fp2);
//if successful, return 0
return 0;
}
int main() {
int ret = 0;
try{
ret = copyfile("dest.txt", "src.txt");
}catch(int e){
std::cout << "there is an error " << e << std::endl;
}catch(const std::string *e){
std::cout << "there is an error " << *e << std::endl;
delete e;
}catch(float e){
std::cout << "there is an error " << *e << std::endl;
}catch(...){
std::cout << "unknown error " << std::endl;
//if facing the error that cannot be processed in current location, keep throwing the catched will be okay, and it will be caught and processed in next catch operation
//throw;
}
std::cout << "program safely running" << std::endl;
return 0;
}
-
第一个测试结果(int类型抛出)如下
D:\ClionProject\exception\cmake-build-debug\exception.exe
there is an error -1
program safely running
Process finished with exit code 0
-
第一个测试结果(int类型抛出)如下
D:\ClionProject\exception\cmake-build-debug\exception.exe
there is an error there is no target file!
program safely running
Process finished with exit code 0
异常接口的声明
-
首先这个异常接口现在是出于一种废弃的状态,从c++17之后就不再支持这种书写方法了。
-
如果一个函数禁止抛出任何异常,则可以使用
throw()
关键字来声明(这是唯一。一个还可以用的方法,但是目前一般使用noexception)。
异常类型的生命周期以及不同类型异常的测试案例
-
异常类型会直接跳过栈的限制直接返回至第一个捕捉到他的catch语句中。
-
在异常之前的局部变量在异常抛出时会正常的被销毁,只有被抛出的类型生命周期有所不同。
-
抛出普通数据类型类似于返回值,但是由超出了返回值的概念,给一个简单的例子看看吧
-
#include <iostream> void func_test(){ throw(1); }; void throw_test(){ func_test(); } int main() { try{ throw_test(); }catch (int e){ switch(e){ case 1: std::cout << "succeed in catching the basic exception: " << e << std::endl; break; } } return 0; }
-
其输出结果如下,可以看出异常的抛出可以将一个临时变量直接抛出他的作用域,而且可以跨出几个函数的作用域,这这里从func_test >> throw_test >> main:
-
D:\ClionProject\exception\cmake-build-debug\exception.exe succeed in catching the basic exception: 1 Process finished with exit code 0
-
-
第二种类型,字符串,直接返回一个字符串不可以被string 类型的catch捕捉到,如果抛出一段字符串实际上抛出的是一个常字符串的首地址,因此需要用const char*形式的catch接收,而不是string。
-
具体看一个例子,其中包含了集中关于字符串类型的测试, 为了方便起见定义了一个函数指针,方便对各个测试代码进行调用。
-
注意我在这里delete掉string*是因为我知道我在这里开辟了一段堆的空间。代码 如下:
-
void test_4_string_throw(){ throw "there is an error type string"; } void test_4_char_throw(){ char* ret = "there is an error type string"; throw ret; } void test_4_string_pointer_throw(){ throw new std::string("there is an error type string"); } typedef void (*functype)(); void test_4_string_catch(functype fun){ try{ fun(); }catch (std::string& e){ std::cout << "succeed in catching the string exception: " << e << std::endl; }catch (std::string* e){ std::cout << "succeed in catching the string pointer exception: " << *e << std::endl; delete e; }catch (char *e){ std::cout << "succeed in catching the char* exception: " << e << std::endl; }catch (const char *e){ std::cout << "succeed in catching the const char* exception: " << e << std::endl; } } int main() { test_4_string_catch(test_4_string_throw); test_4_string_catch(test_4_char_throw); test_4_string_catch(test_4_string_pointer_throw); return 0; }
-
其运行结果如下,可以看出,字符串被const char*捕捉,char* 和string*类型被各自类型捕捉了,z再次可以看出什么叫做严格捕捉,即使是const 以及char*和 string 类型这种我们常见的隐式类型转换也是绝对不允许的:
-
D:\ClionProject\exception\cmake-build-debug\exception.exe succeed in catching the const char* exception: there is an error type string succeed in catching the char* exception: there is an error type string succeed in catching the string pointer exception: there is an error type string Process finished with exit code 0
-
-
抛出自定义类型的异常:
-
简单定义一个MyException的类型
-
分别定义其构造析构以及一个拷贝构造。
-
定义一个成员变量id_,并定义一个public的成员函数用于获取id值。
-
在构造函数中初始化id_为0而拷贝构造函数中初始其为1,由此来分辨在这个测试中的生命周期。
-
再各个函数中输出当前所在函数的名称,析构函数中输出自己从那个构造方法得出(根据之前的id_成员实现)。
-
我们在异常抓取时使用pass by value的方式,来查看一下拷贝发生的时机。
-
下面是我的代码:
-
#include <iostream> #include <string> class MyException{ public: MyException(){ this->id_ = 0; std::cout << "constructor" << std::endl; } MyException(const MyException& other){ this->id_ = 1; std::cout << "copy constructor" << std::endl; } ~MyException(){ std::cout << "destructor " << ((this->getId()==0) ? "constructor" : "copy constructor") << std::endl; } [[nodiscard]]int getId()const{ return this->id_; } private: int id_; }; void test_4_class_exception(){ try{ throw MyException(); }catch(MyException e){ std::cout << "caught an object error: "<< ((e.getId()==0) ? "constructor" : "copy constructor") << std::endl; } } int main() { test_4_class_exception(); return 0; }
-
其运行结果如下:
-
D:\ClionProject\exception\cmake-build-debug\exception.exe constructor copy constructor caught an object error: copy constructor destructor copy constructor destructor constructor Process finished with exit code 0
-
我们来根据这个输出结果大致分析一下这个调用的时机
-
第一条发生在try 语句中的临时变量构造中。
-
第二条时在传递exception时在catch中参数的一个拷贝,这是因为我们使用了pass by value的情况。
-
第三条表示id_值为1,这意味着这个传递过来的时一个拷贝的对象。
-
四条五条直接反映了析构调用的顺序,类似栈先进后出。
-
-
-
类对象类型异常抛出的性能优化
-
继续使用刚才的代码,但是这一次我们线在throw中声明一个对象然后再把它抛出,然后继续使用pass by value 方式接收异常:
-
#include <iostream> #include <string> class MyException{ public: MyException(){ this->id_ = 0; std::cout << "constructor" << std::endl; } MyException(const MyException& other){ this->id_ = 1; std::cout << "copy constructor" << std::endl; } ~MyException(){ std::cout << "destructor " << ((this->getId()==0) ? "constructor" : "copy constructor") << std::endl; } [[nodiscard]]int getId()const{ return this->id_; } private: int id_; }; void test_4_class_exception(){ try{ MyException exc; throw exc; }catch(MyException e){ std::cout << "caught an object error: "<< ((e.getId()==0) ? "constructor" : "copy constructor") << std::endl; } } int main() { test_4_class_exception(); return 0; }
-
输出的结果如下:
-
D:\ClionProject\exception\cmake-build-debug\exception.exe constructor copy constructor destructor constructor copy constructor caught an object error: copy constructor destructor copy constructor destructor copy constructor Process finished with exit code 0
-
一下子出现了很多的输出,说明调用了很多次的拷贝,这无异于 一种资源浪费,我们来分析一下这个输出。
-
前面三条输出,第一条就是正常的构造,然后在抛出时会调用一个拷贝构造函数,并将此靠背后的对象传递给下一层。
-
与此同时前面的局部变量被销毁调用了一个析构函数。
-
由于我们使用的时pass by value,这会在抓取异常时再次调用一次拷贝构造函数,当然我们最后传入的对象来自于拷贝因此有了四五条的输出。
-
六条七条就是把这两个对象全部销毁,虽然没有什么有价值的信息,但是在之前的测试中我们已经知道他们的析构顺序类似于栈,先进后出。
-
-
那么如何优化呢?
-
首先我们知道,在抛出异常时最佳办法就是直接抛出一个临时变量,这样会减少一次拷贝。
-
而在抓取异常时我们使用pass by reference会再次减少一次拷贝构造的调用。
-
改良后的代码如下:
-
class MyException{ public: MyException(){ this->id_ = 0; std::cout << "constructor" << std::endl; } MyException(const MyException& other){ this->id_ = 1; std::cout << "copy constructor" << std::endl; } ~MyException(){ std::cout << "destructor " << ((this->getId()==0) ? "constructor" : "copy constructor") << std::endl; } [[nodiscard]]int getId()const{ return this->id_; } private: int id_; }; void test_4_class_exception(){ try{ //MyException exc; throw MyException(); }catch(MyException& e){ std::cout << "caught an object error: "<< ((e.getId()==0) ? "constructor" : "copy constructor") << std::endl; } } int main() { test_4_class_exception(); return 0; }
-
运行结果如下,只是调用了一次构造函数并在接收异常后析构,被抓取的对象也只是经过构造函数得来的:
-
D:\ClionProject\exception\cmake-build-debug\exception.exe constructor caught an object error: constructor destructor constructor Process finished with exit code 0
-
如果抛出的是一个来自堆内存中的异常类型呢?
-
那么类似string*,他会抛出一个类对象的指针,这样接受也要用相应的形参,还有就是不要忘记堆内存的释放。
-
class MyException{ public: MyException(){ this->id_ = 0; std::cout << "constructor" << std::endl; } MyException(const MyException& other){ this->id_ = 1; std::cout << "copy constructor" << std::endl; } ~MyException(){ std::cout << "destructor " << ((this->getId()==0) ? "constructor" : "copy constructor") << std::endl; } [[nodiscard]]int getId()const{ return this->id_; } private: int id_; }; void test_4_class_exception(){ try{ //MyException exc; throw new MyException(); }catch(MyException& e){ std::cout << "caught an object error: "<< ((e.getId()==0) ? "constructor" : "copy constructor") << std::endl; }catch(MyException* e){ std::cout << "caught an object pointer error: "<< ((e->getId()==0) ? "constructor" : "copy constructor") << std::endl; delete e; } } int main() { test_4_class_exception(); return 0; }
-
输出结果如下:
-
D:\ClionProject\exception\cmake-build-debug\exception.exe constructor caught an object pointer error: constructor destructor constructor Process finished with exit code 0
-
-
异常处理与继承
-
异常可以定义成类 ,因此就会涉及到继承和多态的使用,这也是cpp中的核心功能。
-
一个小案例:设计一个数组容器Vector, 重载[]操作符号,数组初始化时对数组 个数进行 有效化的检查。
-
index<0 抛出异常 errNegativeException
-
index=0 抛出异常 errZeroException
-
index>1000 抛出异常 errOverLargeException
-
index<10 抛出异常 errOverSmallException
-
所有的异常类继承自errSizeException,在父类中要定义有参构造,并实现一个虚函数virtual void printError()用于输出相应的错误。
-
-
首先是Vector类,我直接上代码就不讲解了,这里没有使用任何的模板编程。顺便把改写的异常处理也直接加进去了。首先时头文件:
-
// // Created by herryao on 2024/1/25. // #ifndef EXCEPTION_INHERITANCE_VECTOR_H #define EXCEPTION_INHERITANCE_VECTOR_H class Vector { public: Vector(int size=128); Vector(const Vector& other); [[nodiscard]] int getLength()const; int& operator[](int indx); int operator[](int indx) const; ~Vector(); private: int *list_; int length_; }; #endif //EXCEPTION_INHERITANCE_VECTOR_H
-
然后是源文件,值得注意的是在这里我重载了两种[]的方法,那么在这两种成员方法中我们都要进行异常处理,按照之前的项目要求我已经在方法中给出了相应的声明,利用一个pass by reference 的方法调用了多态,降低了 代码的重复。
-
// // Created by herryao on 2024/1/25. // #include "Vector.h" #include "errSizeException.h" int Vector::operator[](int indx) const { if(indx==0){ throw errorZeroException(indx); }else if(indx > 1000){ throw errorOverLargeException(indx); }else if(indx > 0 && indx < 10){ throw errorOverSmallException(indx); }else if(indx < 0){ throw errorNegativeException(indx); } return this->list_[indx]; } Vector::Vector(int size) { this->list_ = new int[size]; this->length_ = size; } int Vector::getLength() const { return this->length_; } int &Vector::operator[](int indx) { if(indx==0){ throw errorZeroException(indx); }else if(indx > 1000){ throw errorOverLargeException(indx); }else if(indx > 0 && indx < 10){ throw errorOverSmallException(indx); }else if(indx < 0){ throw errorNegativeException(indx); } return this->list_[indx]; } Vector::Vector(const Vector &other) { this->list_ = new int[other.getLength()]; for(int i=0; i<other.getLength(); ++i){ this->list_[i] = other.list_[i]; } this->length_ = other.length_; } Vector::~Vector() { delete []this->list_; this->length_ = 0; }
-
然后是异常处理。
-
直接上代码,原理很简单,就是在异常类中顺便保存一下出错的索引数据并把它输出出来。这个索引数据就保存在父类的一个保护成员中,然后要在有参构造中初始化这个成员变量。所有实现都放在头文件了,代码如下:
-
// // Created by herryao on 2024/1/25. // #ifndef EXCEPTION_INHERITANCE_ERRSIZEEXCEPTION_H #define EXCEPTION_INHERITANCE_ERRSIZEEXCEPTION_H #include <iostream> class errorSizeException{ public: errorSizeException(int size):size_(size){} virtual void printError()const = 0; protected: int size_; }; class errorZeroException:public errorSizeException{ public: errorZeroException(int size): errorSizeException(size){} void printError()const override{ std::cout << "ZeroException!" << this->size_ << std::endl; } }; class errorOverLargeException:public errorSizeException{ public: errorOverLargeException(int size): errorSizeException(size){} virtual void printError()const override{ std::cout << "OverlargeException!" << this->size_ << std::endl; } }; class errorOverSmallException:public errorSizeException{ public: errorOverSmallException(int size): errorSizeException(size){} virtual void printError()const override{ std::cout << "OverSmallException!" << this->size_ << std::endl; } }; class errorNegativeException:public errorSizeException{ public: errorNegativeException(int size): errorSizeException(size){} virtual void printError()const override{ std::cout << "NegativeException!" << this->size_ << std::endl; } }; #endif //EXCEPTION_INHERITANCE_ERRSIZEEXCEPTION_H
-
最后是我们的主函数 文件,像往常一样我定义了测试函数进行相应的测试这几种相应的异常 我都进行了测试:
-
#include <iostream> #include "Vector.h" #include "errSizeException.h" void testExcep(int indx){ //default size = 1024 Vector v; try{ v[indx]; }catch(errorSizeException& e){ e.printError(); } } int main() { testExcep(5); testExcep(100000); testExcep(0); testExcep(-11111); return 0; }
-
最后的输出结果如下,意味着我们的代码符合要求了,实现了异常的多态调用:
-
D:\ClionProject\exception_inheritance\cmake-build-debug\exception_inheritance.exe OverSmallException!5 OverlargeException!100000 ZeroException!0 NegativeException!-11111 Process finished with exit code 0
异常处理的基本思想
-
C++的异常处理机制使得异常的引发和异常的处理不必在同一个函数中,这样底层的函数可以着重解决具体问题,而不必过多的考虑异常的处理。上层调用者可以再适当的位置设计对不同类型异常的处理。
-
异常是专门针对抽象编程中的一系列错误进行处理的,C++中不能借助函数机制实现异常,因为栈结构的本质是先进后出,依次访问,无法进行跳跃,但错误处理的特征却是遇到错误信息就想要转到若干级之上进行重新尝试,因此异常处理是必要的,也就此诞生。
标准库中的异常处理
- cpp标准库中也提供了异常处理的类,所有标准库的异常处理相关类都继承自Excception类,一般调用时则使用 .what()方法打印当前异常的细节,Exception支持重写,但是最好服从相应的标准和习惯。
- 一个简单的练习:
#include <iostream>
#include <exception>
#include <stdexcept>
class Student{
public:
explicit Student(int age){
if(age > 249){
throw std::out_of_range("abnormal age");
}
m_age = age;
m_space = new int[1024*1024*100];
}
private :
int m_age;
int *m_space;
};
void test_4_out_of_range(){
try{
Student stu(300);
}catch(std::out_of_range &e){
std::cout << "exception caught: " << e.what() << std::endl;
}catch(std::bad_alloc &e){
std::cout << "exception caught: " << e.what() << std::endl;
}
}
void test_for_bad_alloc(){
try{
for(int i=1; i<1024; i++){
Student * stu = new Student(18);
}
}catch(std::out_of_range &e){
std::cout << "exception caught: " << e.what() << std::endl;
}catch(std::bad_alloc &e){
std::cout << "exception caught: " << e.what() << std::endl;
}
}
int main(){
test_4_out_of_range();
test_for_bad_alloc();
return 0;
}
- 运行结果如下:
-
D:\ClionProject\std_exception\cmake-build-debug\std_exception.exe exception caught: abnormal age exception caught: std::bad_alloc Process finished with exit code 0
致谢
-
感谢Martin老师的课程。
-
感谢各位的支持,祝大家越来越强,一起进步。