C++学习笔记之异常处理
C++学习笔记之异常处理
目录
六、异常情况处理
6.1 异常处理的任务
6.2 异常处理的方法
6.3 在函数声明中进行异常情况处指定
6.3.1 异常规范的语法形式
6.3.2 具体用法示例
6.3.3 异常规范的作用与注意事项
6.3.4 现代 C++ 中的趋势
6.4 在异常处理中处理析构函数
6.4.1 析构函数的核心原则:绝对不能抛出异常
6.4.2 异常发生时,析构函数的调用规则
6.4.3 处理资源的最佳实践:RAII 模式
六、异常情况处理
6.1 异常处理的任务
程序中常见的错误有两大类:语法错误和运行错误
语法错误(编译错误):在编译时,编译系统能发现程序中的语法错误(如关键字拼写错误,变量名未定义,语句末尾无分号,括号比匹配等)。错误发生在编译阶段。
运行错误: 程序能正常通过编译,也能投入运行。但是在运行过程中会出现异常,得不到正确的运行结果,甚至导致程序不正常种植,或出现死机现象,如:
在计算过程中出现分母为0的情况。
内存空间不够,无法实现指定的操作。
无法打开输入文件,因而无法读取数据。
输入数据时数据类型有错。
在运行没有异常处理机制的程序时,如果运行情况出现异常,由于程序本身不能处理,程序只能终止运行。如果在程序中设置了异常处理机制,若在运行时出现异常,由于程序本身已规定了处理的方法,程序的流程就会转到异常处理的代码去处理。用户可以事先指定应进行的处理。
一般情况下,异常指的是出错(差错),但是异常处理并不完全等同于出错的处理。只要出现与人们期望的情况不同,都可以认为是异常,并对它进行异常处理。
所谓异常处理指的是对运行时出现差错以及其他例外情况的处理。
6.2 异常处理的方法
如果在执行一个函数过程中出现异常,可以不在本函数中立即处理,而是发出一个信息,传给它的上一级(即调用它的函数),它的上级捕捉到这个信息后进行处理。
如果上一级的函数越不能处理,就传给其上一级,由其上一级处理。如此逐级上送,如果到最上一级还无法处理,最后只好异常终止程序的执行。
C++处理异常的机制由3个部分组成:检查(try)、抛出(throw)和捕捉(catch)。把需要检查的语句放到try块中,throw用来当出现异常时发出一个异常信息(形象地称为抛出,throw的意思时抛出),而catch则用来捕捉异常信息,如果捕捉到了异常信息,就处理它。
示例:给三角形的三边a,b,c,求三角形的面积。只有a+b>c,b+c>a,c+a>b时才能构成三角形。设置异常处理,对不符合三角型的输出警告信息,不予计算。
#include<iostream> // 包含输入输出流库,用于控制台输入输出
#include<cmath> // 包含数学函数库,用于sqrt(平方根)等数学运算
using namespace std; // 使用标准命名空间,简化代码书写(无需前缀std::)
int main()
{// 声明函数原型:声明一个名为triangle的函数,接收三个double类型参数,返回double类型double triangle(double,double,double);double a,b,c; // 定义三个double类型变量,用于存储三角形的三条边长cin>>a>>b>>c; // 从控制台读取第一组三角形的三条边长// try块:包含可能抛出异常的代码,用于捕获并处理异常try{// 循环条件:当三条边都为正数时,持续处理(非正数则结束循环)while(a>0&&b>0&&c>0){// 调用triangle函数计算面积,并将结果输出到控制台cout<<triangle(a,b,c)<<endl;// 读取下一组三角形的三条边长,为下一次循环做准备cin>>a>>b>>c;}}// catch块:捕获由triangle函数抛出的double类型异常(用于处理非三角形的情况)catch(double){// 输出错误信息,提示当前输入的三边不能构成三角形cout<<"a="<<a<<",b="<<b<<",c="<<c<<",that is not a triangle!"<<endl;}cout<<"end"<<endl; // 输出程序结束标志return 0; // 主函数返回0,表示程序正常结束
}
// 定义triangle函数:根据三条边长,使用海伦公式计算三角形面积
// 参数:a,b,c 分别为三角形的三条边长
// 返回值:三角形的面积(double类型)
// 异常:当三边不能构成三角形时,抛出double类型异常
double triangle(double a,double b,double c)
{double s=(a+b+c)/2; // 计算三角形的半周长(海伦公式中的s)// 判断三边是否能构成三角形:任意两边之和必须大于第三边// 若不满足条件,则抛出异常(此处用a作为异常值,仅作标识作用)if(a+b<=c||b+c<a||a+c<b)throw a;// 使用海伦公式计算面积:面积 = √[s(s-a)(s-b)(s-c)]return sqrt(s*(s-a)*(s-b)*(s-c));
}
运行结果
由于catch
子句是用来处理处理异常信息的,往往被称为catch异常处理块或catch异常处理器。
异常处理的语法如下:
throw语句的形式
throw 表达式;
try-catch的结构为
try{被检查的语句}
catch{异常信息类型[变量名]}{进行异常处理的语句}
说明:
被检查的各部分必须放在try块中,否则不起作用。
try块和catch块作为一个整体出现,catch块是try-catch结构中的一部分,必须紧跟在try块之后,不能单独使用,在二者之间也不能插入其他语句。但是在一个try-catch结构中,可以只有try块而无catch块。即只检查而不处理,把catch处理快放在其他函数中。
try和catch块中必须有用花括号包起来的复合语句,即使花括号内只有一个语句,也不能省略花括号。
一个try-catch结构中只能有一个try块,但却可以有多个catch块,以便与不同的异常信息匹配。
catch后面的圆括号中,一般只写异常信息的类型名,如: catch(double)
catch只检查所捕获异常信息的类型,而不检查它们的值,例如a,b,c都是double类型,虽然它们的值不同,但在throw语句中写throw a,throw b或throw c,作用均相同。因此如果需要检测多个不同的异常信息,应当由throw抛出不同类型的异常信息。
异常信息可以是C++系统预定义的标准类型,也可以是用户自定义的类型(如结构体或类)。如果由throw抛出的信息属于该类型或其子类型,则catch与throw二者匹配,catch捕获该异常信息。
catch还可以有另外一种写法,即除了指定类型名外,还指定变量名,如 catch(double d)
此时如果throw抛出的异常信息是double型的变量a,则catch在捕获异常信息a的同时,还使得d获得a的值,或者说d得到a的一个拷贝。
如果在catch子句中没有指定异常信息的类型,而用了省略号“...”。则表示它可以捕捉任何类型时的异常信息。 这种catch子句应放在try-catch结构中的最后,相当于“其它”。如果把它作为第一个catch子句,则后面的catch子句都不起作用。
try-catch结构可以与throw出现在同一个函数中,也可以不在同一个函数中。 当throw抛出异常信息后,首先在本函数中找寻与之匹配的catch,如果在本层无try-catch结构或找不到与之匹配的catch,就转到其上一层去处理,如果其上一层无try-catch结构或找不到与之匹配的catch,则再转到更上一层的try-catch结构上去处理,也就是说转到离开出现异常最近的try-catch结构去处理。
在某些情况下,在throw语句中可以不包括表达式,如果在catch块中包含throw: catch(int){ //其他语句 throw; //将已捕获的异常信息再次原样抛出 } 表示“我不处理这个异常,请上级处理”。此时catch块把当前的异常信息再次抛出,给其上一层的catch块处理。
如果throw抛出的异常信息找不到与之匹配的catch块,那么系统就会调用一个系统函数terminate,使程序终止运行。
示例:在函数嵌套的情况下检测异常处理
#include<iostream>
using namespace std;
// 函数声明移至全局范围,符合编码规范
void f1();
void f2();
void f3();
int main()
{try{// 调用f1()函数,可能会抛出异常f1();}// 捕获double类型的异常catch(double){cout << "ERROR0!" << endl; // 处理main函数层级的异常}return 0;
}
// 第一层函数f1()
void f1()
{try{// 调用f2()函数,可能会抛出异常f2();}// 捕获char类型的异常(本示例中不会触发)catch(char){cout << "ERROR1!"; // 处理f1函数层级的异常}cout << "end1" << endl; // f1()函数执行结束标识
}
// 第二层函数f2()
void f2()
{try{// 调用f3()函数,可能会抛出异常f3();}// 捕获int类型的异常(本示例中不会触发)catch(int){cout << "ERROR2!" << endl; // 处理f2函数层级的异常}cout << "end2" << endl; // f2()函数执行结束标识
}
// 第三层函数f3()
void f3()
{double a = 0; // 定义double类型变量atry{// 抛出double类型的异常throw a;}// 修正:原代码使用catch(float)无法匹配double类型异常// 改为catch(double)以正确捕获抛出的异常catch(double){cout << "ERROR3!" << endl; // 处理f3函数层级的异常}cout << "end3" << endl; // f3()函数执行结束标识
}
异常处理示意图
6.3 在函数声明中进行异常情况处指定
在 C++ 中,函数声明时可以指定该函数可能抛出的异常类型,这一机制称为异常规范(Exception Specification)。它的作用是明确告诉函数调用者:该函数可能会抛出哪些类型的异常,帮助开发者能更清晰地处理潜在错误。
6.3.1 异常规范的语法形式
异常规范通过函数声明后的 throw(类型列表)
来指定,格式如下:
返回值类型 函数名(参数列表) throw(异常类型1, 异常类型2, ...);
-
throw(类型列表)
表示该函数只能抛出列表中指定类型的异常 -
若函数声明为
throw()
(空列表),表示该函数不会抛出任何异常 -
C++11 及以上版本引入
noexcept
关键字,作为throw()
的更简洁替代(推荐使用)
6.3.2 具体用法示例
(1)指定可能抛出的异常类型
// 声明:该函数可能抛出int或double类型的异常
void func1() throw(int, double);
// 定义(需与声明的异常规范一致)
void func1() throw(int, double) {if (/* 某种条件 */) {throw 100; // 允许:int类型在列表中}if (/* 另一种条件 */) {throw 3.14; // 允许:double类型在列表中}// throw "error"; // 错误:const char*不在列表中,会导致程序终止
}
(2)声明不抛出任何异常
// 方式1:C++98风格(throw空列表)
void func2() throw();
// 方式2:C++11风格(推荐,更清晰)
void func3() noexcept;
// 定义
void func2() throw() {// throw 1; // 错误:声明不抛异常却抛出,会导致程序终止
}
6.3.3 异常规范的作用与注意事项
(1)主要作用
-
增强代码可读性:调用者能快速了解函数可能抛出的异常,提前做好捕获准备
-
编译器检查:若函数抛出了规范外的异常,编译器可能会报错(或导致程序异常终止)
-
文档化作用:作为函数接口的一部分,明确错误处理责任
(2)注意事项
-
异常规范必须严格遵守:若函数实际抛出了规范外的异常,C++ 会调用
std::unexpected()
函数,默认导致程序终止 -
派生类重写函数的异常规范:必须不超过基类函数的异常范围(更严格),例如:
class Base { public:virtual void f() throw(int); // 基类允许抛出int }; class Derived : public Base { public:// 正确:派生类异常范围更小(只抛double,或不抛)void f() throw() override;// 错误:派生类抛出了基类未允许的double// void f() throw(int, double) override; };
-
C++11 后推荐使用
noexcept
:noexcept
比throw()
更高效(编译器可优化),且语义更明确。noexcept(true)
等价于throw()
,noexcept(false)
表示可能抛出任何异常(默认)
6.3.4 现代 C++ 中的趋势
-
C++11 及以上版本逐渐弱化
throw(类型列表)
形式,更推荐:-
用
noexcept
明确标识不抛异常的函数(如析构函数、工具函数) -
对于可能抛异常的函数,通常不写异常规范,而是通过文档说明(因为过度严格的规范会降低代码灵活性)
-
-
标准库中,大部分函数不使用异常规范,仅对绝对不抛异常的函数标记
noexcept
(如移动构造函数)
总结
函数声明中的异常规范是 C++ 异常处理机制的补充,其核心价值在于明确函数的异常行为。实际开发中,应根据场景选择合适的形式:
-
对于不抛异常的函数,优先用
noexcept
标记 -
避免过度使用
throw(类型列表)
(维护成本高) -
派生类重写函数时,注意异常规范的兼容性
6.4 在异常处理中处理析构函数
在 C++ 异常处理中,析构函数的行为非常关键,因为它负责释放对象所占用的资源(如内存、文件句柄、网络连接等)。异常发生时,若析构函数处理不当,可能导致资源泄漏或程序崩溃。以下是关于析构函数与异常处理的核心原则和实践方法:
6.4.1 析构函数的核心原则:绝对不能抛出异常
原因:
当异常正在传播(即已有一个未处理的异常)时,若析构函数又抛出新的异常,C++ 会直接调用 std::terminate()
终止程序,无法继续执行任何清理操作。
例如,以下代码会导致程序终止:
class Resource {
public:~Resource() {// 错误示例:析构函数抛出异常throw "Error in destructor"; }
};
int main() {try {Resource res;throw "Error in main"; // 抛出第一个异常} catch (...) {// 永远无法执行到这里,因为析构函数抛出了第二个异常}return 0;
}
正确做法:
析构函数内部必须捕获并处理所有可能的异常,确保不向外传播任何异常。
class Resource {
public:~Resource() {try {// 可能抛出异常的操作(如关闭文件)closeFile(); } catch (...) {// 内部处理异常(如记录日志),不向外抛出logError("Failed to close file");}}
};
6.4.2 异常发生时,析构函数的调用规则
当异常在对象生命周期内抛出时,C++ 会自动调用所有已构造完成的对象的析构函数(即使异常未被捕获),这一机制称为 “栈展开(Stack Unwinding)”。
示例:栈展开过程中的析构函数调用
class MyClass {int id;
public:MyClass(int i) : id(i) { cout << "Construct " << id << endl; }~MyClass() { cout << "Destruct " << id << endl; } // 安全的析构函数
};
void func() {MyClass a(1);MyClass b(2);throw "Exception in func"; // 抛出异常MyClass c(3); // 永远不会构造,因此也不会析构
}
int main() {try {func();} catch (...) {cout << "Caught exception" << endl;}return 0;
}
输出结果:
Construct 1 Construct 2 Destruct 2 // 异常发生后,自动析构已构造的对象 Destruct 1 Caught exception
结论:异常发生时,C++ 会保证所有已构造的对象被正确析构,前提是析构函数自身不抛出异常。
6.4.3 处理资源的最佳实践:RAII 模式
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 中管理资源的经典模式,其核心是用对象的生命周期管理资源,配合析构函数确保资源在任何情况下(包括异常)都能被释放。
示例:用 RAII 封装文件资源
#include <fstream>
#include <stdexcept>
class FileHandler {std::ofstream file;
public:// 构造函数获取资源(打开文件)FileHandler(const std::string& filename) {file.open(filename);if (!file.is_open()) {throw std::runtime_error("Failed to open file"); // 构造函数可抛异常}}
// 析构函数释放资源(关闭文件),确保不抛异常~FileHandler() {if (file.is_open()) {file.close(); // 即使close()可能失败,也不向外抛异常}}
// 提供操作资源的接口void write(const std::string& content) {if (!file.write(content.c_str(), content.size())) {throw std::runtime_error("Failed to write to file");}}
};
// 使用RAII类
void processFile() {FileHandler file("data.txt"); // 获取资源// 若以下代码抛出异常,FileHandler的析构函数仍会被调用,确保文件关闭file.write("Hello, RAII!");throw std::runtime_error("Something went wrong");
}
int main() {try {processFile();} catch (const std::exception& e) {std::cout << "Error: " << e.what() << std::endl;}return 0;
}
优势:无论 processFile()
中是否抛出异常,FileHandler
的析构函数都会被调用,保证文件被正确关闭,避免资源泄漏。
四、总结:析构函数与异常处理的核心要点
-
析构函数绝对不能抛出异常:必须在内部捕获所有异常,否则可能导致程序终止。
-
异常触发栈展开时,析构函数会自动调用:C++ 保证所有已构造的对象被正确清理。
-
优先使用 RAII 模式管理资源:将资源封装在对象中,利用析构函数的特性确保资源安全释放,这是处理异常场景下资源管理的最佳实践。
遵循这些原则可以有效避免异常处理中常见的资源泄漏和程序稳定性问题。