【C++入门精讲13】异常处理

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,能提升代码的健壮性和性能。本文所有代码均保留原始注释,可直接复制运行,对照知识点理解更高效。

原创不易,转载请注明出处,感谢阅读!

相关推荐
计算机安禾1 小时前
【c++面向对象编程】第5篇:类与对象(四):赋值运算符重载
java·前端·c++
样例过了就是过了1 小时前
LeetCode热题100 颜色分类
c++·算法·leetcode
ZPC82101 小时前
C++ 跨平台 UDP 收发测试程序
c++·算法·机器人
hanbr1 小时前
C++ 类型转换与异常处理全解析
开发语言·c++
ym_xixi1 小时前
《类和对象》—— 构造函数与析构函数总结
前端·c++·算法
凯瑟琳.奥古斯特2 小时前
丑数II C++三指针解法(力扣264)
数据结构·c++·算法·leetcode·职场和发展
YYYing.2 小时前
【C++项目之高并发内存池 (四)】三层缓存的空间回收流程详解
c++·笔记·缓存·高并发·内存池
小小de风呀2 小时前
de风——【从零开始学C++】(六):模板初阶
开发语言·c++
j_xxx404_2 小时前
力扣算法:用栈消消乐,巧解相邻重复与退格字符串
c++·算法·leetcode