目录
抛出的异常类型大致可以分为三种。
第一种 基本类型
1. 可以直接抛出常量
2. 也可以抛出定义好的变量
3. 如果我们使用const,会不会影响到异常的匹配。
第二种 字符串类型以及指针类型
1. 使用字符指针
注意:
2. 使用string类型
第三种 自定义类型(类类型)
注意事项:
catch是根据我们抛出的异常信息类型来捕获的。
抛出的异常类型大致可以分为三种。
第一种 基本类型
int, char, float,double等类型。
以抛出int类型的数据为例
1. 可以直接抛出常量
void func1() {throw - 1;printf("func1");
}int main(void) {try {func1();}catch (int error) {printf("异常处理 %d\n",error);}system("pause");return 0;
}
2. 也可以抛出定义好的变量
void func1() {int err = -1; // 定义变量throw err;printf("func1");
}int main(void) {try {func1();}catch (int error) {printf("异常处理 %d\n",error);}system("pause");return 0;
}
3. 如果我们使用const,会不会影响到异常的匹配。
我们对上面的代码进行修改, 我们在func1定义err时和catch参数中两个地方加上const,或者一个加一个不加。执行代码,会发现依然能匹配成功。所以对于普通类型的数据,有没有const都不会影响到异常的匹配的。
第二种 字符串类型以及指针类型
首先C语言的字符串类型是字符指针,c++的字符串类型string(当然c++中包含C语言指针)。
1. 使用字符指针
第一种: 直接抛出字符串常量
对于字符串常量,我们直接使用非const指针指向它是不安全的,但是有的编译器允许这么做。所以有的编译器char*类型也可以与字符串常量匹配,但是有的编译器认为字符串常量必须使用const的字符指针指向才行,所以只能与const char*匹配。
void func1() {throw "异常";printf("func1");
}int main(void) {try {func1();}catch (const char* error) {printf("异常处理 %s\n",error);}system("pause");return 0;
}
注意:
虽然在c++中我们可以使用string定义字符串,但是string只是c++封装的一个类型。字符串常量或者字符指针,本身表示的是一个地址,所以它是没有办法与string类型匹配成功的,需要使用字符指针。
第二种: 使用字符指针指向或者字符数组
字符数组:
void func1() {char arr[] = "异常";throw arr;printf("func1");
}int main(void) {try {func1();}catch (const char* error) {printf("异常处理 %s\n",error);}system("pause");return 0;
}
字符指针:
void func1() {char* arr = (char*)"异常";throw arr;printf("func1");
}int main(void) {try {func1();}catch (char* error) {printf("异常处理 %s\n",error);}system("pause");return 0;
}
无论是字符数组还是字符指针:
1. 如果我们抛出的是const 修饰的,那么只能和catch中const修饰的char*匹配。
2. 如果我们抛出的是非const修饰的,那么catch中用不用const修饰都可以匹配成功。
3. 其实和赋值时,const修饰的不能赋值给非const修饰的,非const修饰的可以赋值给const修饰的是一个道理的。
4. 当然如果const在*后面修饰,那么就不会影响匹配。char*const可以和char*匹配成功。
2. 使用string类型
string类型其实和普通类型差不多,string*和char*也类似。但是string类型和char*类型是无法匹配的,虽然它们都可以表示字符串,但是是不同的类型。
第三种 自定义类型(类类型)
我们可以将相应的异常封装成一个类,类中封装一些方法,在出现这类异常之后, 可以抛出一个此类的对象。
看下面这段代码,将打开文件异常和写入文件异常封装成两个类,然后抛出它们的对象。
#define BUFFER_SIZE 1024class OpenFileError {
public:OpenFileError(int err) :errorData(err) {};void print() {switch (errorData) {case -1:printf("源文件打开失败 %d\n", errorData);break;case -2:printf("目的文件打开失败 %d\n", errorData);break;}}
private:int errorData;
};class WriteFileError {
public:void print() {printf("文件写入失败");}
};// 将一个文件中的内容拷贝到另外一个文件中去
int makeFile(const char* dest, const char* src) {// 定义文件指针FILE* fp1 = NULL, * fp2 = NULL;// 打开文件, 以只读二进制形式打开文件,打开失败返回NULLfp1 = fopen(src, "rb"); // 判断文件是否成功打开if (!fp1) {throw OpenFileError(-1);}// 打开文件,以只写二进制形式打开文件,打开失败返回NULLfp2 = fopen(dest, "wb");// 判断文件是否成功打开if (!fp2) {throw OpenFileError(-2); // 返回错误标记,表示目标文件打开失败}// 进行文件的拷贝char buffer[BUFFER_SIZE]; // 1024字节的缓存int readLen, writeLen; // 每次读取的长度和写入的长度// 读取的长度大于0,说明有内容可以写入,执行循环体的写入内容while ((readLen = fread(buffer, 1, BUFFER_SIZE, fp1)) > 0) {writeLen = fwrite(buffer, 1, readLen, fp2);// 如果一次写入的长度和读取的长度不等,那么说明写入失败if (readLen != writeLen) {throw WriteFileError(); }}// 关闭文件fclose(fp1);fclose(fp2);return 0; // 一切正常返回0
}int makeFile2(const char* dest, const char* src) {int ret;ret = makeFile(dest, src);printf("makeFile2 函数被调用");return ret;
}int main(void) {int ret = 0;try {ret = makeFile2("dest.txt", "src.txt");}catch (OpenFileError& error) {error.print();}catch (WriteFileError& error) {error.print();}system("pause");return 0;
}
其实抛出类对象的写法不止一种,但是我们为什么选择使用上面的方式呢?
我们使用下面的代码进行说明。
class Error {
public:Error(int err) :errorData(err) {cout << "构造函数" << errorData << endl;};~Error() {cout << "析构函数" << errorData << endl;};Error(const Error& error) {errorData = error.errorData;cout << "拷贝构造函数"<< errorData << endl;};void print() {printf("异常:%d", errorData);}
public:int errorData;
};void func1() {Error err(-1);throw err;printf("func1");
}int main(void) {try {func1();}catch (Error error) {error.errorData = 10;printf("异常处理 %d\n",error);}system("pause");return 0;
}
运行结果:
分析:
上面代码,我们抛出异常时是抛出的定义的对象,在catch接收的时候也是直接使用的普通参数形式(Error error)。 我们使用类对象的构造,析构,拷贝来观察抛出的过程。
运行结果:
第一个构造函数是用来构造我们的func1函数中的error对象的。
第一个拷贝构造函数是在抛出的时候,编译器会根据我们抛出的error对象,创建一个匿名对象进行抛出,所以会调用一次拷贝构造函数。
第二个拷贝构造函数是我们抛出的匿名对象,在与catch中的参数Error error配对之后,直接将匿名对象在error初始化时赋值给它。
第一个析构函数是我们抛出创建的匿名对象后,func1函数就执行结束了,error是其内部的局部变量,就会被销毁,所以这个析构函数是用来销毁func1中的error对象的。
异常处理 10是我们配对成功,之后执行的异常处理代码。
析构函数 10是我们在和catch匹配的时候,根据其参数创建了对象error,其作用域就是这个catch开始到结束,catch的代码执行完,结束的时候,这个对象的生命周期也就结束了,所以调用析构函数
析构函数 -1是用来销毁系统抛出的匿名对象,而调用的构造函数。
说明:
最后两个析构函数我们怎么能确定是来销毁哪个对象的呢?在代码中我们在对应的构造函数中打印了数据errorData的值,我们在func1中创建对象时将其初始化为-1,然后编译器会将其拷贝给匿名对象,匿名对象内的属性值我们无法处理,但是在catch接收匿名对象的时候,定义了另外一个对象error,我们将这个对象的值显示修改为10。所以,析构函数 10就是用来析构它的。
对比:
我们将第二段代码进行修改 -- 在抛出时,使用匿名对象,接收是使用引用
class Error {
public:Error(int err) :errorData(err) {cout << "构造函数" << errorData << endl;};~Error() {cout << "析构函数" << errorData << endl;};Error(const Error& error) {errorData = error.errorData;cout << "拷贝构造函数"<< errorData << endl;};void print() {printf("异常:%d", errorData);}
public:int errorData;
};void func1() {throw Error(-1);printf("func1");
}int main(void) {try {func1();}catch (Error& error) {error.errorData = 10;printf("异常处理 %d\n",error);}system("pause");return 0;
}
运行结果:
分析:
首先第一眼看,这个运行的效率就比前面的效率高很多。 就是在抛出类对象的时候,直接抛出匿名对象,在catch的参数中写使用类的引用接收。
运行结果:
构造函数 -1: 创建匿名对象并抛出。
异常处理 10: 匹配成功,执行异常处理代码。
析构函数 10: 调用析构函数,销毁匿名对象。
说明:
为什么构造函数和析构函数打印出来的errorData不一样?因为我们在构造匿名函数时将errorData初始化为-1,但是在catch捕获异常的时候,我们将其修改为了10。因为我们catch中使用的是引用,所以error还是表示哪个匿名对象,所以在析构时errorData变为了10。
那么为什么可以使用引用来接收函数抛出的匿名对象呢?
因为,我们在第二段代码中知道,在异常机制中,函数抛出的匿名对象析构要在catch中创建的对象析构之后。所以,我们使用引用来接收函数返回的匿名对象之后,在catch语句结束时,是引用的量先被释放,匿名对象后被释放。这样就不会存在引用指向局部变量的问题了。
总结:
综上所述,我们在使用自定义类抛出异常的时候,应该直接抛出其匿名对象,并且使用引用来接收。这样就会少调用几次构造函数和析构函数,那么就大大提高了效率。
注意事项:
1. 上面说到,catch的参数中对象的引用和对象的定义都可以与抛出的匿名对象进行匹配,那么如果同时存在两个catch,参数分别为这两种,那么发生什么?
会出错,因为两个都能匹配,编译器区分不了,自然就报错了。
2. 1.中的情况,不仅是类类型,对于普通类型和字符串类型也是一样的。
3. 上面说到,普通类型,catch中参数加const和不加const都能匹配,所以两者都存在的话,编译器也无法区分,会报错。 但是对于指针就不会报错。
4. 其它情况也还类似。