WHCSRL 技术网

C++异常处理机制(超级详细)

目录

0.异常处理机制简介

1.传统错误处理机制(通过函数返回值)

2.异常处理机制语法 

​3.异常接口声明

4.异常类型和声明周期

4.1throw基本类型异常(int 、float、char.....)

4.2throw字符串类型异常

​ 4.3throw类对象类型异常

4.3.1第一种:抛出匿名对象,使用形参接收

​4.3.2第二种: 抛出局部变量,使用形参接收

4.3.3第三种:抛出匿名对象,使用引用接收(代码优化 )

5.异常和继承

6. 额外补充——标准库里的异常类


0.异常处理机制简介

异常无处不在,程序随时可能误入歧途!C++ 提出了新的异常处理机制!

异常是一种程序控制机制,与函数机制互补。

函数是一种以栈结构展开的上下函数衔接的程序控制系统,异常是另一种控制结构,它可以在出现“意外”时中断当前函数,并以某种机制(类型匹配)回馈给隔代的调用者相关的信息。

 图像示例:

当C函数发生错误时,不需要将错误一级级 返回给main,利用异常,直接返回给main:

1.传统错误处理机制(通过函数返回值)

需求: 实现文件的二进制拷贝(当希望文件能够原封不动的拷贝时,要用二进制拷贝)

将要拷贝文件:

代码实现:

  1. // demo 15-14
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #define BUFSIZE 1024
  5. //实现文件的二进制拷贝
  6. int copyfile(const char* dest, const char* src) {
  7. FILE* fp1 = NULL, * fp2 = NULL;
  8. //rb 只读方式打开一个二进制文件,只允许读取数据
  9. fopen_s(&fp1, src, "rb");
  10. if (fp1 == NULL) {
  11. return -1;
  12. }
  13. //wb 以只写的方式打开或新建一个二进制文件,只允许写数据。
  14. fopen_s(&fp2, dest, "wb");
  15. if (fp2 == NULL) {
  16. return -2;
  17. }
  18. char buffer[BUFSIZE];
  19. int readlen, writelen;
  20. //如果读到数据,则大于0
  21. while ((readlen = fread(buffer, 1, BUFSIZE, fp1)) > 0) {
  22. writelen = fwrite(buffer, 1, readlen, fp2);
  23. if (readlen != writelen) {
  24. return -3;
  25. }
  26. }
  27. fclose(fp1);
  28. fclose(fp2);
  29. return 0;
  30. }
  31. void main() {
  32. int ret = 0;
  33. //temp.txt为要拷贝的文件,CopyTemp.txt为拷贝文件
  34. ret = copyfile("CopyTemp.txt","temp.txt");
  35. if (ret != 0) {
  36. switch (ret) {
  37. case -1:
  38. printf("打开源文件失败! ");
  39. break;
  40. case -2:
  41. printf("打开目标文件失败! ");
  42. break;
  43. case -3:
  44. printf("拷贝文件时失败! ");
  45. break;
  46. default:
  47. printf("出现未知的情况! ");
  48. break;
  49. }
  50. }
  51. system("pause");
  52. }

运行截图:

拷贝文档截图:  

当temp.txt文档不存在时,运行截图:

传统错误返回需要一层层返回错误,而异常处理可以直接到达能够处理异常的地方。

2.异常处理机制语法 

  1. 异常发生第一现场,抛出异常
  2. void function( ){
  3. //... ...
  4. throw 表达式;
  5. //... ...
  6. }
  7. 在需要关注异常的地方,捕捉异常
  8. try{
  9. //程序
  10. function(); 把function至于try
  11. //程序
  12. }catch(异常类型声明){ 比如只写一个int
  13. //... 异常处理代码 ...
  14. }catch(异常类型 形参){ 形参将会取得抛出的值
  15. //... 异常处理代码 ...
  16. }catch(...){ 抛出的其它异常类型,可以接收任意类型
  17. //
  18. }
  19. 如果没有catch(...),并且没有catch子句与抛出的异常类型匹配,程序会直接中断报错。

