目录
一. C语言处理错误的方法
C语言是面向过程的语言,有以下两种处理错误的方法:
- 终止程序,如通过assert断言强行终止程序运行。缺点:在大型软件项目中,某个部分发生错误就终止整个程序很难别接收,因为可能这个软件的大部分功能可以正常使用,只是局部功能收到了影响。另外assert只在Debug版本下才有用,在Release版本下被忽略。
- 设置全局错误码errno,errno是C语言内置的全局错误码,被包在头文件<errno.h>中。缺点:需要用户自己去根据错误码匹配错误信息,不够直观。
C语言处理错误涉及一个内置全局变量errno及两个库函数strerror和perror。
- strerror函数:以字符串的形式,获取错误码对应错误信息,在头文件<string.h>中。
- perror函数:打印错误信息,可以添加自定义的前置声明。
代码1.1企图用只读的方式打开不存在的文件test.txt,检查文件指针,发现为NULL,然后依次打印错误码、调用strerror和perror打印错误信息,程序运行结果见图1.1。
代码1.1:C语言错误码处理异常
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
printf("errno = %d\n", errno); //打印错误码
printf("%s\n", strerror(errno)); //获取错误码对应错误信息并打印
perror("fopen"); //输出 fopen:错误信息
return 1;
}
fclose(pf);
return 0;
}
二. C++中异常的概念
C++及其它面向对象的语言(如JAVA),对错误的处理方法一般都为抛异常。异常,就是当函数遇到一个其自身无法处理的错误时,将这个错误抛出,由外部的函数进行处理或者直接终止程序。C++处理异常有3个重要关键字:
- try:后跟大括号,大括号内部是可能抛出异常的语句。
- catch:捕获异常,并在其后面的大括号里对异常进行处理。
- throw:抛出特定的异常。
代码2.2定义了一个整形除法运算函数Div,如果除数为0,那么就抛除0异常错误(throw "Divide by 0 error"),通过catch不会Div中的错误,在外部catch抛出的错误,并打印错误信息。
代码2.2:C++抛异常
#include<iostream>
int Div(int a, int b)
{
if (0 == b)
throw "Divide by 0 error";
else
return a / b;
}
int main()
{
try
{
int ret = Div(3, 0); //抛异常:除0错误
}
catch (const char* errmsg)
{
std::cout << errmsg << std::endl;
}
return 0;
}
三. 异常的抛出和捕捉
3.1 异常抛出和捕捉的原则
- 异常都是由对象抛出的,抛出对象的类型,决定了被哪一个catch捕捉。
- 异常对象被调用链中与抛出位置最近的且类型匹配的catch捕捉。调用链是指可理解为函数的嵌套调用顺序,如main函数调用Func1,Func1调用Func2,Func2调用Func3,那么main()->Func1()->Func2()->Func3()就是一条调用链(见图3.1)。
- 当有异常抛出时,先检查throw是否在catch内部,然后沿着调用链,去查找匹配的catch,如果到了main()函数还没有catch被激活,那么程序终止运行。
- 一般而言,不允许因为异常没有被捕获而造成程序终止运行。catch(...)能匹配任意类型的异常,因此一般要求所有的try...catch...体系都要有catch(...)的存在。
- 由于异常对象为临时对象,因此在异常对象抛出后,会生成一份它的拷贝,传递给catch进行捕捉,这类似于函数的值返回/传值调用。
- 在实际的大型工程项目中,经常使用继承体系的对象来捕获异常。抛出派生类作为异常对象,用基类的引用来捕获。
如代码3.1所示,在Func函数中调用Div函数,Div函数会发生除0错误而抛异常,异常对象的类型为char*,在Func和main函数中,都有对char*类型异常的捕获,因为Func离异常抛出的位置更近,所以优先激活Func函数中的catch,运行程序输出:Func() -> Divide by 0 error。
代码3.1:异常捕捉1
int Div(int a, int b)
{
if (0 == b)
throw "Divide by 0 error";
else
return a / b;
}
void Func()
{
try
{
int ret = Div(3, 0);
}
catch(const char* errmsg)
{
std::cout << "Func() -> " << errmsg << std::endl;
}
}
int main()
{
try
{
Func();
}
catch(const char* errmsg)
{
std::cout << "main() -> " << errmsg << std::endl;
}
return 0;
}
如代码3.2所示,在Func函数中被调用的Div函数抛除0异常,在Func函数中的catch只能匹配到int类型的异常对象,无法激活,到了main()函数中,才会有捕捉char*类型的异常对象的catch,运行程序输出:main() -> Divide by 0 error。
代码3.2:异常捕捉2
int Div(int a, int b)
{
if (0 == b)
throw "Divide by 0 error";
else
return a / b;
}
void Func()
{
try
{
int ret = Div(3, 0);
}
catch (int errId)
{
std::cout << "Func() -> " << errId << std::endl;
}
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
std::cout << "main() -> " << errmsg << std::endl;
}
return 0;
}
在代码3.3中,调用链中没有catch能够捕捉Div函数抛出的char*类型的异常对象,因此,程序在抛出异常后会终止执行。
代码3.3:异常捕捉3
int Div(int a, int b)
{
if (0 == b)
throw "Divide by 0 error";
else
return a / b;
}
void Func()
{
try
{
int ret = Div(3, 0);
}
catch (int errId)
{
std::cout << "Func() -> " << errId << std::endl;
}
}
int main()
{
try
{
Func();
}
catch (int errId)
{
std::cout << "Func() -> " << errId << std::endl;
}
return 0;
}
代码3.4中虽然Func函数和main函数都没有直接接收char*类型的catch,但是,在main函数中有catch(...)可以被任意类型的异常对象激活,运行程序输出:未知异常。
代码3.4:异常捕捉4
int Div(int a, int b)
{
if (0 == b)
throw "Divide by 0 error";
else
return a / b;
}
void Func()
{
try
{
int ret = Div(3, 0);
}
catch (int errId)
{
std::cout << "Func() -> " << errId << std::endl;
}
}
int main()
{
try
{
Func();
}
catch (int errId)
{
std::cout << "Func() -> " << errId << std::endl;
}
catch (...)
{
std::cout << "未知异常" << std::endl;
}
return 0;
}
3.2 继承异常体系
C++虽然有内置的标准异常体系(见图3.3),但是,在实际的大型项目中,一般都会自定义一套区别与C++标准的异常体系,通常采用继承的体系,来定义异常对象。一般会以派生类对象作为异常对象抛出,用基类的引用来catch派生类的异常对象。
代码3.5定义了一套服务器异常继承体系,包含一个基类Exception,这个基类中有一个what成员函数,用于获取异常信息。认为服务器可能会抛出网络异常、缓存异常和数据库异常,因此,以Exception类为基类,定义了三个派生类对象:HttpServeException、CacheException和SqlException,分别用于网络异常、缓存异常和数据库出错时抛出异常对象,每个派生类对象重写what()函数,用于获取各自的错误信息,在主函数中,只需要使用基类对象引用Exception接收异常对象即可。
代码3.5:服务器异常继承体系
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
, _id(id)
{}
virtual string what() const
{
return _errmsg;
}
protected:
string _errmsg;
int _id;
};
class SqlException : public Exception
{
public:
SqlException(const string& errmsg, int id, const string& sql)
:Exception(errmsg, id)
, _sql(sql)
{}
virtual string what() const
{
string str = "SqlException:";
str += _errmsg;
str += "->";
str += _sql;
return str;
}
private:
const string _sql;
};
class CacheException : public Exception
{
public:
CacheException(const string& errmsg, int id)
:Exception(errmsg, id)
{}
virtual string what() const
{
string str = "CacheException:";
str += _errmsg;
return str;
}
};
class HttpServerException : public Exception
{
public:
HttpServerException(const string& errmsg, int id, const string& type)
:Exception(errmsg, id)
, _type(type)
{}
virtual string what() const
{
string str = "HttpServerException:";
str += _type;
str += ":";
str += _errmsg;
return str;
}
private:
const string _type;
};
四. 异常规范问题
- 在函数后面接throw(类型1, 类型2, ... ),用于指定函数可以抛出的异常类型。函数后面接throw(),表示函数不会抛出任何异常。
- 如果函数后面没有throw(...)进行声明,则表示函数可以抛出任何类型的异常。
- C++11新增noexcept关键字,用于表示某个函数不会抛异常。
- C++异常规范并不是强制规定,编译器不会对异常规范进行检查,这是由于C++要兼容C语言语法及一些历史坑造成的,声明func throw(A,B,C),我们就会认为func只会抛出ABC三种类型异常对象,但即使抛出D类型异常对象也不会报错,异常规范,非常考验程序员的素养。
void func1() throw(A, B, C); //func1会抛出A、B、C三种类型异常对象
void func2() throw(); //func2不会抛出异常
void func3(); //func3可以抛出各种类型的异常对象
//C++11 noexcept关键字
void func4() noexcept; //func4不会抛异常
五. 异常安全问题
安全隐患1:构造函数和析构函数不要抛出异常
如果构造函数抛出异常,那么有可能出现对象构造不完全的问题,后面使用该对象可能出现不确定的错误。析构函数中不能抛异常,如果析构函数抛出异常,可能会造成资源释放不完全从而导致内存泄漏问题。
如果构造函数和析构函数确实有可能抛出异常,那么就应当在函数内部使用try...catch...处理掉异常,不能让异常离开构造和析构函数。
代码5.1:在析构函数中捕捉异常
class AA
{
public:
close()
{
...
if (...)
{
throw "wrong";
}
}
~AA()
{
try
{
close() //释放资源(可能抛异常)
}
catch (const char* errmsg)
{
... //异常处理
}
catch (...)
{
//未知异常
}
}
};
安全隐患2:C++没有垃圾回收机制,new/delete若抛异常容易引发内存泄漏问题。(应使用智能指针RAII解决)
代码3.2中,如果Func函数中Div抛出了除0异常,并且adelete[] arr1出现了异常,那么程序会直接抛出delete[] arr1产生的异常,而调用func的函数却无法释放arr2的空间,造成arr2的空间没有被释放,引发内存泄漏。
代码3.2:new / delete 异常安全问题
int Div(int a, int b)
{
if (0 == b)
throw "Divide by 0 error";
else
return a / b;
}
void func()
{
int* arr1 = new int[10];
int* arr2 = new int[10];
int a = 0, b = 0;
std::cin >> a >> b;
try
{
int ret = Div(a, b);
}
catch (...)
{
//如果此处抛异常,那么delete[] arr2不会被执行,异常直接被throw抛到func之外
delete[] arr1;
//此处有不被执行导致的内存泄漏风险
delete[] arr2;
throw; //抛出捕获到的异常
}
delete[] arr1;
delete[] arr2;
}
六. 异常的优缺点
异常的优点:
- 方便定位错误和,查看错误信息,还可以包含调用堆栈的信息,方便Bug的修改。
- C语言采用错误码需要层层返回进行处理,无法一次跳过多级调用,我们返回最外层来处理错误码获取错误信息。
- 对于构造函数和析构函数等没有返回值的函数,采用抛异常的方法更加方便。
异常的缺点:
- 异常容易造成执行流乱跳,不便于调试程序。
- 相比于JAVA等主流语言,C++没有垃圾回收机制,抛异常容易引起内存泄漏、锁死等问题。
- C++标准库中异常体系并不优秀,实际项目中多会自定义异常体系,造成混乱。同时,C++异常标准不具有强制性,因此对程序员的素质要求极高,且容易造成混乱。
七. 总结
- C语言一般采用错误码+返回值的方法来标识错误,C++等面向对象的语言则采用抛异常的方法标处理错误。C++通过对象来抛出异常,通过try...catch...来捕获异常并进行相应处理。
- 异常的捕获位置由异常对象的类型确定,会被调用流中离throw最近的且类型匹配的catch捕获,一般需要catch(...)捕获未知类型异常,如果异常到了main()函数还没有被捕获则程序终止。
- 在实际项目中一般采用继承结构来定义异常体系,抛出的异常为派生类对象,使用基类对象的引用进行捕捉。
- C++异常存在安全问题,new/delete体系中抛异常易出现内存泄漏问题,不允许构造函数和析构函数抛出异常,构造函数和析构函数中的异常应当在函数内部进行处理。
- C++有异常规范,但不严格要求,编译器更不会检查。
- 异常的最大优点在于容易定位错误和获取错误信息,最大的缺点在于容易造成执行流乱跳,程序追踪调试困难。