《C++异常处理完全指南》

发布于:2025-08-19 ⋅ 阅读:(21) ⋅ 点赞:(0)

✨✨小新课堂开课了,欢迎欢迎~✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:C++:由浅入深篇

小新的主页:编程版小新-CSDN博客

一.异常的概念和使用

异常的概念

在C++中,异常是一种处理程序运行时错误或异常情况的机制。它允许程序在遇到错误时,将控制权从错误的部分转移到可以处理该错误的部分。

异常处理使得程序更加健全,并且可以更优雅的处理错误情况,而不是直接崩溃。

C语言主要通过错误码的形式处理错误,错误码本质就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息。

int divide(int a, int b, int* result)
{
	if (b == 0)
	{
        errno = EINVAL;
		return EINVAL;//返回错误码
	}
	*result = a / b;
	return 0;
}
int main()
{
	int res;
	int ret = divide(10, 0, &res);
	if (ret != 0)
	{
		perror("Division failed");
		return 1;
	}
	printf("%d\n", res);
	return 0;
}

异常处理关键字

throw:当程序出现错误时,可以用throw关键字抛出一个异常。抛出的异常可以是任意类型的对象。当throw被执行时,throw后面的语句将不再被执行。

try:用于包含可能会抛出异常的代码。try块后面通常会跟一个或者多个catch块。抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝对象会在catch子句后销毁。

catch:用来捕获并处理异常。每个catch块指定它可以处理的异常类型。当异常被抛出时,程序会寻找匹配的且离抛出异常位置最近的那一个catch块来处理它。catch可能是同一函数中的一个局部的catch,也可能是另一个函数中的catch,控制权从throw位置转移到了catch位置。

**示例**

double Divide(int a, int b)
{
	try
	{
		// 当b == 0时抛出异常
		if (b == 0)
		{
			string s("Divide by zero condition!");
			throw s;//抛出的是s的拷贝,s在当前局部域,出了作用域就销毁了
			cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
		} 
		else
		{
		return ((double)a / (double)b);
		}
	} 
	catch(int errid)
	{
		cout << errid << endl;
	} 
	return 0;
} 

void Func()
{
	int len, time;
	cin >> len >> time;
	try
	{
		cout << Divide(len, time) << endl;
	} 
	catch(const char* errmsg)
	{
		cout << errmsg << endl;
	} 
	cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
} 

int main()
{
	while (1)
	{
		try
		{
			Func();
		} 
		catch(const string & errmsg)
		{
			cout << errmsg << endl;
		}
	} 
	return 0;
}

**调试演示**

异常调试演示

我们第一组输入的是10和0,在Divide函数内throw抛出一个异常,这个异常是个string对象,可以注意到,当throw被执行时,throw后面的cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;语句没有被执行。随后catch进行捕捉,我们发现是匹配到了主函数内catch。因为与其他的catch类型不匹配。第二组输入的3和4。不存在异常,调用完Divide函数后,执行了cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;语句后进入的下一轮的循环。

**运行结果**

栈展开

抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理,如果找到匹配的catch子句处理后,catch子句代码会继续执行。如果当前函数中没有try/catch子句,或者有try/catch子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch过程被称为栈展开。

还是以上面的代码为例。在DIvide函数中的try块内部,throw抛出一个异常。该异常是一个string对象。接下里去寻找匹配的catch语句,由于与当前函数的cach子句类型不匹配,就退出了当前函数,继续从外层的调用链中查找,来到了Func函数,依旧类型不匹配,最后来到了主函数,类型匹配,异常得到处理。

如果到达main函数,依旧没有找到匹配的catch子句,程序会调调标准库的 terminate 函数终止程序。

查找匹配的处理代码

一般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近那个,但是也有一些例外。

 允许从非常量向常量的类型转换,也就是权限缩小。因为常量引用或指针只承诺不修改对象,而非常量对象完全满足这个条件。

try
{
	throw 45;//抛出int类型(非常量)
}
catch (const int& e)//可以用常量引用捕获
{
	//
}

允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针。在C++中,数组和函数在大多数表达式中会自动转换为指针。(数组退化为指向数组首元素的指针,函数退化为函数值指针,保持与C的兼容)

try
{
	throw"Hello";//抛出const char[6]类型,自动转化为const char*
}
catch (const char* e)
{
	//...
}

允许从派生类向基类类型的转换。我们通常使用继承  来建立类之间的层次关系,基类表示一般概念,派生类表示更具体的概念。在异常处理中,我们通常希望捕获基类可以处理所有派生类异常。这样我们可以用基类捕获整个类族的异常,而不用为每个派生类编写单独的catch块。

