前言
- 异常是一种程序控制机制,与函数机制独立和互补:
函数是一种以栈结构展开的上下函数衔接的程序控制系统,异常是另一种控制结构,它依附于栈结构,却可以同时设置多个异常类型作为网捕条件,从而以类型匹配在栈机制中跳跃回馈. - 异常设计目的:
栈机制是一种高度节律性控制机制,面向对象编程却要求对象之间有方向、有目的的控制传动,从一开始,异常就是冲着改变程序控制结构,以适应面向对象程序更有效地工作这个主题,而不是仅为了进行错误处理。
异常设计出来之后,却发现在错误处理方面获得了最大的好处。
异常基本语法
- 若有异常则通过throw操作创建一个异常对象并抛掷。
- 将可能抛出异常的程序段嵌在try块之中。控制通过正常的顺序执行到达try语句,然后执行try块内的保护段。
- 如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行。程序从try块后跟随的最后一个catch子句后面的语句继续执行下去。
- catch子句按其在try块后出现的顺序被检查。匹配的catch子句将捕获并处理异常(或继续抛掷异常)。
- 如果匹配的处理器未找到,则运行函数terminate将被自动调用,其缺省功能是调用abort终止程序。
- 处理不了的异常,可以在catch的最后一个分支,使用throw语法,向上扔。
这是一个C++中异常的抛出机制:
#include <iostream>
using namespace std;
void divde(int x, int y)
{
if (y == 0)
{
throw x; // 当y == 0时,抛出异常
}
cout << "y/x的结果" << x/y << endl;
}
int main()
{
try
{
divde(10, 2);
divde(10, 0);
}
catch (int e)
{
cout << e << "被零整数" << endl;
}
catch (...) //其他种类的异常处理
{
cout << "未知异常" << endl;
}
return 0;
}
异常可以不处理,继续外抛。
#include <iostream>
using namespace std;
void divde(int x, int y)
{
if (y == 0)
{
throw x; // 当y == 0时,抛出异常
}
cout << "y/x的结果" << x/y << endl;
}
void mydivde(int x, int y)
{
try
{
divde(x, y);
}
catch (...)
{
cout << "我不处理异常" << endl;
throw;
}
}
int main()
{
try
{
mydivde(100, 0);
}
catch (int e)
{
cout << e << "被零整数" << endl;
}
catch (...) //其他种类的异常处理
{
cout << "未知异常" << endl;
}
return 0;
}
#include <iostream>
using namespace std;
void testFunction1() {
if (1) throw 1;
}
void testFunction2() {
try
{
testFunction1();
}
catch (char) /*异常时严格的按照类型匹配的*/
{
cout << "期待的异常char" << endl;
}
}
int main()
{
testFunction2();
return 0;
}
throw 1将穿透函数testFunction1,testFunction2和main,抵达系统的最后一道防线——激发terminate函数(C++中,异常不可以忽略,当异常找不到匹配的catch字句时,会调用系统的库函数terminate(),该函数调用引起运行终止的abort函数。)最后一道防线的函数可以由程序员设置.从而规定其终止前的行为.
修改系统默认行为:
- 可以通过set_terminate函数修改捕捉不住异常的默认处理器,从而使得发生捉不住异常时,被自定义函数处理:
- void myTerminate(){cout<<“HereIsMyTerminate\n”;}
- set_terminate(myTerminate);
- set_terminate函数在头文件exception中声明,参数为函数指针void(*)().
栈解旋(unwinding)
异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上的构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反。这一过程称为栈的解旋(unwinding)。
案例:
#include <iostream>
using namespace std;
class MyException {};
class Test
{
public:
Test(int a = 0, int b = 0)
{
this->a = a;
this->b = b;
cout << "Test 构造函数执行" << "a:" << a << " b: " << b << endl;
}
void printT()
{
cout << "a:" << a << " b: " << b << endl;
}
~Test()
{
cout << "Test 析构函数执行" << "a:" << a << " b: " << b << endl;
}
private:
int a;
int b;
};
void myFunc() throw (MyException)
{
Test t1(1, 2);
Test t2(2, 3);
cout << "要发生异常" << endl;
throw 1;
}
int main()
{
//异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上的构造的所有对象,
//都会被自动析构。析构的顺序与构造的顺序相反。
//这一过程称为栈的解旋(unwinding)
try
{
myFunc();
}
catch (int e)
{
cout << "接收到int类型异常" << endl;
}
catch (...)
{
cout << "未知类型异常" << endl;
}
return 0;
}
测试结果:这个也很好理解,throw
异常相当这个函数要返回了,把异常信息返回给捕获函数。那么就要把在这个函数压栈产生的变量给进行栈解旋。
异常接口声明
- 为了加强程序的可读性,可以在函数声明中列出可能抛出的所有异常类型,例如:
void func() throw (A, B, C , D);
//这个函数func()
能够且只能抛出类型A B C D及其子类型的异常。 - 如果在函数声明中没有包含异常接口声明,则次函数可以抛掷任何类型的异常,例如:
void func();
- 一个不抛掷任何类型异常的函数可以声明为:
void func() throw();
- 如果一个函数抛出了它的异常接口声明所不允许抛出的异常,
unexpected
函数会被调用,该函数默认行为调用terminate
函数中止程序。
异常类型和异常变量的生命周期
throw
的异常是有类型的,可以使,数字、字符串、类对象。throw
的异常是有类型的,catch
严格按照类型进行匹配。- 注意 异常对象的内存模型
下面我比较传统的错误处理机制和C++的异常处理机制
传统的错误处理机制
#include <iostream>
using namespace std;
// 传统的错误处理机制
int my_strcpu(char *dest, char *soucre)
{
if (dest == nullptr)
{
return 1;
}
if (soucre == nullptr)
{
return 2;
}
// 比如做一个copy的数据限定
if (*soucre == 'a')
{
return 3;
}
while (*soucre != '\0')
{
*dest++ = *soucre++;
}
*dest = '\0';
return 0;
}
int main()
{
int ret;
char destBuff[1024] = {0};
char sourceBuff[] = "123456789";
ret = my_strcpu(destBuff,sourceBuff);
if (ret != 0)
{
switch (ret)
{
case 1: cout << "destBuff is error" << endl;
break;
case 2: cout << "sourceBuff is error" << endl;
break;
case 3: cout << "copy is error" << endl;
break;
default:cout << "未知错误" << endl;
break;
}
}
cout << destBuff << endl;
return 0;
}
异常处理机制
#include <iostream>
using namespace std;
// C++的异常错误处理机制
class BadDest {};
class BadSource {};
class BadCopy {};
void my_strcpu(char* dest, char* soucre)
{
if (dest == nullptr)
{
throw BadDest();
}
if (soucre == nullptr)
{
throw BadSource();
}
// 比如做一个copy的数据限定
if (*soucre == 'a')
{
throw BadCopy();
}
while (*soucre != '\0')
{
*dest++ = *soucre++;
}
*dest = '\0';
}
int main()
{
int ret;
char destBuff[1024] = { 0 };
char sourceBuff[] = "a123456789";
try
{
my_strcpu(destBuff, sourceBuff);
}
catch (BadDest e)
{
cout << "destBuff is error" << endl;
}
catch (BadSource e)
{
cout << "sourceBuff is error" << endl;
}
catch (BadCopy e)
{
cout << "copy is error" << endl;
}
catch (...)
{
cout << "未知错误" << endl;
}
cout << destBuff << endl;
return 0;
}
这个异常会throw一个匿名对象,那么这个有一个问题就值得谈论?这个匿名对象是copy给e还是e就是这个匿名对象了?先说结论:这个匿名对象是拷贝给e的。
测试代码
#include <iostream>
using namespace std;
// C++的异常错误处理机制
class BadDest {};
class BadSource {};
class BadCopy
{
public:
BadCopy()
{
cout << "BadCopy 构造函数 do" << endl;
}
BadCopy(const BadCopy &obj)
{
cout << "BadCopy 拷贝构造函数 do" << endl;
}
~BadCopy()
{
cout << "BadCopy 析构函数 do" << endl;
}
};
void my_strcpu(char* dest, char* soucre)
{
if (dest == nullptr)
{
throw BadDest();
}
if (soucre == nullptr)
{
throw BadSource();
}
// 比如做一个copy的数据限定
if (*soucre == 'a')
{
cout << "开始throw BadCopy" << endl;
throw BadCopy();
}
while (*soucre != '\0')
{
*dest++ = *soucre++;
}
*dest = '\0';
}
int main()
{
int ret;
char destBuff[1024] = { 0 };
char sourceBuff[] = "a123456789";
try
{
my_strcpu(destBuff, sourceBuff);
}
catch (BadDest e)
{
cout << "destBuff is error" << endl;
}
catch (BadSource e)
{
cout << "sourceBuff is error" << endl;
}
catch (BadCopy e)
{
cout << "copy is error" << endl;
}
catch (...)
{
cout << "未知错误" << endl;
}
cout << destBuff << endl;
return 0;
}
测试结果如下:
进一步讨论如果这个是引用的话,匿名对象是copy还是直接使用throw出来的那个对象。
#include <iostream>
using namespace std;
// C++的异常错误处理机制
class BadDest {};
class BadSource {};
class BadCopy
{
public:
BadCopy()
{
cout << "BadCopy 构造函数 do" << endl;
}
BadCopy(const BadCopy &obj)
{
cout << "BadCopy 拷贝构造函数 do" << endl;
}
~BadCopy()
{
cout << "BadCopy 析构函数 do" << endl;
}
};
void my_strcpu(char* dest, char* soucre)
{
if (dest == nullptr)
{
throw BadDest();
}
if (soucre == nullptr)
{
throw BadSource();
}
// 比如做一个copy的数据限定
if (*soucre == 'a')
{
cout << "开始throw BadCopy" << endl;
throw BadCopy();
}
while (*soucre != '\0')
{
*dest++ = *soucre++;
}
*dest = '\0';
}
int main()
{
int ret;
char destBuff[1024] = { 0 };
char sourceBuff[] = "a123456789";
try
{
my_strcpu(destBuff, sourceBuff);
}
catch (BadDest e)
{
cout << "destBuff is error" << endl;
}
catch (BadSource e)
{
cout << "sourceBuff is error" << endl;
}
catch (BadCopy &e)
{
cout << "copy is error" << endl;
}
catch (...)
{
cout << "未知错误" << endl;
}
cout << destBuff << endl;
return 0;
}
测试结果:
更进一步讨论如果这个是引用的话,匿名对象是copy还是直接使用throw出来的那个对象。
大家可以试试,你使用指针的类型接收上述代码throw的异常是接受不到,还有一个小细节,catch (BadCopy &e)
和catch (BadCopy e)
不可以同时存在,但是catch (BadCopy &e)
和catch (BadCopy *e)
可以同时存在,catch (BadCopy e)
和catch (BadCopy *e)
也是可以的。那想要让catch (BadCopy *e)
可以接受的到,那么throw就必须抛出一个地址,哪只要代码这么 throw &(BadCopy());
改一下就可以了
异常生命周期总结:
结论1:如果在接受异常的时候使用一个异常变量,则copy
构造异常变量。
结论2:如果在接受异常的时候使用一个异常引用,会使用throw
的那个匿名对象,所以引用e
还是那个匿名对象
结论3:指针和引用/变量可以同时存在,但是引用和变量不能写在一块。
结论4:若想使用指针接受异常,throw
的值是一个地址,这样子写还可能导致野指针的问题!!!地址throw
出来后析构了,但是指针还没有指向nullptr
。好的方法是throw new (BadCopy());
但是记得delete e
在捕获中避免内存泄漏。
最好的方法:引用接,简单方便安全
异常的层次结构(继承在异常中的应用)
- 异常是类 – 创建自己的异常类
- 异常派生
- 异常中的数据:数据成员
- 按引用传递异常
- 在异常中使用虚函数
案例:
案例:设计一个数组类 MyArray,重载[]操作,
数组初始化时,对数组的个数进行有效检查
1) index<0 抛出异常eNegative
2) index = 0 抛出异常 eZero
3) index>1000抛出异常eTooBig
4) index<10 抛出异常eTooSmall
5) eSize类是以上类的父类,实现有参数构造、并定义virtual void printErr()输出错误。
#include <iostream>
using namespace std;
class MyArrary
{
public:
MyArrary(int arrayLength);
~MyArrary();
int& operator [](int index);
int getArraryLength();
//异常处理类
public:
class eSize
{
public:
eSize(int size)
{
mySize = size;
}
virtual void printError()
{
cout << "size" << mySize << endl;
}
protected:
int mySize;
};
class eNegative : public eSize
{
public:
eNegative(int size) : eSize(size)
{
}
virtual void printError()
{
cout << "eNegative is error ArrayLength is " << mySize << endl;
}
};
class eZero : public eSize
{
public:
eZero(int size) : eSize(size)
{
}
virtual void printError()
{
cout << "eZero is error ArrayLength is " << mySize << endl;
}
};
class eTooBig : public eSize
{
public:
eTooBig(int size) : eSize(size)
{
}
virtual void printError()
{
cout << "eTooBig is error ArrayLength is " << mySize << endl;
}
};
class eTooSmall : public eSize
{
public:
eTooSmall(int size) : eSize(size)
{
}
virtual void printError()
{
cout << "eTooSmall is error ArrayLength is " << mySize << endl;
}
};
protected:
private:
int arrayLength;
int* mySpace;
};
MyArrary::MyArrary(int arrayLength)
{
if (arrayLength < 0)
{
throw eNegative(arrayLength);
}
else if (arrayLength == 0)
{
throw eZero(arrayLength);
}
else if (arrayLength > 1000)
{
throw eTooBig(arrayLength);
}
else if (arrayLength < 10)
{
throw eTooSmall(arrayLength);
}
this->arrayLength = arrayLength;
mySpace = new int[arrayLength];
}
MyArrary::~MyArrary()
{
if (mySpace != nullptr)
{
delete[] mySpace;
mySpace = nullptr;
arrayLength = 0;
}
arrayLength = 0;
}
int& MyArrary::operator [](int index)
{
return mySpace[index];
}
int MyArrary::getArraryLength()
{
return arrayLength;
}
int main()
{
//1) index < 0 抛出异常eNegative
// 2) index = 0 抛出异常 eZero
// 3) index>1000抛出异常eTooBig
// 4) index < 10 抛出异常eTooSmall
// 5) eSize类是以上类的父类,实现有参数构造、并定义virtual void printErr()输出错误。
try
{
MyArrary a(0);
for (int i = 0; i < a.getArraryLength(); i++)
{
a[i] = i;
cout << a[i] << endl;
}
}
catch (MyArrary::eSize & e)
{
e.printError(); // 利用多态
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
可以使用多态和异常相结合大大的提高异常处理的灵活性。
标准程序库异常
使用标准异常库的案例:
#include <iostream>
#include <stdexcept>
using namespace std;
class Teacher
{
public:
Teacher(int age)
{
if (age > 100)
{
throw out_of_range("年龄太大");
}
this->age = age;
}
private:
int age;
};
int main()
{
try
{
Teacher t1(101);
}
catch (out_of_range e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}