【C++】异常处理

时间:2024-10-01 21:14:53

目录

一、C语言中传统的异常处理方式:

二、C++中的异常处理方式:

三、异常的使用

1、关于抛出与捕获:

2、关于异常的抛出和匹配:

3、异常的重新抛出:

4、异常安全:

5、异常规范:

四、异常的优缺点:

1、优点:

2、缺点:


一、C语言中传统的异常处理方式:

当C语言程序出现错误的时候会出现两种情况:

1、在程序结束时返回错误码(一般如果正常结束返回0)

2、在程序进行时直接发现错误直接进行终止(比如assert)

以上就是断言错误或者是错误码返回,

在断言错误中它会自己报出错具体位置和出错原因,就可以定位出错位置

注意:这是在debug的环境下才会有assert,如果在release版本会自动删除assert

二、C++中的异常处理方式:

在C++中,祖师爷觉得C语言的那些方式依然给的信息不够,所以就创造出来了抛异常,捕获异常这些概念,在C++中引入了三个关键字来进行关于程序异常的信息捕获。

1、try:

这个就是在try的代码段中进行监视的,如果有,就会进行抛出,捕获异常。

try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。

try {
    //在这代码区域中进行监视,如果有异常就进行抛出
}
catch (const exception& e) {
    // 处理异常
}

2、throw:

这个就是抛出异常,

3、catch:

这个就是将抛出的异常捕获到

在try后面还可以进行多次catch的接收:

try
{
    //保护代码
}
catch (const exception& e)
{}
catch (const exception& e)
{}
catch (const exception& e)
{}

三、异常的使用

1、关于抛出与捕获:

double Division(int a, int b)
{
	if (b == 0)
		throw("被除数不能为零");
	else
		return (float)a / (float)b;

}

void func()
{
	double x, y;
	cin >> x>> y;
	cout << Division(x,y) << endl;
}

int main()
{
	try
	{
		func();
	}
	catch(const char* err)
	{
		cout << err << endl;
	}
	return 0;
}

如果传输有问题的话就会catch,否则就正常返回

2、关于异常的抛出和匹配:

1、如果在不同函数中有多个匹配的,就会找离catch最近的那一层的catch,并且返回catch后,会继续执行catch后面的语句。

double Division(int a, int b)
{
	if (b == 0)
		throw("被除数不能为零");
	else
		return (float)a / (float)b;

}

void func()
{
	double x, y;
	cin >> x>> y;
	try
	{
		cout << Division(x,y) << endl;
	}
	catch(const int* err)
	{
		cout << "catch(const int* err)" << endl;
		cout << err << endl;
	}
	catch(const int err)
	{
		cout << "catch(const int err)" << endl;
		cout << err << endl;
	}
	catch (const char* err)
	{
		cout << "catch (const char* err)" << endl;
		cout << err << endl;
	}
	cout << "void func()" << endl;
}

int main()
{
	try
	{
		func();
	}
	catch(const char* err)
	{
		cout << err << endl;
	}
	return 0;
}

2、如果有多个catch就会找类型相同的catch语句进行执行,如果没有找到就会报错

3、那么就有一种方法:

catch(...)这样来进行捕获,这个是捕获任意类型的,作为程序异常捕获的最后一道防线,避免程序因为没有找到异常匹配而挂了

4、在抛出的过程中,如果跨越了多个函数后找到捕获了,就会直接跳转到对应地方,并不会一层一层地返回。

5、在实际情况中,异常并不是传这种内置类型的,而是通过派生类继承基类来传派生类这种的自定义类型的,所以实际中都会定义一套继承的规范体系。 这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了

// 服务器开发中通常使用的异常继承体系
//作为父类
class Exception
{
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		, _id(id)
	{}
	virtual string what() const
	{
		return _errmsg;
	}
protected:
	string _errmsg;
	int _id;
};
//SQL模块的继承
class SqlException : public Exception
{
public:
	SqlException(const string& errmsg, int id, const string& sql)
		:Exception(errmsg, id)
		, _sql(sql)
	{}
	virtual string what() const
	{
		string str = "SQL异常:";
		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 = "缓存异常:";
		str += _errmsg;
		return str;
	}
};
//Http模块的继承
class HttpServerException : public Exception
{
public:
	HttpServerException(const string& errmsg, int id, const string& type)
		:Exception(errmsg, id)
		, _type(type)
	{}
	virtual string what() const
	{
		string str = "HttpServer异常:";
		str += _type;
		str += ":";
		str += _errmsg;
		return str;
	}
private:
	const string _type;
};