修改“文件的二进制拷贝”代码:

  1. // demo 15-16
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string>
  5. using namespace std;
  6. #define BUFSIZE 1024
  7. //实现文件的二进制拷贝
  8. int copyfile2(const char* dest, const char* src) {
  9. FILE* fp1 = NULL, * fp2 = NULL;
  10. //通过throw操作创建一个异常对象并抛掷
  11. //rb 只读方式打开一个二进制文件,只允许读取数据
  12. fopen_s(&fp1, src, "rb");
  13. if (fp1 == NULL) {
  14. throw new string("文件不存在");
  15. }
  16. //wb 以只写的方式打开或新建一个二进制文件,只允许写数据。
  17. fopen_s(&fp2, dest, "wb");
  18. if (fp2 == NULL) {
  19. throw 0.01f;;
  20. }
  21. char buffer[BUFSIZE];
  22. int readlen, writelen;
  23. //如果读到数据,则大于0
  24. while ((readlen = fread(buffer, 1, BUFSIZE, fp1)) > 0) {
  25. writelen = fwrite(buffer, 1, readlen, fp2);
  26. if (readlen != writelen) {
  27. throw - 3;
  28. }
  29. }
  30. fclose(fp1);
  31. fclose(fp2);
  32. return 0;
  33. }
  34. //这种写法是将copyfile2的抛出异常给copyfile1,copyfile1再给main
  35. int copyfile1(const char* dest, const char* src) {
  36. try {
  37. copyfile2(dest, src);
  38. }
  39. catch (float e) {
  40. //throw ;
  41. printf("copyfile1 - catch ... ");
  42. //提示:处理不了的异常,我们可以在catch的最后一个分支,使用throw语法,继续向调用者throw。就比如再抛给main~
  43. throw; //throw后面可以写接收到的e,也可以不写,不写的话会给出“未使用局部变量”的警告
  44. }
  45. return 0;
  46. }
  47. void main() {
  48. int ret = 0;
  49. //在需要捕捉异常的地方,将可能抛出异常的程序段嵌在try块之中
  50. //按正常的程序顺序执行到达try语句,然后执行try块{}内的保护段
  51. //如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行,程序从try块后跟随
  52. //的最后一个catch子句后面的语句继续执行下去
  53. try {//保护段
  54. printf("开始执行 copyfile1... ");
  55. ret = copyfile1("CopyTemp.txt", "temp.txt");
  56. printf("执行 copyfile1 完毕 ");
  57. //catch子句按其在try块后出现的顺序被检查,匹配的catch子句将捕获并按catch子句中的代码
  58. //处理异常(或继续抛掷异常)
  59. }
  60. catch (int error) {
  61. printf("出现异常啦!%%d ", error);
  62. }
  63. catch (string* error) {
  64. printf("捕捉到字符串异常:%%s ", error->c_str());
  65. delete error;
  66. }
  67. catch (float error) {
  68. printf("出现异常啦!%%f ", error);
  69. }
  70. catch (...) {
  71. printf("catch ... ");
  72. }
  73. //如果没有找到匹配,则缺省功能是调用abort终止程序。
  74. system("pause");
  75. }

 注意:

1.throw后面可跟任何表达式,除了整数外,指针、字符常量等也可以,如:throw "文档打开失败"

2.通过throw操作创建一个异常对象并抛掷

3.在需要捕捉异常的地方,将可能抛出异常的程序段嵌在try块之中

4.按正常的程序顺序执行到达try语句,然后执行try块{}内的保护段

5.如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行,程序从try块后跟随的最后一个catch子句后面的语句继续执行下去

6.catch子句按其在try块后出现的顺序被检查,匹配的catch子句将捕获并按catch子句中的代码处理异常(或继续抛掷异常)

7.如果没有找到匹配,则缺省功能是调用abort终止程序。

运行结果:

当temp.txt文档不存在,运行结果:

3.异常接口声明

可以在函数声明中列出可能抛出的所有异常类型,加强程序的可读性。

如:

int copyfile2(char *dest, char *src) throw (float, string *, int)  { }

这样,在阅读代码时便可知这个函数会抛出float、string*和int类型异常,增加可读性

注意:

1.对于异常接口的声明,函数声明中列出可能抛出的所有异常类型

2.如果没有包含异常接口声明,此函数可以抛出任何类型的异常

3.如果函数声明中有列出可能抛出的所有异常类型,那么抛出其它类型的异常讲可能导致程序终止(对于有些编译器可以抛出其它未在声明列表中的异常类型,如老式VC++)

