调用某个函数,当发生错误时,就是异常,那么该如何处理呢??
传统的错误异常处理
按照传统的方式,当函数发生错误时,返回一个特定的值,然后在main函数中判断,如果是该值,就输出一句话提示程序员该函数放生错误了。
举一个例子:
将一个文件的内容以二进制方式拷贝到另一个文件中。
代码:
#include <stdio.h>
#include <stdlib.h>
#define BUFSIZE 1024
// 实现文件的二进制拷贝
int copyfile1(const char* dest, const char* src) {
FILE* fp1 = NULL;
FILE* fp2 = NULL;
// rb 只读方式打开一个二进制文件,只允许读取数据
fopen_s(&fp1, src, "rb");
if (!fp1) {
return -1; // 异常返回
}
// wb 以只写的方式打开或新建一个二进制文件,只允许写数据
fopen_s(&fp2, dest, "wb");
if (!fp2) {
return -2; // 异常返回
}
char buffer[BUFSIZE];
int readlen, writelen;
// 如果读到数据, 则大于零
while ((readlen = fread(buffer, 1, BUFSIZE, fp1)) > 0) {
writelen = fwrite(buffer, 1, readlen, fp2);
if (readlen != writelen) {
return -3; // 异常返回
}
}
fclose(fp1);
fclose(fp2);
return 0;
}
int main(void) {
int ret = 0;
ret = copyfile1("dest.txt", "src.txt");
if (ret != 0) {
switch (ret) {
case -1:
printf("打开源文件失败!\n");
break;
case -2:
printf("打开目的文件失败!\n");
break;
case -3:
printf("拷贝文件时失败!\n");
break;
default:
printf("出现位置情况!\n");
break;
}
} else {
printf("文件拷贝完成!\n");
}
return 0;
}
当没有src.txt文件时:
运行截图:
这种方式很麻烦,且要了一圈才回到main函数中,所以C++出了一种新的异常处理机制 try-catch.
新的异常处理机制
使用 throw 返回内容
再配合:
try {
} catch(…) {
}
可以返回所有的数据类型
基本语法:
异常发生第一现场,抛出异常
void function( ){
//… …
throw 表达式;
//… …
}
在需要关注异常的地方,捕捉异常
try{
//程序
function();
//程序
}catch(异常类型声明){
//… 异常处理代码 …
}catch(异常类型 形参){
//… 异常处理代码 …
}catch(…){ //其它异常类型
//
}
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string>
constexpr auto BUFSIZE = 1024;
// 使用
// throw 返回内容
// 再配合:
/*try {
} catch(...) {
}
可以返回所有的数据类型
*/
// 实现文件的二进制拷贝
int copyfile2(const char* dest, const char* src) {
FILE* fp1 = NULL;
FILE* fp2 = NULL;
//throw 0.01f;
// rb 只读方式打开一二进制文件,只允许读取数据
fopen_s(&fp1, src, "rb");
if (!fp1) {
throw new std::string("打开目标文件失败!"); // 通过throw操作创建一个异常对象并抛掷
}
// wb 以只写的方式打开或新建一个二进制文件,只允许写数据
fopen_s(&fp2, dest, "wb");
if (!fp2) {
throw -2; // 通过throw操作创建一个异常对象并抛掷
}
char buffer[BUFSIZE];
int readlen, writelen;
// 如果读到数据, 则大于零
while ((readlen = fread(buffer, 1, BUFSIZE, fp1)) > 0) {
writelen = fwrite(buffer, 1, readlen, fp2);
if (readlen != writelen) {
throw -1; // 通过throw操作创建一个异常对象并抛掷
}
}
fclose(fp1);
fclose(fp2);
return 0;
}
int main(void) {
// 在需要捕捉异常的地方,将可能抛出异常的程序段嵌在try块之中
// 按正常的程序顺序执行到达try语句,然后执行try块{}内的保护段
// 如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行,程序从try块后跟随的最后一个catch子句后面的语句继续执行下去
try {
printf("开始执行copyfile()\n");
copyfile2("dest.txt", "src.txt");
printf("copyfile()执行完毕\n");
// catch子句按其在try块后出现的顺序被检查,匹配的catch子句将捕获并按catch子句中的代码处理异常(或继续抛掷异常)
} catch (int error) {
printf("捕获到异常:%d\n", error);
} catch (std::string *error) {
printf("捕获到异常:%s\n", error->c_str());
} catch (...) { // ...相当于else 或者 default 用法
// 当上面的catch没有匹配,就会执行该语句
}
// 如果没有加上 catch(...), 也没有找到匹配,则缺省功能是调用abort终止程序。
return 0;
}
当没有src.txt文件时:
运行截图:
注意事项:
- 通过throw操作创建一个异常对象并抛掷
throw 表达式; // 例如: throw -1;
- 在需要捕捉异常的地方,将可能抛出异常的程序段嵌在try块之中
try {
copyfile2("dest.txt", "src.txt"); // 将该函数放在 try 中
} catch (int error) {
// ...
}
- 按正常的程序顺序执行到达try语句,然后执行try块{}内的保护段
try { // 假如不触发异常,则按照顺序执行里面的代码
printf("开始执行copyfile()\n");
copyfile2("dest.txt", "src.txt");
printf("copyfile()执行完毕\n");
} catch (int error) {
// ...
}
- 如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行,程序从try块后跟随的最后一个catch子句后面的语句继续执行下去
int main(void) {
try {
printf("开始执行copyfile()\n");
copyfile2("dest.txt", "src.txt");
printf("copyfile()执行完毕\n");
} catch (int error) {
printf("捕获到异常:%d\n", error);
} catch (std::string *error) {
printf("捕获到异常:%s\n", error->c_str());
} catch (...) { // ...相当于else 或者 default 用法
// ...
}
/*******************************************************
假如没有异常,那么就不会执行catch语句,然后执行system("pause");语句!!!
********************************************************/
system("pause");
return 0;
}
- catch子句按其在try块后出现的顺序被检查,匹配的catch子句将捕获并按catch子句中的代码处理异常(或继续抛掷异常)
int main(void) {
try {
printf("开始执行copyfile()\n");
copyfile2("dest.txt", "src.txt");
printf("copyfile()执行完毕\n");
/*******************************************************
假如发生异常,且throw返回的是一个整数的话,那么就执行第一个catch语句1!!
********************************************************/
} catch (int error) {
printf("捕获到异常:%d\n", error);
} catch (std::string *error) {
printf("捕获到异常:%s\n", error->c_str());
} catch (...) { // ...相当于else 或者 default 用法
// ...
}
system("pause");
return 0;
}
- 如果没有找到匹配,则缺省功能是调用abort终止程序。
int main(void) {
try {
printf("开始执行copyfile()\n");
copyfile2("dest.txt", "src.txt");
printf("copyfile()执行完毕\n");
} catch (int error) {
printf("捕获到异常:%d\n", error);
} catch (std::string *error) {
printf("捕获到异常:%s\n", error->c_str());
} catch (...) { // ...相当于else 或者 default 用法
// 当上面的catch没有匹配,就会执行该语句
}
/********************************************************************
如果没有加上 catch(...), 也没有找到匹配,则缺省功能是调用abort终止程序。
**********************************************************************/
return 0;
}
提示:处理不了的异常,我们可以在catch的最后一个分支,使用throw语法,继续向调用者throw。
void functon1(void) {
throw 0.1f; // ----------
} // |
// |
void function2(void) { // |
try { // | // 对应关系
functon1(); // |
// |
} catch (float f) { //<-----|
throw f; // ----------
} // |
} // |
// | // 对应关系
int main(void) { // |
try { // |
function2();// |
} catch(float f) { // <-----|
// ...
}
return 0;
}
异常接口声明
可以在函数声明中列出可能抛出的所有异常类型,加强程序的可读性。
如:
void function(void) throw(int, float, string *);
- 对于异常接口的声明,在函数声明中列出可能抛出的所有异常类型
- 如果没有包含异常接口声明,此函数可以抛出任何类型的异常
- 如果函数声明中有列出可能抛出的所有异常类型,那么抛出其它类型的异常讲可能导致程序终止
- 如果一个函数不想抛出任何异常,可以使用
throw ()
声明
void function(void) throw() {
// ...
}
代码:
#include <iostream>
#include <string>
using namespace std;
/**************************************************/
// 声明列出可能抛出的所有异常类型,加强程序的可读性
void function(void) throw(int, float, string *) { // 声明之后就不可以在抛出其他的数据类型了
if ((1 > 2) == false) { // 有些编译器可以,有些则会报错
throw -1;
}
if ((2 > 3) == false) {
throw -0.1;
}
if ((3 > 4) == false) {
throw new string("发生错误!"); // 返回一个字符串指针
}
}
int main(void) {
try {
function();
} catch (int in) {
cout << "错误异常1:" << in << endl;
} catch (float f) {
cout << "错误异常2:" << f << endl;
} catch (string * str) {
cout << "错误异常3:" << str << endl;
}
system("pause");
return 0;
}
因为第一个就发生错误了,出发了错误异常处理,所以第二第三个就不会再执行
运行截图:
异常类型和生命周期
可分为三种:
- 第一种情况,throw 普通类型;
- 第二种情况,throw 字符串类型;
- 第三种情况,throw 类类型。
第一种情况,throw 普通类型
和函数返回传值是一样的。
其实上面那些代码就已经详细解析了,现在这里再讲一遍。
代码:
#include <iostream>
#include <Windows.h>
using namespace std;
//第一种情况,throw 普通类型,和函数返回传值是一样的
int func(int n) {
if (n < 0) {
throw -1; // 返回任何整数都可以
/* 也可以像函数一样这样写:
int ret = -1;
throw ret;
*/
}
return n;
}
int main(void) {
try {
func(-10);
} catch (int error) {
cout << "出现异常错误啦!" << error << endl;
}
system("pause");
return 0;
}
运行截图:
第二种情况,throw 字符串类型
即:throw "字符串";
实际抛出的是指针,而且,修饰指针的const 也要严格进行类型匹配。
代码:
#include <iostream>
#include <Windows.h>
using namespace std;
//第二种情况,throw 字符串类型,实际抛出的指针,而且,修饰指针的const 也要严格进行类型匹配
int func(int n) {
if (n < 0) {
throw "异常又出现啦!";
/* 也可以像函数一样这样写:
const char *ret = "异常又出现啦!";
throw ret;
*/
}
return n;
}
int main(void) {
try {
func(-10);
// 注意:返回字符串返回的是一个指针,所以得用 const char * 类型接收
} catch (const char *error) {
cout << "异常打印:" << error << endl;
}
system("pause");
return 0;
}
运行截图:
注意:返回字符串返回的是一个指针,所以得用 const char * 类型接收
第三种情况,throw 类类型
其实就是 throw 一个对象。
第三种情况,throw 类类型,最佳的方式是使用引用类型捕捉,抛出匿名对象
当然,如果是动态分配的对象,直接抛出其指针
注意:引用和普通的形参传值不能共存
他有五种形式,每种形式的声明周期也不一样。
一、匿名对象和普通对像
throw Error();// 匿名对象
try {
func1(-10);
} catch (Error error) { // 普通对象
cout << "异常打印:Error" << endl;
}
运行截图:
二、普通对象和普通对象
Error error;
throw error; // 普通对象
try {
func1(-10);
} catch (Error error) { // 普通对象
cout << "异常打印:Error" << endl;
}
运行截图:
三、普通对象和使用引用
Error error;
throw error; // 普通对象
try {
func1(-10);
} catch (Error &error) { // 使用引用
cout << "异常打印:Error" << endl;
}
运行截图:
四、匿名对象和使用引用
throw Error(); // 匿名对象
try {
func1(-10);
} catch (Error &error) { // 使用引用
cout << "异常打印:Error" << endl;
}
运行截图:
五、匿名指针对象和使用指针
throw new Error(); // 指针匿名对象
try {
func1(-10);
} catch (Error *error) { // 使用指针
cout << "异常打印:Error" << endl;
delete error;
}
运行截图:
由上面几种方式可以知道,最佳的方式是使用引用类型捕捉,抛出匿名对象;
不管上面五种方式你有没有看懂,只要记得,需要抛出类类型时,记得使用“匿名对象和使用引用”就对了!
下面是 使用引用 的代码:
#include <iostream>
#include <Windows.h>
using namespace std;
//第三种情况,throw 类类型,最佳的方式是使用引用类型捕捉,抛出匿名对象
//当然,如果是动态分配的对象,直接抛出其指针
//注意:引用和普通的形参传值不能共存
class Error {
public:
Error() {
id = 0;
cout << "构造函数!" << endl;
}
~Error() {
cout << "~析构函数!" << "id:" << id << endl;
}
Error(const Error& error) {
id = 1;
cout << "拷贝构造函数!" << endl;
}
public:
int id;
};
int func1(int n) {
if (n < 0) {
throw Error();// 匿名对象
//也可以像函数一样这样写:
//Error error;
//throw error;
}
return n;
}
int main(void) {
try {
func1(-10);
// 最佳的方式是使用引用类型捕捉,抛出匿名对象
} catch (Error &error) {
cout << "异常打印:Error" << endl;
}
system("pause");
return 0;
}
运行截图:
异常与继承
异常也是类,我们可以创建自己的异常类,在异常中可以使用(虚函数,派生,引用传递和数据成员等)
案例:设计一个数组类容器 Vector,重载[]操作,数组初始化时,对数组的个数进行有效检查 。
- index<0 抛出异常errNegativeException;
- index = 0 抛出异常 errZeroException;
- index>1000抛出异常errTooBigException;
- index<10 抛出异常errTooSmallException;
- errSizeException类是以上类的父类,实现有参数构造、并定义virtual void printError()输出错误。
代码:
#include <iostream>
#include <Windows.h>
using namespace std;
/*
设计一个数组类容器 Vector,重载[]操作,数组初始化时,对数组的个数进行有效检查
1) index<0 抛出异常errNegativeException
2) index = 0 抛出异常 errZeroException
3)index>1000抛出异常errTooBigException
4)index<10 抛出异常errTooSmallException
5)errSizeException类是以上类的父类,实现有参数构造、并定义virtual void printError()输出错误。
*/
class errSizeException {
public:
errSizeException(int size) {
this->size = size;
}
virtual void printError() { // 多态-->虚函数
cout << "size:" << this->size << endl;
}
protected:
int size;
};
class errNegativeException : public errSizeException {
public:
errNegativeException(int size) : errSizeException(size) {
}
virtual void printError() {
cout << "发生小于零异常错误 size:" << size << endl;
}
};
class errZeroException : public errSizeException {
public:
errZeroException(int size) : errSizeException(size) {
}
virtual void printError() {
cout << "发生等于零异常错误 size:" << size << endl;
}
};
class errTooBigException : public errSizeException {
public:
errTooBigException(int size) : errSizeException(size) {
}
virtual void printError() {
cout << "发生大于一千异常错误 size:" << size << endl;
}
};
class errTooSmallException : public errSizeException {
public:
errTooSmallException(int size) : errSizeException(size) {
}
virtual void printError() {
cout << "发生小于十异常错误 size:" << size << endl;
}
};
class Vector {
public:
Vector(int len) {
if (len < 0) {
throw errNegativeException(len);
} else if (len == 0) {
throw errZeroException(len);
} else if (len > 1000) {
throw errTooBigException(len);
} else if (len < 10) {
throw errTooSmallException(len);
}
this->length = len;
this->m_length = new int[length];
}
~Vector() {
delete[] m_length;
this->m_length = NULL;
this->length = 0;
}
int &operator[](int index) {
return m_length[index];
}
int getLength() const {
return length;
}
private:
int length;
int* m_length;
};
int main(void) {
try {
Vector vector(-10);
for (int i = 0; i < vector.getLength(); i++) {
vector[i] = i + 10;
cout << vector[i] << endl;
}
} catch (errSizeException &err) {
err.printError();
}
/*
catch (errNegativeException &err) {
cout << "发生小于零异常错误..." << endl;
} catch (errZeroException & err) {
cout << "发生等于零异常错误..." << endl;
} catch (errTooBigException & err) {
cout << "发生大于一千异常错误..." << endl;
} catch (errTooSmallException & err) {
cout << "发生小于十异常错误..." << endl;
}
*/
system("pause");
return 0;
}
运行截图:
异常处理的基本思想
C++的异常处理机制使得异常的引发和异常的处理不必在同一个函数中,这样底层的函数可以着重解决具体问题,而不必过多的考虑异常的处理。上层调用者可以再适当的位置设计对不同类型异常的处理。
异常是专门针对抽象编程中的一系列错误进行处理的,C++中不能借助函数机制实现异常,因为栈结构的本质是先进后出,依次访问,无法进行跳跃,但错误处理的特征却是遇到错误信息就想要转到若干级之上进行重新尝试, 如图: