C++之异常
目录
一、异常的概念
异常(错误/bug)在现实项目中并不少见,出现了异常我们程序员也必须去解决。解决代码错误的前提是知晓错误的发生位置与产生原因,C/C++也会在异常发生时给我们相关查看手段。
在C语言中程序的错误通常以错误码的形式体现,但这种方式并不方便观察具体错误信息(因为要自己去查错误码的具体信息)。在C++中,我们可以通过捕获一个错误类来呈现具体的错误信息。
二、异常的抛出与捕获
double Divide(int x, int y)
{
try
{
if (y == 0)
{
string s = "除零错误";
throw s;
}
else
{
return (double)x / y;
}
}
catch (string x)
{
cout << "error:" << x << endl;
}
}
以上面代码为例:
1、程序出现问题时,我们通过抛出(throw) 一个异常对象。C++标准规定这个被throw抛出的异常对象(无论原对象是否为const)具有强制的const属性
2、异常的接收通过捕捉(catch) 实现。
3、抛出(throw)异常后,throw 后面的代码不会执行,程序会跳转到最近的、符合异常类型要求的 catch 语句中执行。catch 语句可能在当前函数内,也可能在调用链的其他函数中。这会导致两个结果:沿着调用链的函数可能提前退出;一旦执行异常处理程序,调用链上创建的所有局部对象都会被销毁(即 “栈展开” 过程)。
4、由于捕捉异常后,调用链上的相关对象会被销毁(如上面代码的局部变量s),因此 throw 抛出的是原对象的一份拷贝。这份拷贝对象会被传递给 catch 语句,在 catch 处理结束后才会被销毁。
static int h = 10;
double Divide(int x, int y)
{
try
{
if (y == 0)
{
string s = "除零错误";
throw s;
h = 20;
return -1;//随便写的
}
else
{
return (double)x / y;
}
}
catch (double x)
{
cout << "error:" << x << endl;
}
catch(int x)
{
cout << "error:" << x << endl;
}
}
int main()
{
int a, b; cin >> a >> b;
try
{
Divide(a, b);
}
catch (string x)
{
cout << x << endl;
}
cout << "h:" << h << endl;
return 0;
}

上面很清楚看到throw后面的代码语句并没有执行下去。并且还不是在原函数catch而是跳出原函数到main函数中捕捉。在跳出的时候调用链上的原函数栈帧会被销毁。
当没有合适的catch捕捉时程序同样会报错终止

三、栈展开
1、抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理。
2、如果当前函数中没有try/catch子句,或者有try/catch子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch过程被称为栈展开。
3、如果到达main函数,依旧没有找到匹配的catch子句,程序会调用标准库的 terminate 函数终止程序。(上面有示例)
4、如果找到匹配的catch子句处理后,catch子句代码(包括后面代码)会继续执行。(异常捕获这种处理方式与报错直接粗暴中断程序很不同)

上方图例中,f3中throw出异常,而这异常的捕获(catch)是在外部f1中实现。上图右侧从throw到catch的过程是栈展开的形象描述,在栈展开的过程里f3、f2都会被销毁。
栈展开避免因程序的某一环节出错使整个程序中断崩溃的极端情况,大大优化了使用舒适度(不会因为小毛病使得程序不可用)、开发效率、程序的稳定可维护性。
四、捕捉(catch)中的类型匹配
一般情况下我们设计的异常捕获都会有类型匹配的catch捕获(不然就报错了),但凡是都有例外:
catch捕获允许从非常量向常量的类型转换,也就是权限缩小;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派生类向基类类型的转换。尤其是最后一个转化十分实用。
class Person
{
public:
Person(int x = 0)
:_a(x)
{ }
virtual void Fun()const
{
cout << "Person" << endl;
}
private:
int _a;
};
class Student :public Person
{
public:
Student(const string& s, int x)
:_name(s)
, Person(x)
{ }
virtual void Fun()const
{
cout << "Student" << endl;
}
private:
string _name;
};
class Staff :public Person
{
public:
Staff(double x, int y)
:_year(x)
, Person(y)
{ }
virtual void Fun()const
{
cout << "Staff" << endl;
}
private:
double _year;
};
int main()
{
Person a(1);
Student b("zhangsan", 2);
Staff c(18.5, 3);
for (int i = 1; i <= 3; i++)
{
try
{
if (i == 1) throw a;
else if ((i == 2)) throw b;
else if (i == 3) throw c;
}
catch (const Person& x)
{
x.Fun();
}
}
return 0;
}

上面代码不仅在捕获(catch)阶段体现派生类向基类的转化,还体现了异常抛出捕获后仍然会继续执行catch后面的代码(注意栈展开)。
下面补充一个小知识,我们分别用linux的g++和vs2022运行下面代码:
class Person
{
public:
Person(int x = 0)
:_a(x)
{
}
virtual void Fun()//const
{
cout << "Person" << endl;
}
private:
int _a;
};
class Student :public Person
{
public:
Student(const string& s, int x)
:_name(s)
, Person(x)
{
}
virtual void Fun()//const
{
cout << "Student" << endl;
}
private:
string _name;
};
class Staff :public Person
{
public:
Staff(double x, int y)
:_year(x)
, Person(y)
{
}
virtual void Fun()//const
{
cout << "Staff" << endl;
}
private:
double _year;
};
int main()
{
Person a(1);
Student b("zhangsan", 2);
Staff c(18.5, 3);
for (int i = 1; i <= 3; i++)
{
try
{
if (i == 1) throw a;
else if ((i == 2)) throw b;
else if (i == 3) throw c;
}
catch ( Person& x)
{
x.Fun();
}
}
return 0;
}