4.如果一个函数不想抛出任何异常,可以使用 throw () 声明(有些编译器会警告throw(),如老式VC++)

4.异常类型和声明周期

4.1throw基本类型异常(int 、float、char.....)

  1. // demo 15-17
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string>
  5. using namespace std;
  6. #define BUFSIZE 1024
  7. //实现文件的二进制拷贝
  8. //第一种情况,throw 普通类型,和函数返回传值是一样的
  9. int copyfile2(const char *dest, const char *src){
  10. FILE *fp1 = NULL, *fp2 = NULL;
  11. //rb 只读方式打开一个二进制文件,只允许读取数据
  12. fopen_s(&fp1, src, "rb");
  13. if(fp1 == NULL){
  14. //int ret = -1;
  15. char ret = 'a';
  16. throw ret;
  17. }
  18. //wb 以只写的方式打开或新建一个二进制文件,只允许写数据。
  19. fopen_s(&fp2, dest, "wb");
  20. if(fp2 == NULL){
  21. throw -2;
  22. }
  23. char buffer[BUFSIZE];
  24. int readlen, writelen;
  25. //如果读到数据,则大于0
  26. while( (readlen = fread(buffer, 1, BUFSIZE, fp1)) > 0 ){
  27. writelen = fwrite(buffer, 1, readlen, fp2);
  28. if(readlen != writelen){
  29. throw -3 ;
  30. }
  31. }
  32. fclose(fp1);
  33. fclose(fp2);
  34. return 0;
  35. }
  36. int copyfile1(const char *dest, const char *src){
  37. return copyfile2(dest, src);
  38. }
  39. void main(){
  40. int ret = 0;
  41. try{//保护段
  42. //printf("开始执行 copyfile1... ");
  43. ret = copyfile1("CopyTemp.txt", "temp.txt");
  44. //printf("执行 copyfile1 完毕 ");
  45. }catch(int error){
  46. printf("出现异常啦!%%d ", error); //error周期只在此语句中
  47. }catch(char error){
  48. printf("出现异常啦!%%c ", error); //error周期只在此语句中
  49. }
  50. system("pause");
  51. }

对于基本类型异常,使用对应的类型 catch捕捉即可~。

4.2throw字符串类型异常

  1. // demo 15-18
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string>
  5. using namespace std;
  6. #define BUFSIZE 1024
  7. int copyfile3(const char* dest, const char* src) {
  8. FILE* fp1 = NULL, * fp2 = NULL;
  9. //rb 只读方式打开一个二进制文件,只允许读取数据
  10. fopen_s(&fp1, src, "rb");
  11. if (fp1 == NULL) {
  12. const char* error = "大佬,你的源文件打开有问题";
  13. printf("throw 前,error 的地址:%%p ", error);
  14. //第二种情况,throw 字符串类型,实际抛出的指针,而且,修饰指针的const 也要严格进行类型匹配
  15. throw error; //常量字符串类型,返回的是个地址
  16. }
  17. //wb 以只写的方式打开或新建一个二进制文件,只允许写数据。
  18. fopen_s(&fp2, dest, "wb");
  19. if (fp2 == NULL) {
  20. throw - 2;
  21. }
  22. char buffer[BUFSIZE];
  23. int readlen, writelen;
  24. //如果读到数据,则大于0
  25. while ((readlen = fread(buffer, 1, BUFSIZE, fp1)) > 0) {
  26. writelen = fwrite(buffer, 1, readlen, fp2);
  27. if (readlen != writelen) {
  28. throw - 3;
  29. }
  30. }
  31. fclose(fp1);
  32. fclose(fp2);
  33. return 0;
  34. }
  35. int copyfile1(const char* dest, const char* src) {
  36. return copyfile3(dest, src);
  37. }
  38. void main() {
  39. int ret = 0;
  40. try {//保护段
  41. //printf("开始执行 copyfile1... ");
  42. ret = copyfile1("CopyTemp.txt", "temp.txt");
  43. //printf("执行 copyfile1 完毕 ");
  44. }
  45. catch (int error) {
  46. printf("出现异常啦!%%d ", error);
  47. }
  48. catch (char error) {
  49. printf("出现异常啦!%%c ", error);
  50. }
  51. catch (string error) {
  52. printf("出现异常啦!%%s ", error.c_str());
  53. }
  54. //严格类型匹配,这里也需要加const
  55. catch (const char* error) { //接收常量字符串类型异常地址
  56. printf("出现异常啦(char *)!%%s(地址:%%p) ", error, error);
  57. }
  58. catch (...) {
  59. printf("没捉到具体的异常类型 ");
  60. }
  61. system("pause");
  62. }

