Loading

C++ 异常处理

 

assert

有时需要在一些特定的地方主动报错以避免更大的问题,这时就需要使用 assert 断言

#include <cassert>
assert(condition);

如果 condition 为真,则什么都不做;如果 condition 为假,则停止程序

 

我们需要自己寻找程序中的异常,并进行分类,因此需要捕获异常

try
{
	// 需要执行的语句
}
catch (错误类型 e1)
{
	// 当出现错误 e1 时执行的语句
}
catch (错误类型 e2)
{
	// 当出现错误 e2 时执行的语句
}

如果没有 catch 语句,出现异常会直接终止程序; C++ 允许定义多条 catch 语句,让每条 catch 语句分别对应一种可能的异常类型

如果我们不知道具体是什么样的异常,可以使用 ... 作为捕获参数

catch (...) 
{
	// 当出现错误时执行的语句
}  

它可以捕获任何类型的异常。

 

throw

在程序中,可以用 throw 保留字来抛出一个异常,在某个 try 语句块里执行过 throw 语句,在它后面的所有语句(截止到这个 try 语句块末尾)将永远不会被执行

类型 函数名(参数) throw(数据类型);

例如我们可以定义抛出指定类型的异常

int check() throw (const char *) 
{
	throw "这里出现错误!";
}

如果没有使用这种语法来定义函数,就意味着函数可以抛出任意类型的异常。有些编译器不支持这种语法,则可以改为

int check()
{
	throw "这里出现错误!";
}

下面展示一个可以抛出和捕获异常的例子

try
{
	check(); // 抛出错误
	throw "Hello";
}
catch (char * e)
{
	cout << e << endl;
}

第一个 check() 已经抛出异常,之后的 throw 就不会执行了。

 

基本思想

传统的异常处理方式是通过函数返回值来判断,而 C++ 提供的机制使得异常的引发和异常的处理不必在同一个函数中

这样底层的函数就不必过多地考虑异常处理的问题,上层调用者可以在不同的位置设置不同的异常处理机制。

 

栈解旋

栈解旋是一种现象,当异常被抛出时,在此之前所有定义在栈上的变量会全部析构,析构顺序与构造顺序相反。

class A
{
public:
	A()
	{
		cout << "A is created." << endl;
	}

	~A()
	{
		cout << "A is deleted." << endl;
	}
};

void test()
{
	try
	{
		A a1, a2;
		// 抛出异常
		throw 1;
	}
	catch (int e)
	{
		// 此时 a1 a2 已经析构
		cerr << "Something wrong." << endl;
	}
}

int main()
{
	test();
	system("pause");

	return 0;
}

 

异常接口声明

为了增强可读性,可以在函数声明中添加关于异常抛出的信息

// 此函数可以抛出任何异常
void test()
{
	// ...
}

// 此函数只能抛出 int, char, char* 异常
void test() throw(int, char, char*)
{
	// ...
}

// 此函数不能抛出任何异常
void test() throw()
{
	// ...
}

 

作用域

一般类型

对于一般类型, throw 的类型值赋值给异常抛出的变量

void test()
{
	int x = 6;
	throw x;	// x 的值会赋给 e
}

void deal()
{
	try
	{
		test();
	}
	catch (int e)
	{
		cerr << "Something wrong." << endl;
	}
}

并且引用类型异常和一般类型异常等价

void deal()
{
	try
	{
		test();
	}
	catch (int &e)
	{
		cerr << "Something wrong &." << endl;
	}
	catch (int e)
	{
		cerr << "Something wrong." << endl;
	}
}

会按照先后顺序来 catch ,即如果引用在前,就会进入引用异常;如果一般类型在前,就会进入一般类型异常。

 

指针类型

抛出 char* 类型的异常时,如果值为常量,就可以直接获取

void test()
{
	throw "abc";	// 因为常量存放在全局区,因此可以直接传递给 e
}

void deal()
{
	try
	{
		test();
	}
	catch (char* e)
	{
		cerr << "Something wrong." << endl;
	}
}

 

类异常

抛出自定义类的异常,此时需要显示调用构造函数抛出异常,并且会用拷贝构造函数创建新对象作为 e

