C++异常处理全解析(含自定义异常类+noexcept)
在C++编程中,异常处理是应对运行时错误的核心机制,能够避免程序因未处理的错误直接崩溃,提升代码的健壮性和可维护性。本文将结合实际代码案例,详细解析C++异常处理的核心知识点、用法及注意事项,便于对照学习。
一、异常的基础认知
1.1 什么是异常?
运行时,因内存、算法、逻辑等相关错误发生时,产生了不可估计的错误,使得程序中断运行,我们就说程序产生了异常。
1.2 C语言与C++异常处理的区别(C语言错误处理方式)
C语言中没有专门的异常处理关键字,通常通过以下方式处理错误:
-
函数返回值判断(如malloc申请内存失败返回NULL、open函数打开文件失败返回-1);
-
错误号(errno.h中的errno)配合strerror(errno)、perror()打印错误信息;
-
断言(assert):表达式为假时,系统发送abort()中断程序。
补充:Linux中可通过 man 2|3 函数名 查看函数的参数与功能说明,快速定位错误原因。
1.3 C++异常处理核心关键字
C++提供了专门的异常处理关键字,用于抛出、捕获和处理异常,核心组合为 try-catch-throw。
-
throw:抛出异常,可抛出常数、变量、类对象等任何数据类型; -
try:包裹可能存在异常的语句块,一个程序中可以有多个try块(注意:原代码注释中"整个程序只能有一个try"表述不准确,实际可多个try,每个try对应自身的catch); -
catch:捕获异常,根据异常类型匹配处理逻辑,一个try可对应多个catch(按异常类型顺序匹配)。
注意:若程序中存在异常但未被捕获,程序会中断运行,并输出异常信息。
对应代码段(基础异常抛出与捕获)
cpp
#if 0
#include<iostream>
#include<cstdlib>
#include<string>
#include<stdexcept>
using namespace std;
#if 0
//c加加中的异常处理?
//1.什么是异常?
//运行时,因内存,算法,逻辑等相关的错误发生时,产生了不可估计的错误,使得我们的程序中断运行,我们就说程序产生了异常。
//2.如何识别并处理存在的错误?
//C:例如,malloc内粗申请,可能,申请失败。
//int fd = open()函数,打开文件,可能,文件不存在,文件路径错误,文件权限错误,文件被占用等,要具体判断具体分析。
//int scfd = socket();
//int ret = bind(scfd,sockaddr_in);
//<errno.h> errno 错误号
//assert<断言条件判断表达式> 表达式为假时,系统则发送abort();
//<string .h> strerror(errno)
//perror()
//Linux 中查看函数的参数与功能说明: man 2|3 函数名
//1.2 c加加中异常抛出相关的关键字:
// throw 常数|变量|类对象等任何数据类型 throw有抛出的意思,指的是抛出异常
//
//如果程序中存在异常时,而没有得到处理,(异常捕获),则程序会中断运行,并输出异常信息。
//1.3 常熟捕获异常的关键字
//try {可能存在异常的语句;}
//catch(异常类型 变量名) {处理异常的语句;}
int divture(int a, int b) {
if (b == 0) throw "除数为零";
return a / b;
}
int main()
{
int a, b;
//当语句出现异常时,未处理则程序中断
try//(整个程序只能有一个,不可以有多个try,但可以有多个catch)
{
while (1) {
cout << "请输入两个整数:";
cin >> a >> b;
try {
cout << divture(a, b) << endl;//如过这句话出现了错误,并没有即时的处理,那么程序中断(程序中断了,那么肯定就时循环中断了),并从其作用域开始往上找是否有try尝试捕获信息,如果没有则程序中断,如果有则从catch开始往下找,找到则执行catch中的语句,如果没有找到则程序中断。
}
catch (const char* err) {
cout << "error: " << err << endl;
}//这个时候有try捕获到了异常,所以程序没有中断,而是执行了catch中的语句,并继续往下执行。
}
cout << divture(1, 0) << endl;
}
catch (const char* err) {
cout << "error: " << err << endl;
}//
cout << "---ok---" << endl;
return 0;
}
#endif
1.4 多类型异常捕获
throw可抛出不同类型的异常,catch需按类型顺序匹配(无匹配则程序中断),支持int、double、string等基础类型。
对应代码段(多类型异常捕获)
cpp
#if 0
void testerror(int a, int b, int c) {
if (a > 10) {
throw a;
}
if (b > 5) {
throw 1.25;
}
if (c > 2) {
throw string("c值不能超出2");
}
cout<<"a:"<<a<<" b:"<<b<<" c:"<<c<<endl;
}
#endif
#if 0
int a, b, c;
while (1) {
cout << "a,b,c: ";
cin >> a >> b >> c;
if (a == 0) break;
try {
testerror(a, b, c);
}
catch (int error) {
cout << "error: " << error << endl;
}
catch (double error) {
cout << "error: " << error << endl;
}
catch (string& error) {
cout << "error: " << error << endl;
}
}
cout << "Over" << endl;
#endif
二、自定义异常类
C++允许自定义异常类,无强制要求,但建议参考系统异常类(如exception)的设计,便于统一管理和扩展。
2.1 自定义异常类的基本设计
核心要点:
-
包含私有成员
string errinfo,用于存储异常信息; -
提供构造函数(初始化异常信息)、拷贝构造函数;
-
提供公开的
what()或error()方法,返回异常信息(建议用what(),与系统异常类保持一致); -
析构函数建议设为虚函数,便于派生类重写(支持多态)。
对应代码段(基础自定义异常类)
cpp
//二,自定义异常类
// 2.1 自定义异常类,没有任何要求,靠近c加加异常类
//建议存在string errinfo; 消息成员属性和构造函数
//提供一个公开的error()或what() 返回这个string 类的 errinfo
class Exception {
private:
string einfo;
public:
Exception(const string& einfo) : einfo(einfo) {
cout << "Exception(const string& einfo) : einfo(einfo) " << this <<endl;;
}
Exception(const Exception& other) {
einfo = other.einfo;
}
~Exception() { cout << "~Exception() " << this << endl; }
virtual string what() {
return einfo;
}
};
void test1() {
//c加加中,当抛出异常类对象时,则会将这个异常类对象拷贝或存储到异常栈中。
//throw Exception("测试异常1");//临时对象,以及局部对象还有全局对象我们都是直接存储到栈中,
//对于局部对象,要拷贝到异常栈中
Exception e("测试异常2");
throw e;//临时对象我们时直接存储到栈中,但是对于非临时对象,它会直接在创建的这个异常类的基础上,自动进行拷贝操作拷贝到异常栈中,然后拷贝完成后,原来的那个异常类就会调用析构函数,自动删除,也就是,临时对象是直接存在于异常类的栈中,不存在拷贝临时异常对象的操作
//局部对象以及全局对象是要多经过一一部拷贝的操作,会有更大的开销。
}
#if 0
try {
test1();
}
catch (Exception& e) {//这里建议用引用,因为不用引用的话会将异常栈中的对象拷贝一个新的内存空间,调用拷贝构造,作为形式参数来传值
//所以我们使用引用,来减少这样的开支。强烈建议使用
cout<<e.what()<<endl;
cout<<&e<<endl;
//当异常栈中的对象被处理后,则会自动释放【也叫做异常栈对象解旋】
}//当我们的catch 处理完异常后,即进行了相应的处理后,这个异常类对象会自动进行释放,调用析构函数。
cout << "---ok---" << endl;
#endif
2.2 自定义异常类的继承(派生类异常)
可通过继承自定义异常类或系统异常类,实现异常的分级管理,核心注意事项:
-
派生类异常的捕获顺序必须在基类异常之前(否则派生类异常会被基类catch捕获,无法执行派生类的处理逻辑);
-
派生类可重写
what()方法,扩展异常信息。
对应代码段(派生类异常)
cpp
class OutOFRangeException : public Exception {//对于派生类与基类的异常捕获,派生类异常的捕获顺序要在异常基类之前,这是针对c加加中的多态性的一种规则。
public:
OutOFRangeException():Exception("数组越界") {}
string whar() {
return string("严重错误:数组越界");
}
};
void test2() {
throw OutOFRangeException();
}
#if 0
/ 二、自定义异常类
// 2.1 自定义异常类 没有任何要求, 靠近C++异常类体系中exception类
// 建议 存在 string errinfo 消息成员属性和构造函数
// 提供一个公开的 error()或what() 返回errinfo
class Exception {
private:
string einfo;
public:
Exception(const string& einfo) :einfo(einfo) {
cout << "new error obj: " << this << endl;
}
Exception(const Exception& other) {
cout << "new error obj: " << this << " from copy " << &other << endl;
}
virtual ~Exception() {
cout << "delete error obj:" << this << endl;
}
virtual string what() {
return einfo;
}
};
class OutOfRangeException : public Exception {
public:
OutOfRangeException() :Exception("下标越界") {}
string what() {
return string("严重错误: ") + Exception::what();
}
};
void test2() {
throw OutOfRangeException();
}
int main() {
try {
//test1();
test2();
}
catch (OutOfRangeException& e) {
// 对于 派生类异常的捕获顺序高于 异常基类
cout << "OutOfRangeException->" << e.what() << endl;
}
catch (Exception& e) {
// 1) 创建新异常对象(拷贝构造,拷贝异常栈中的对象)
// 2) 使用引用方式,引用异常栈中的对象 【建议】
cout << e.what() << endl;
// 当异常栈中的对象被处理之后, 则会自动释放 【异常栈对象解旋】
cout << "catch error obj: " << &e << endl;
}
cout << "--Main-OK--" << endl;
return 0;
}
#endif
2.3 继承系统异常类
C++标准库提供了基础异常类exception(定义在<stdexcept>中),我们可继承系统异常类(如invalid_argument),重写what()方法,实现更贴合业务的异常处理。
补充:系统异常类的what()方法通常返回const char*类型,重写时需注意返回值类型和const修饰。
对应代码段(继承系统异常类)
cpp
//当然,我们自己继承自己自定义的异常类,我们也可以去继承这个c加加中一直存在的异常类,但是我们需要注意的是,我们要知道这个系统的异常类是怎么写的,依据异常基类来设计我们继承的异常类
void test3(int a) {
if (a < 0 || a>100)
throw invalid_argument("a的取值范围在0-100之间");
}
// 2.2 自定义异常类时,可以派生C++已存在的异常类,建议派生c加加中已经存在的类(expection)这是最大的最基础的派生类,并且重写虚函数char const * waht() const{}
//
//
//例如:
class InvalidException : public invalid_argument {
private:
int argV;
public:
InvalidException(int argV, const string& e) :argV(argV), invalid_argument(e) {}
const char* what() const override {
static char buff[128]{ 0 };
sprintf_s(buff, "%d %s", argV, invalid_argument::what());
return buff;
}
};
三、noexcept函数
3.1 noexcept的作用
在函数后添加noexcept关键字,表示该函数内不会抛出任何异常,编译器会对此进行优化,提升程序性能。
3.2 推荐使用场景
-
构造函数、拷贝构造函数;
-
只读成员属性的函数(无修改操作,不易产生异常);
-
明确不会抛出异常的函数(避免编译器生成额外的异常处理代码)。
对应代码段(noexcept函数使用)
cpp
//三 noexpect 函数
// 修改在函数后,表示此函数内无throw
//建议使用noexpect 函数
//构造函数,拷贝构造函数,只读成员属性的函数
void test4() noexcept {
cout<<"test4: ok" << endl;
}
四、异常处理关键注意事项(补充)
-
异常栈对象解旋:当异常被catch捕获并处理后,异常栈中的对象会自动释放,调用析构函数,无需手动释放;
-
catch捕获建议用引用:避免拷贝异常对象,减少内存开销,同时支持多态(基类引用可接收派生类对象);
-
异常捕获顺序:派生类异常在前,基类异常在后;具体类型异常在前,通用类型异常在后;
-
临时异常对象:throw抛出临时异常对象时,会直接存储到异常栈中,无需拷贝;非临时对象(如局部对象)会拷贝到异常栈中,原对象会立即析构。
五,整体原代码和笔记
cpp
#if 0
#include<iostream>
#include<cstdlib>
#include<string>
#include<stdexcept>
using namespace std;
#if 0
//c加加中的异常处理?
//1.什么是异常?
//运行时,因内存,算法,逻辑等相关的错误发生时,产生了不可估计的错误,使得我们的程序中断运行,我们就说程序产生了异常。
//2.如何识别并处理存在的错误?
//C:例如,malloc内粗申请,可能,申请失败。
//int fd = open()函数,打开文件,可能,文件不存在,文件路径错误,文件权限错误,文件被占用等,要具体判断具体分析。
//int scfd = socket();
//int ret = bind(scfd,sockaddr_in);
//<errno.h> errno 错误号
//assert<断言条件判断表达式> 表达式为假时,系统则发送abort();
//<string .h> strerror(errno)
//perror()
//Linux 中查看函数的参数与功能说明: man 2|3 函数名
//1.2 c加加中异常抛出相关的关键字:
// throw 常数|变量|类对象等任何数据类型 throw有抛出的意思,指的是抛出异常
//
//如果程序中存在异常时,而没有得到处理,(异常捕获),则程序会中断运行,并输出异常信息。
//1.3 常熟捕获异常的关键字
//try {可能存在异常的语句;}
//catch(异常类型 变量名) {处理异常的语句;}
int divture(int a, int b) {
if (b == 0) throw "除数为零";
return a / b;
}
int main()
{
int a, b;
//当语句出现异常时,未处理则程序中断
try//(整个程序只能有一个,不可以有多个try,但可以有多个catch)
{
while (1) {
cout << "请输入两个整数:";
cin >> a >> b;
try {
cout << divture(a, b) << endl;//如过这句话出现了错误,并没有即时的处理,那么程序中断(程序中断了,那么肯定就时循环中断了),并从其作用域开始往上找是否有try尝试捕获信息,如果没有则程序中断,如果有则从catch开始往下找,找到则执行catch中的语句,如果没有找到则程序中断。
}
catch (const char* err) {
cout << "error: " << err << endl;
}//这个时候有try捕获到了异常,所以程序没有中断,而是执行了catch中的语句,并继续往下执行。
}
cout << divture(1, 0) << endl;
}
catch (const char* err) {
cout << "error: " << err << endl;
}//
cout << "---ok---" << endl;
return 0;
}
#endif
#if 0
void testerror(int a, int b, int c) {
if (a > 10) {
throw a;
}
if (b > 5) {
throw 1.25;
}
if (c > 2) {
throw string("c值不能超出2");
}
cout<<"a:"<<a<<" b:"<<b<<" c:"<<c<<endl;
}
#endif
//二,自定义异常类
// 2.1 自定义异常类,没有任何要求,靠近c加加异常类
//建议存在string errinfo; 消息成员属性和构造函数
//提供一个公开的error()或what() 返回这个string 类的 errinfo
class Exception {
private:
string einfo;
public:
Exception(const string& einfo) : einfo(einfo) {
cout << "Exception(const string& einfo) : einfo(einfo) " << this <<endl;;
}
Exception(const Exception& other) {
einfo = other.einfo;
}
~Exception() { cout << "~Exception() " << this << endl; }
virtual string what() {
return einfo;
}
};
class OutOFRangeException : public Exception {//对于派生类与基类的异常捕获,派生类异常的捕获顺序要在异常基类之前,这是针对c加加中的多态性的一种规则。
public:
OutOFRangeException():Exception("数组越界") {}
string whar() {
return string("严重错误:数组越界");
}
};
#if 0
/ 二、自定义异常类
// 2.1 自定义异常类 没有任何要求, 靠近C++异常类体系中exception类
// 建议 存在 string errinfo 消息成员属性和构造函数
// 提供一个公开的 error()或what() 返回errinfo
class Exception {
private:
string einfo;
public:
Exception(const string& einfo) :einfo(einfo) {
cout << "new error obj: " << this << endl;
}
Exception(const Exception& other) {
cout << "new error obj: " << this << " from copy " << &other << endl;
}
virtual ~Exception() {
cout << "delete error obj:" << this << endl;
}
virtual string what() {
return einfo;
}
};
class OutOfRangeException : public Exception {
public:
OutOfRangeException() :Exception("下标越界") {}
string what() {
return string("严重错误: ") + Exception::what();
}
};
void test1() {
// C++中 ,当抛出异常类对象时,则拷贝或存储到异常栈中
//throw Exception("测试异常1"); // 临时对象 直接存储到异常中栈
Exception e("测试异常1_1");
throw e;// 局部对象 拷贝到 异常栈中
}
void test2() {
throw OutOfRangeException();
}
int main() {
try {
//test1();
test2();
}
catch (OutOfRangeException& e) {
// 对于 派生类异常的捕获顺序高于 异常基类
cout << "OutOfRangeException->" << e.what() << endl;
}
catch (Exception& e) {
// 1) 创建新异常对象(拷贝构造,拷贝异常栈中的对象)
// 2) 使用引用方式,引用异常栈中的对象 【建议】
cout << e.what() << endl;
// 当异常栈中的对象被处理之后, 则会自动释放 【异常栈对象解旋】
cout << "catch error obj: " << &e << endl;
}
cout << "--Main-OK--" << endl;
return 0;
}
#endif
void test1() {
//c加加中,当抛出异常类对象时,则会将这个异常类对象拷贝或存储到异常栈中。
//throw Exception("测试异常1");//临时对象,以及局部对象还有全局对象我们都是直接存储到栈中,
//对于局部对象,要拷贝到异常栈中
Exception e("测试异常2");
throw e;//临时对象我们时直接存储到栈中,但是对于非临时对象,它会直接在创建的这个异常类的基础上,自动进行拷贝操作拷贝到异常栈中,然后拷贝完成后,原来的那个异常类就会调用析构函数,自动删除,也就是,临时对象是直接存在于异常类的栈中,不存在拷贝临时异常对象的操作
//局部对象以及全局对象是要多经过一一部拷贝的操作,会有更大的开销。
}
void test2() {
throw OutOFRangeException();
}
//当然,我们自己继承自己自定义的异常类,我们也可以去继承这个c加加中一直存在的异常类,但是我们需要注意的是,我们要知道这个系统的异常类是怎么写的,依据异常基类来设计我们继承的异常类
void test3(int a) {
if (a < 0 || a>100)
throw invalid_argument("a的取值范围在0-100之间");
}
// 2.2 自定义异常类时,可以派生C++已存在的异常类,建议派生c加加中已经存在的类(expection)这是最大的最基础的派生类,并且重写虚函数char const * waht() const{}
//
//
//例如:
class InvalidException : public invalid_argument {
private:
int argV;
public:
InvalidException(int argV, const string& e) :argV(argV), invalid_argument(e) {}
const char* what() const override {
static char buff[128]{ 0 };
sprintf_s(buff, "%d %s", argV, invalid_argument::what());
return buff;
}
};
//三 noexpect 函数
// 修改在函数后,表示此函数内无throw
//建议使用noexpect 函数
//构造函数,拷贝构造函数,只读成员属性的函数
void test4() noexcept {
cout<<"test4: ok" << endl;
}
int main() {
#if 0
int a, b, c;
while (1) {
cout << "a,b,c: ";
cin >> a >> b >> c;
if (a == 0) break;
try {
testerror(a, b, c);
}
catch (int error) {
cout << "error: " << error << endl;
}
catch (double error) {
cout << "error: " << error << endl;
}
catch (string& error) {
cout << "error: " << error << endl;
}
}
cout << "Over" << endl;
#endif
#if 0
try {
test1();
}
catch (Exception& e) {//这里建议用引用,因为不用引用的话会将异常栈中的对象拷贝一个新的内存空间,调用拷贝构造,作为形式参数来传值
//所以我们使用引用,来减少这样的开支。强烈建议使用
cout<<e.what()<<endl;
cout<<&e<<endl;
//当异常栈中的对象被处理后,则会自动释放【也叫做异常栈对象解旋】
}//当我们的catch 处理完异常后,即进行了相应的处理后,这个异常类对象会自动进行释放,调用析构函数。
cout << "---ok---" << endl;
#endif
return 0;
}
#endif
总结
C++异常处理通过try-catch-throw关键字实现,配合自定义异常类和系统异常类,可实现灵活、可扩展的错误处理。核心是"抛出异常-捕获异常-处理异常"的流程,合理使用引用、虚函数和noexcept,能提升代码的健壮性和性能。本文所有代码均保留原始注释,可直接复制运行,对照知识点理解更高效。
原创不易,转载请注明出处,感谢阅读!