如果到main函数,异常仍旧没有被匹配就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以一般main函数中最后都会使用catch(...),它可以捕获任意类型的异常,但是是不知道异常错误是什么。

**示例**

class Exception
{
	public :
	Exception(const string& errmsg, int id)
		: _errmsg(errmsg)
		, _id(id)
	{}
	virtual string what() const //what是个虚函数,派生类可以重写
	{
		return _errmsg;
	} 
	int getid() const
	{
		return _id;
	}
protected:
	string _errmsg;
	int _id;
};
class SqlException : public Exception
{
public:
	SqlException(const string& errmsg, int id, const string& sql)
		: Exception(errmsg, id)
		, _sql(sql)
	{}
	virtual string what() const //对what进行重写后,给出更详细的错误信息
	{
		string str = "SqlException:";
		str += _errmsg;
		str += "->";
		str += _sql;
		return str;
	}
private:
	const string _sql;
};
class CacheException : public Exception
{
	public :
	CacheException(const string& errmsg, int id)
		: Exception(errmsg, id)
	{}
	virtual string what() const
	{
		string str = "CacheException:";
		str += _errmsg;
		return str;
	}
};
class HttpException : public Exception
{
	public :
	HttpException(const string& errmsg, int id, const string& type)
		: Exception(errmsg, id)
		, _type(type)
	{}
	virtual string what() const
	{
		string str = "HttpException:";
		str += _type;
		str += ":";
		str += _errmsg;
		return str;
	}
private:
	const string _type;
};
void SQLMgr()
{
	if (rand() % 7 == 0)
	{
		throw SqlException("权限不足", 100, "select * from name = '张三'");
	} 
	else
	{
	cout << "SQLMgr 调用成功" << endl;
	}
}

void CacheMgr()
{
	if (rand() % 5 == 0)
	{
		throw CacheException("权限不足", 100);
	} 
	else if (rand() % 6 == 0)
	{
		throw CacheException("数据不存在", 101);
	} 
	else
	{
	cout << "CacheMgr 调用成功" << endl;
	} 
	SQLMgr();
} 

void HttpServer()
{
	if (rand() % 3 == 0)
	{
		throw HttpException("请求资源不存在", 100, "get");
	} 
	else if (rand() % 4 == 0)
	{
		throw HttpException("权限不足", 101, "post");
	} 
	else
	{
	cout << "HttpServer调用成功" << endl;
	} CacheMgr();
} 

int main()
{
	srand(time(0));
	while (1)
	{
		this_thread::sleep_for(chrono::seconds(1));
		try
		{
			HttpServer();
		} 
		catch(const Exception & e) // 这里捕获基类,基类对象和派生类对象都可以被捕获
		{
		cout << e.what() << endl;
		} 
		catch(...)//捕获任意类型的异常
		{
			cout << "Unkown Exception" << endl;
		}
	} 
	return 0;
}

**运行结果**

异常的重新抛出

在异常处理中,重新抛出是指在捕获一个异常后不完全处理它,而是将其再次抛出,让更高层的调用者继续处理。

下面程序模拟展示了聊天时发送消息,发送失败捕获异常,但是可能在电梯地下室等场景手机信号不好,则需要多次尝试,如果多次尝试都发送不出去,则就需要捕获异常再重新抛出,其次如果不是网络差导致的错误,捕获后也要重新抛出。

class Exception
{
public:
	Exception(const string& errmsg, int id)
		: _errmsg(errmsg)
		, _id(id)
	{}
	virtual string what() const //what是个虚函数,派生类可以重写
	{
		return _errmsg;
	}
	int getid() const
	{
		return _id;
	}
protected:
	string _errmsg;
	int _id;
};

class HttpException : public Exception
{
public:
	HttpException(const string& errmsg, int id, const string& type)
		: Exception(errmsg, id)
		, _type(type)
	{}
	virtual string what() const
	{
		string str = "HttpException:";
		str += _type;
		str += ":";
		str += _errmsg;
		return str;
	}
private:
	const string _type;
};

void _SeedMsg(const string& s)
{
	if (rand() % 2 == 0)
	{
		throw HttpException("网络不稳定,发送失败", 102, "put");
	} 
	else if (rand() % 7 == 0)
	{
		throw HttpException("你已经不是对象的好友,发送失败", 103, "put");
	} 
	else
	{
	cout << "发送成功" << endl;
	}
} 