void test()
{
	throw A();	// 需要显式调用构造函数
}

void deal()
{
	try
	{
		test();
	}
	// e 的值是通过拷贝构造函数获得的
	catch (A e)
	{
		cerr << "Something wrong." << endl;
	}
}

对于类对象,抛出引用类型的异常和非引用类型异常不能同时出现。这可能是因为引用类型异常会直接将抛出对象转化为 e 而不是用拷贝构造函数重新构造对象

void deal()
{
	try
	{
		test();
	}
	// e 的值是匿名对象 A() 直接转化得到的
	catch (A &e)
	{
		cerr << "Something wrong." << endl;
	}
}

 

对于指针类型的异常,只能选择在抛出位置 new 一个新的对象,然后由 e 获取指针

void test()
{
	throw new A;	// 不用调用构造函数,因为 new 会自动调用
}

void deal()
{
	try
	{
		test();
	}
	// e 获取 A 的地址
	catch (A *e)
	{
		cerr << "Something wrong." << endl;
		// 要销毁指针
		delete e;
	}
}

如果直接取 throw 变量的地址,由于作用域的问题,该对象会析构, e 会得到一个野指针

void test()
{
	throw &(A());	// 直接返回地址的话,对象离开作用域就会析构
}

可以看出通过指针来抛出异常,就不得不在两个函数中分别 new 和 delete ,因此并不建议这样使用。

 

自定义异常类

class MyExcept 
{
public:
    MyExcept(string _type) 
    {
        type = _type;
    }
    void what() 
    {
        cout << type << endl;
    }
    
private:
    string type;
};

try
{
    throw MyExcept("Hello");
}
catch (const MyExcept &e) 
{
    e.what();
}

 

异常的层次结构

实际应用中,通常是在需要进行异常处理的类中添加异常类来进行异常抛出,例如

class A
{
public:
	A()
	{
		cout << "A is created." << endl;
	}

	// 定义类作为不同的类型
	class eNegative
	{
	};

	class eZero
	{
	};
};

void test()
{
	try
	{
		// ... 抛出类型异常
		throw A::eNegative();
	}
	catch (A::eNegative e)
	{
		// ...
	}
	catch (A::eZero e)
	{
		// ...
	}
}

 

为了提供更加丰富的异常处理,以及简化处理过程,可以对异常类实行继承。

class A
{
public:
	A()
	{
		cout << "A is created." << endl;
	}

	// 定义基类异常类
	class eErr
	{
	public:
		virtual void printErr()
		{
			cout << "eError" << endl;
		}
	};

	class eNegative : public eErr
	{
		void printErr()
		{
			cout << "eNegative" << endl;
		}
	};

	class eZero : public eErr
	{
		void printErr()
		{
			cout << "eZero" << endl;
		}
	};
};

void test()
{
	try
	{
		// ...
		throw A::eNegative();
	}
	catch (A::eErr &e)
	{
		e.printErr();
	}
}

注意这里我们使用了虚函数用来实现多态,这样就可以在不同子类中设置不同的异常处理方式。由于多态特性,只需要通过基类对象的引用类型就可以 catch 所有子类的异常,再利用虚函数简化代码流程。

 

标准异常类

C++ 头文件 <stdexcept> 中定义了系统提供的异常类。它是一个 exception 类,然后派生出多种不同的异常类型。例如

class A
{
public:
	void test()
	{
		try
		{
			throw out_of_range("超出范围");
		}
		catch (const std::exception &e)
		{
			std::cerr << e.what() << '\n';
		}
	}
};

其中 out_of_range 就是 exception 类的派生类。利用多态,只需要一个分支来获取 exception 进行输出。

 

exception 类中有一个虚函数,用于输出错误信息,声明为:

virtual const char *what() const throw();

因此可以自定义派生的异常类

class TypeErr : public exception
{
public:
	TypeErr(const char *p) : ep(p) {}
	virtual const char *what()
	{
		cout << "类型错误" << endl;
		return ep;
	}

private:
	char *ep;
}
posted @ 2022-03-17 18:15  Bluemultipl  阅读(332)  评论(0)    收藏  举报