当抛出的异常类型为地址(指针)时候,catch捕捉类型也要为指针。并且,当抛出异常类型有const修饰时,捕捉类型也要有cosnt修饰。即严格类型匹配。

运行结果:

4.3throw类对象类型异常

当抛出的异常类型为一个类对象时:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string>
  4. class ErrorException {
  5. public:
  6. ErrorException() {
  7. id = 0;
  8. printf("ErrorException 构造! ");
  9. }
  10. ~ErrorException() {
  11. printf("ErrorException ~析构!(id: %%d) ", id);
  12. }
  13. ErrorException(const ErrorException& e) {
  14. id = 1;
  15. printf("ErrorException 拷贝构造函数! ");
  16. }
  17. int id;
  18. };

下面对throw语句进行分类研究: 

4.3.1第一种:抛出匿名对象,使用形参接收

  1. throw ErrorException(); //抛出匿名对象
  2. catch (ErrorException error) {
  3. printf("出现异常啦!捕捉到 ErrorException 类型 id: %%d ", error.id);
  4. }

运行结果:

4.3.2第二种: 抛出局部变量,使用形参接收

  1. ErrorException error1;
  2. throw error1;
  3. catch (ErrorException error) {
  4. error.id=2; //增加一句error.id=2,便于惯出
  5. printf("出现异常啦!捕捉到 ErrorException 类型 id: %%d ", error.id);
  6. }

运行结果:

由以上两种结果可得,不管是抛出匿名对象,还是抛出局部变量,程序都会生成一个匿名对象,并且,当catch是普通参数时,程序还要再次调用拷贝函数。 

4.3.3第三种:抛出匿名对象,使用引用接收(代码优化 )

  1. throw ErrorException(); //直接抛出匿名对象
  2. catch (ErrorException& error) { 用引用方式来接
  3. //error.id = 2;
  4. printf("出现异常啦!捕捉到 ErrorException &类型 id: %%d ", error.id);
  5. }

 运行结果: 

当然也可以 动态分配内存,直接抛出指针,当然也要用指针接收。(严格类型匹配)

  1. throw new ErrorException();
  2. catch (ErrorException* error) {
  3. printf("出现异常啦!捕捉到 ErrorException *类型 id: %%d ", error->id);
  4. delete error;
  5. }

运行结果:

 注意: 引用和普通的形参传值不能共存!

因为引用和普通形参都可以catch捕捉,编译器无法识别被哪个捕捉 。

5.异常和继承

异常也是类,我们可以创建自己的异常类,在异常中可以使用(虚函数,派生,引用传递和数据成员等)。

 案例:设计一个数组类容器 Vector,重载[]操作,数组初始化时,对数组的个数进行有效检查

1.index<0 抛出异常errNegativeException  

2.index = 0 抛出异常 errZeroException

3.index>1000抛出异常errTooBigException 

4.index<10 抛出异常errTooSmallException 

5.errSizeException类是以上类的父类,实现有参数构造、并定义virtual void printError()

Vector容器实现:

  1. #include<iostream>
  2. using namespace std;
  3. class Vector{
  4. public:
  5. Vector(int length = 128);
  6. int getLength();
  7. int& operator[](int index);
  8. ~Vector();
  9. private:
  10. int*m_base;
  11. int m_length;
  12. };
  13. Vector::Vector(int length) {
  14. m_length = length;
  15. m_base = new int[length];
  16. }
  17. int Vector::getLength() {
  18. return m_length;
  19. }
  20. //引用方式返回可以作为左值修改(即存储,Vector va(5); va[1]=0)
  21. int& Vector::operator[](int index) {
  22. return m_base[index];
  23. }
  24. Vector::~Vector() {
  25. if (m_base) {
  26. delete[] m_base;
  27. m_length = 0;
  28. }
  29. }

