1️⃣ assert处理错误:直接终止程序,但是一旦出问题就直接把程序终止掉,方法过于暴力。而且release版本不起作用。
2️⃣ 返回错误码:用errno表示错误,但需要程序员自己查找对应的错误。
而C++引入异常的概念来处理错误。
当有一个函数发现了自己无法处理的错误时就可以抛出异常,这样就可以让该函数或者函数的调用者直接处理错误。
使用:
throw
: 当问题出现时,程序会抛出一个异常(是一个对象)。这是通过使用 throw 关键字来完成的。
catch
: 在您想要处理问题的地方,通过异常处理程序捕获异常,catch 关键字用于捕获异常,可以有多个catch进行捕获。
try
: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个catch 块。
double Div(double left, double right)
{
if (right == 0)
{
throw "除0错误!";
}
else
{
return left / right;
}
}
void fun()
{
double left, right;
cin >> left >> right;
cout << Div(left, right) << endl;
}
int main()
{
try
{
fun();
}
catch (const char* errstr)
{
cout << errstr << endl;
}
return 0;
}
如果没有发生异常就会走正常逻辑,不会走catch,而一旦出现异常(throw后),直接跳到catch。
异常的抛出和匹配原则:
1️⃣ 异常是通过throw引发的,可能有多个catch,throw对象的类型决定了使用哪个catch来处理异常。进入一个catch后就不会再进入其他的catch,如果都不匹配直接终止程序报错。
2️⃣ 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
double Div(double left, double right)
{
if (right == 0)
{
throw "除0错误!";
}
else
{
return left / right;
}
}
void fun()
{
try
{
double left, right;
cin >> left >> right;
cout << Div(left, right) << endl;
}
catch (const char* errstr)
{
cout << errstr << endl;
}
cout << "*********************" << endl;
}
int main()
{
try
{
fun();
}
catch (const char* errstr)
{
cout << errstr << endl;
}
return 0;
}
这里的作用主要只为解决可能发生的内存泄漏问题:在里面new了一块空间,还没来得及销毁就异常出去了。
void fun()
{
int* p = new int[10];
try
{
double left, right;
cin >> left >> right;
cout << Div(left, right) << endl;
}
catch (const char* errstr)
{
cout << errstr << endl;
}
catch (...)
{
cout << "未知异常!" << endl;
}
cout << "--delete--" << endl;
delete[]p;
}
3️⃣ catch(…)可以捕获任意类型的异常,问题是不知道异常错误是什么。这样就解决了1️⃣都不匹配的问题。
try
{
fun();
}
catch (const char* errstr)
{
cout << errstr << endl;
}
catch (...)
{
cout << "未知异常!" << endl;
}
4️⃣ 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类于函数的传值返回)
5️⃣ 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获。在后面会详细讲述。
当我们为了防止内存泄漏写了多个捕获异常函数,但是我们想要在最外边一层捕获怎么办?
void fun()
{
int* p = new int[10];
try
{
double left, right;
cin >> left >> right;
cout << Div(left, right) << endl;
}
catch (const char* errstr)
{
cout << "--delete--" << endl;
delete[]p;
// 重新抛出
throw errstr;
}
catch (...)
{
cout << "未知异常!" << endl;
}
}
一个项目中如果大家随意抛异常,那么外层的调用者就会相当难受,为了解决这个问题,引入了异常继承体系这个概念。接下来我们可以模拟实现一下。
class Exception
{
public:
Exception(const std::string& str, int id)
: _errstr(str)
, _id(id)
{}
// 获取异常信息
virtual std::string what() const
{
return _errstr;
}
protected:
std::string _errstr;
int _id;
};
// A错误
class AException : public Exception
{
public:
AException(const std::string& str, int id, const std::string& A)
: Exception(str, id)
, _A(A)// const成员变量只能走初始化列表
{}
virtual std::string what() const
{
std::string str("A Exception:");
str += _errstr;
str += "->";
str += _A;
return str;
}
private:
const std::string _A;
};
// B异常
class BException : public Exception
{
public:
BException(const std::string& str, int id)
: Exception(str, id)
{}
virtual std::string what() const
{
std::string str("B Exception:");
str += _errstr;
return str;
}
private:
};
// C异常
class CException : public Exception
{
public:
CException(const std::string& str, int id, const std::string& C)
: Exception(str, id)
, _C(C)// const成员变量只能走初始化列表
{}
virtual std::string what() const
{
std::string str("C Exception:");
str += _errstr;
str += "->";
str += _C;
return str;
}
private:
const std::string _C;
};
void A()
{
srand(time(0));
if (rand() % 7 == 0)
{
throw AException("权限不足", 100, "A出问题");
}
cout << "调用成功" << endl;
}
void B()
{
srand(time(0));
if (rand() % 5 == 0)
{
throw BException("权限不足", 100);
}
else if (rand() % 6 == 0)
{
throw BException("数据不存在", 101);
}
A();
}
void C()
{
srand(time(0));
if (rand() % 3 == 0)
{
throw CException("请求资源不存在", 100, "C出问题");
}
else if (rand() % 4 == 0)
{
throw CException("权限不足", 101, "C出问题");
}
B();
}
int main()
{
while (1)
{
Sleep(500);
try {
C();
}
catch (const Exception& e) // 这里捕获父类对象就可以
{
// 多态
cout << e.what() << endl;
}
catch (...)
{
cout << "未知错误!" << endl;
}
}
return 0;
}
我们使用取模操作来模拟发生的异常,主函数调用C()
,C()
没出问题调用B()
,B()
没出问题调用A()
。A()
没有问题就调用成功。这样只用基类进行捕获,就可以捕获到不同类型的异常。
1️⃣ 不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化。
2️⃣ 不要在析构函数内抛出异常,否则可能导致资源泄漏。
3️⃣ C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题。
首先要知道这里只是规范,并没有强制。
1️⃣ 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。
2️⃣ 函数的后面接throw(),表示函数不抛异常。
3️⃣ 若无异常接口声明,则此函数可以抛掷任何类型的异常
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread(thread&& x) noexcept;
异常的优点:
1️⃣ 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
2️⃣ 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误。而异常就直接会出去走catch。
3️⃣ 很多第三方库都包含异常,如果我们想使用也得用异常。
4️⃣ 对于一些没有返回值的函数,出了问题也可以获取异常信息。
异常的缺点:
1️⃣ 异常会导致执行流乱跳,调试起来困难。
2️⃣ 异常会有一些性能消耗,但可以忽略不计。
3️⃣ 在C++中没有垃圾回收机制,有了异常后容易导致内存泄漏和死锁。
4️⃣ C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
5️⃣ 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:① 抛出异常类型都继承自一个基类。② 函数是否抛异常、抛什么异常,都使用func()
noexcpt()
;的方式规范化。