void SendMsg(const string& s)
{
	// 发送消息失败,则再重试3次
	for (size_t i = 0; i < 4; i++)
	{
		try
		{
			_SeedMsg(s);
			break;
		} 
		catch(const Exception & e)
		{
			// 捕获异常,if中是102号错误,网络不稳定,则重新发送
			// 捕获异常,else中不是102号错误,则将异常重新抛出
			if (e.getid() == 102)
			{
				// 重试三次以后否失败了,则说明网络太差了,重新抛出异常
				if (i == 3)
					throw;
				cout << "开始第" << i + 1 << "重试" << endl;
			} 
			else
			{
			throw;
			}
		}
	}
} 

int main()
{
	srand(time(0));
	string str;
	while (cin >> str)
	{
		try
		{
			SendMsg(str);
		} 
		catch(const Exception & e)
		{
			cout << e.what() << endl << endl;
		} 
		catch(...)
		{
			cout << "Unkown Exception" << endl;
		}
	} 
	return 0;
}

**运行结果**

异常安全问题

异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。

double Divide(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Division by zero condition!";
	} 
	return(double)a / (double)b;
}
void Func()
{
	// 这⾥可以看到如果发⽣除0错误抛出异常,另外下面的array没有得到释放。
	// 所以这⾥捕获异常后并不处理异常,异常还是交给外层处理,这⾥捕获了再
	// 重新抛出去。
	int* array = new int[10];
	try
	{
		int len, time;
		cin >> len >> time;
		cout << Divide(len, time) << endl;
	} 
	catch(...)
	{
		// 捕获异常释放内存
		cout << "delete []" << array << endl;
		delete[] array;
		throw; // 异常重新抛出,捕获到什么抛出什么
	} 
	cout << "delete []" << array << endl;
	delete[] array;
} 
int main()
{
	try
	{
		Func();
	} 
	catch(const char* errmsg)
	{
		cout << errmsg << endl;
	} 
	catch(const exception & e)
	{
		cout << e.what() << endl;
	} 
	catch(...)
	{
		cout << "Unkown Exception" << endl;
	} 
	return 0;
}

异常规范

对于用户和编译器而言,预先知道某个程序会不会抛出异常大有裨益,知道某个函数是否会抛出异常有助于简化调用函数的代码。

C++98中函数参数列表的后面接throw(),表示函数不抛异常,函数参数列表的后面接throw(类型1,类型2...)表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割。

// C++98
// 这⾥表⽰这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这⾥表⽰这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();

C++98的这种方式过于复杂,实践中并不好用,C++11中进行了简化,函数参数列表后面加noexcept表示不会抛出异常,啥都不加表示可能会抛出异常。

// C++11
size_type size() const noexcept;
iterator begin() noexcept;
const_iterator begin() const noexcept;

 编译器并不会在编译时检查noexcept,也就是说如果一个函数用noexcept修饰了,但是同时又包含了throw语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是一个声明了noexcept的函数抛出了异常,程序会调用terminate 终止程序。

double Divide(int a, int b) noexcept
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Division by zero condition!";
	} 
	return(double)a / (double)b;
} 

 noexcept(expression)还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会,注意这里说的是可能,则返回false,不会就返回true。

**示例**

int main()
{
	int i = 0;
	cout << noexcept(Divide(1, 2)) << endl;
	cout << noexcept(Divide(1, 0)) << endl;
	cout << noexcept(++i) << endl;
}

**运行结果**

二.标准库的异常

C++标准库也定义了一套自己的一套异常继承体系库,基类是exception,所以我们日常写程序,需要在主函数捕获exception即可,要获取异常信息,就调用what函数。

**示例**

#include <exception>
#include <string>

// 基础业务异常(通用场景)
class BusinessException : public std::exception //继承exception就可以
{
public:
    explicit BusinessException(const std::string& msg)
        : msg_(msg) 
    {}

    const char* what() const noexcept override 
    {
        return msg_.c_str();
    }

private:
    std::string msg_;
};

// 具体领域异常(推荐继承最匹配的标准异常)
class DatabaseException : public std::runtime_error //也可以继承更具体的标准异常
{
public:
    explicit DatabaseException(const std::string& msg, int error_code = 0)
        : std::runtime_error(msg),
        error_code_(error_code) {}

    int error_code() const noexcept 
    {
        return error_code_;
    }

private:
    int error_code_;
};

创作不易,还请各位大佬支持~


网站公告

今日签到

点亮在社区的每一天
去签到