不是说C++标准throw抛出的是const类型错误对象吗,怎么上面的代码还能在g++、vs上编译?
这是编译器的 “非标准扩展”:允许非const引用捕获const异常对象.
根据 C++ 标准,throw抛出的异常对象强制为const类型(目的是防止catch块修改异常状态)。此时,catch(Person&x)用非const引用捕获const异常对象,理论上属于 “权限放大”,标准不允许。
但 GCC、VS 等编译器存在 “非标准扩展”:为了兼容旧代码或简化使用,会放宽对 “异常对象const属性” 的检查,允许非const引用直接绑定const异常对象。
虽然编译器会在这放宽要求,但依旧建议使用const写法!
既然谈到了编译器,我们还需要知道目前主流的编译器都会对throw与catch步骤做出优化:比如throw、catch的过程中不创建临时对象而是直接构造以减少拷贝。我们一般不需要关心其中的效率问题。
好了,假如我们抛出异常后到了main函数中依旧查找不到合适的捕捉语句,我们可以在main函数中最后都会使用catch(...),它可以捕获任意类型的异常,但是不知道异常错误是什么。
nt main()
{
int a = 1;
try
{
if (a == 1)
throw int(10);//例子随便举的
}
catch(const string&s)
{
cout << s << endl;
}
catch (...)
{
cout << "error" << endl;
}
return 0;
}

五、异常安全问题
异常抛出后,后面的代码就不再行(指throw后面那部分),前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。
中间我们需要捕获异常,释放资源后面再重新抛出。
下面就是捕获到无法解析的异常(catch(…)捕获),就需要往上层(这里是main函数)重新抛出异常捕获具体的错误信息。
void func()
{
int* p = new int(10); // 1. 分配堆内存(资源)
try
{
throw runtime_error("出错了"); // 2. 抛异常(模拟错误)
}
catch (...)
{
delete p; // 3. 捕获后,先释放资源(避免泄漏)
cout << "已释放内存,再抛异常给上层" << endl;
throw; // 4. 重新抛出异常
}
}
int main()
{
try
{
func();
}
catch (const exception& e)
{
cout << "上层捕获到:" << e.what() << endl;
}
return 0;
}

当然使用RAII思想来解决这个问题更好。(智能指针那里介绍)
其次析构函数中,如果抛出异常也要谨慎处理,比如析构函数要释放10个资源,释放到第5个时抛出异常,则也需要捕获处理,否则后面的5个资源就没释放,也资源泄漏了。所以别让异常逃离析构函数
class ResourceManager
{
private:
int* resources[10];
public:
ResourceManager()
{
for (int i = 0; i < 10; ++i)
{
resources[i] = new int(i); // 资源编号0~9
cout << "创建资源 " << i << "(地址:" << resources[i] << ")" << endl;
}
}
// 析构函数(重点关注)
~ResourceManager()
{
cout << "\n开始释放资源:" << endl;
// 方式1:不处理异常(错误示范)
// for (int i = 0; i < 10; ++i)
// {
// if (i == 5) { // 模拟释放第5个资源时抛异常
// throw runtime_error("释放资源5时失败!");
// }
// delete resources[i];
// cout << "已释放资源 " << i << endl;
// }
// 方式2:捕获异常并处理(正确示范)
for (int i = 0; i < 10; ++i)
{
try
{
if (i == 5)
{ // 模拟释放第5个资源时抛异常
throw runtime_error("释放资源5时失败!");
}
delete resources[i];
cout << "已释放资源 " << i << endl;
}
catch (const exception& e)
{
// 捕获异常后,记录错误但继续释放后续资源
cerr << "处理资源" << i << "时出错:"
<< e.what() << "(继续释放后续资源)" << endl;
// 注意:此处不重新抛出,避免异常逃离析构函数
}
}
cout << "所有资源释放流程结束" << endl;
}
};
int main()
{
try
{
ResourceManager rm; // 创建对象,自动初始化10个资源
// 假设业务逻辑正常执行,对象销毁时调用析构函数
}
catch (const exception& e)
{
cout << "\n上层捕获到异常:" << e.what() << endl;
}
return 0;
}
六、异常规范


noexcept 修饰函数的核心作用是保证“异常不会逃离该函数”,而非“禁止函数体内部产生或捕获异常”。
标准库中的异常:https://legacy.cplusplus.com/reference/exception/exception/

#include <iostream>
#include <stdexcept> // 包含标准异常
using namespace std;
// 计算正数的平方(参数必须为正数)
int square(int n)
{
if (n < 0)
{
// 抛出“无效参数”异常,附带错误信息
throw invalid_argument("参数必须为正数(square函数)");
}
return n * n;
}
int main()
{
try
{
cout << square(-5) << endl; // 传入无效参数
}
catch (const exception& e)
{
// 捕获所有标准异常
cout << "错误:" << e.what() << endl; // 输出错误信息
}
return 0;
}
抛出异常时用专门的类,接受时用exception就可以了。
当使用万能捕获(catch(...))捕获不确定异常时可用throw;(万能捕获)再次抛出,外部用const exception&二次接收并结合what接口分析具体错误信息。
更多推荐

所有评论(0)