void SQLMgr()
{
	srand(time(0));
	if (rand() % 7 == 0)
	{
		throw SqlException("权限不足", 100, "select * from name = '张三'");
	}
	//throw "xxxxxx";
}

void CacheMgr()
{
	srand(time(0));
	if (rand() % 5 == 0)
	{
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 6 == 0)
	{
		throw CacheException("数据不存在", 101);
	}
	SQLMgr();
}

void HttpServer()
{
	srand(time(0));
	if (rand() % 3 == 0)
	{
		throw HttpServerException("请求资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpServerException("权限不足", 101, "post");
	}
	CacheMgr();
}
int main()
{
	while (1)
	{
		Sleep(500);
		try {
			HttpServer();
			CacheMgr();
			SQLMgr();
		}
			catch (const Exception& e) // 这里捕获父类对象就可以
		{
			// 多态
			cout << e.what() << endl;
		}
		catch (...)
		{
			cout << "未知异常" << endl;
		}
	}
	return 0;
}

3、异常的重新抛出:

在抛出异常时,毕竟是直接跳转的,在中间省略的函数中可能会存在内存泄漏问题,如下,在func函数中,其开辟的数组就不会被释放。

//异常信息类
class Exception
{
public:
	Exception(int errcode, const string& content)
		:_errno(errcode), _content(content)
	{}

	string what() const
	{
		return to_string(_errno) + " : " + _content;
	}
public:
	int _errno = 0;//作为错误编号,在实际中方便查找
	string _content;//记录错误信息
};

double Division(int a, int b)
{
	if (b == 0)
	{
		Exception e(1, "被除数不能为0");
		throw e;
	}
	else
		return (float)a / (float)b;

}

void func()
{
	double x, y;
	cin >> x>> y;
	int* arr = new int[10];

	cout << Division(x,y) << endl;

	delete[] arr;
	cout << "delete[] arr: " << arr << endl;
	
	cout << "void func()" << endl;
}

int main()
{
	try
	{
		func();
	}
	catch(const Exception& err)
	{
		cout << "int main()" << endl;
		err.what();
	}
	catch (...)
	{
		cout << "int main()" << endl;
		cout << "未知错误" << endl;
	}
	return 0;
}

解决方法:

要想正确的释放内存,需要在func函数中主动捕获异常,将空间释放后,重新抛出异常,

所以func函数就需要改进为:

void func()
{
	int* arr = new int[10];
	try {
		int x, y;
		cin >> x >> y;
		cout << Division(x, y) << endl;
	}
	catch (...) {
		delete arr;
		throw;
	}
}

这里捕获异常后并不处理异常,异常还是交给main处理,这里捕获了再重新抛出去,

注意:这里捕获了什么类型的异常就抛出什么类型的异常。

4、异常安全:

1、构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
2、析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏

3、C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,但是才C++11中引入了智能指针来解决这一问题,使用智能指针可以有效地避免因异常和忘记释放内存导致的资源泄漏

5、异常规范:

1、异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的 后面接throw(类型),列出这个函数可能抛掷的所有异常类型。

2、函数的后面接throw(),表示函数不抛异常。

3、若无异常接口声明,则此函数可以抛掷任何类型的异常。

void func1() throw(int, char, string); // 可能抛出这三种类型的异常

void func2() throw(); // 该函数不会抛出异常

void func3(); // 该函数可以抛出任何类型的异常

但是在C++11中引入了noexcept关键字,这样只需标记这个函数不会抛出异常,

但是如果对标记后的函数进行抛异常了,进程就会直接被终止。

四、异常的优缺点:

1、优点:

1、如果使用好了可以展示更多的错误信息,能够更好的定位bug

2、 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么就得层层返回错误,最外层才能拿到错误,而如果在最外层捕获,异常就是直接返回到最外层,不用层层返回。

3、 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常

4、部分函数使用异常更好表示错误,比如 T& operator[](size_t pos) 如果越界了就抛异常,而不是返回 T() 或 断言。

2、缺点:

1、在进行调试的时候代码流的跨度大,导致跟踪程序比较困难

2、因为捕获异常捕获的是一份临时拷贝,所以存在性能开销,但是现如今的设备几乎可以忽略不计

3、C++的标准库异常体系定义的不好,导致各自定义各自的比较混乱

4、异常尽量规范使用:

        a、抛出异常类型都继承自一个基类

        b、函数是否抛异常、使用关键字noexcept的方式规范化

但是异常总之是利大于弊的,在工程中也尽量按规范使用异常