异常类实现:

  1. class errNegativeException {
  2. };
  3. class errZeroException {
  4. };
  5. class errTooBigException {
  6. };
  7. class errTooSmallException {
  8. };

修改Vector的构造函数:

  1. Vector::Vector(int length) {
  2. if (length < 0) {
  3. throw errNegativeException();
  4. }
  5. else if (length == 0) {
  6. throw errZeroException();
  7. }
  8. else if (length > 1000) {
  9. throw errTooBigException();
  10. }
  11. else if (length <= 10) {
  12. throw errTooSmallException();
  13. }
  14. m_length = length;
  15. m_base = new int[length];
  16. }

实现主函数接口:

  1. int main() {
  2. try {
  3. Vector va(10);
  4. for (int i = 0; i < va.getLength(); i++) {
  5. va[i] = i + 3;
  6. }
  7. }
  8. catch (errNegativeException& erro) {
  9. cout << "errNegativeException" << endl;
  10. }
  11. catch (errZeroException& erro) {
  12. cout << "errZeroException" << endl;
  13. }
  14. catch (errTooBigException& erro) {
  15. cout << "errTooBigException" << endl;
  16. }
  17. catch (errTooSmallException& erro) {
  18. cout << "errTooSmallException" << endl;
  19. }
  20. system("pause");
  21. return 0;
  22. }

 运行结果:

 这种抛出异常方法跟直接return一个结果没啥两样,现在通过以前学习的虚函数进行代码优化: 

异常类代码优化:

  1. class errSizeException {
  2. public:
  3. errSizeException(int size) {
  4. m_size = size;
  5. }
  6. virtual void printErroe() {
  7. cout << "size:" << m_size << endl;
  8. }
  9. protected:
  10. int m_size;
  11. };
  12. //子类errNegativeException
  13. class errNegativeException :public errSizeException {
  14. public:
  15. errNegativeException(int size) :errSizeException(size){}
  16. virtual void printErroe() {
  17. cout << "errNegativeException size:" << m_size << endl;
  18. }
  19. };
  20. //子类errZeroException
  21. class errZeroException :public errSizeException {
  22. public:
  23. errZeroException(int size) :errSizeException(size) {}
  24. virtual void printErroe() {
  25. cout << "errZeroException size:" << m_size << endl;
  26. }
  27. };
  28. //子类errTooBigException
  29. class errTooBigException :public errSizeException {
  30. public:
  31. errTooBigException(int size) :errSizeException(size) {}
  32. virtual void printErroe() {
  33. cout << "errTooBigException size:" << m_size << endl;
  34. }
  35. };
  36. //子类errTooSmallException
  37. class errTooSmallException :public errSizeException {
  38. public:
  39. errTooSmallException(int size) :errSizeException(size) {}
  40. virtual void printErroe() {
  41. cout << "errTooSmallException size:" << m_size << endl;
  42. }
  43. };

父类是 errSizeException,子类继承父类。通过虚函数,实现子类代替父类。

主函数接口:

  1. int main() {
  2. try {
  3. Vector va(10);
  4. for (int i = 0; i < va.getLength(); i++) {
  5. va[i] = i + 3;
  6. }
  7. }
  8. catch (errSizeException& erro) {
  9. erro.printErroe();
  10. }
  11. system("pause");
  12. return 0;
  13. }

运行结果:

6. 额外补充——标准库里的异常类

无需硬背,混个眼熟即可~

代码示例:

  1. #include <iostream>
  2. #include <exception>
  3. #include <stdexcept>
  4. using namespace std;
  5. class Student{
  6. public:
  7. Student(int age){
  8. if(age > 249){
  9. throw out_of_range("年龄太大,你是外星人嘛?");
  10. }
  11. m_age = age;
  12. m_space = new int[1024*1024*100];
  13. }
  14. private :
  15. int m_age;
  16. int *m_space;
  17. };
  18. void main(){
  19. try{
  20. for(int i=1; i<1024; i++){
  21. Student * xiao6lang = new Student(18);
  22. }
  23. }catch(out_of_range &e){
  24. cout<<"捕捉到一只异常:"<<e.what()<<endl;
  25. }catch(bad_alloc &e){
  26. cout<<"捕捉到动态内存分配的异常:"<<e.what()<<endl;
  27. }
  28. system("pause");
  29. }

运行结果:

推荐阅读