【C++中的异常机制】

88 阅读3分钟

1. 异常的类型匹配与跨函数机制

首先把可能会发生异常的语句放在try语句内,然后通过catch语句接异常,接异常的时候是严格按照类型匹配的(不像函数参数可以进行隐式类型转换)。而抛异常的时候,通过throw语句抛出异常,然后直接转到接收该类型的异常的catch语句处执行,throw后面的语句不再执行,抛出异常的函数后面的语句不再执行(try语句内),这是异常的跨函数特性。异常在接收后也可以不处理继续抛出,但是必须要有接异常,否则程序会挂。

下面通过一个实例分析:

#include <iostream>
using namespace std;

//可以抛出任何类型异常
void TestFunc1(const char* p)
{
	if (p == NULL)
	{
		throw 1;
	}
	else if (!strcmp(p, ""))
	{
		throw 2;
	}
	else
	{
		cout << "the string is : " << p << endl;
	}
}

//只能抛出以下类型异常 int char 
void TestFunc2(const char* p) throw(int, char)
{
	if (p == NULL)
	{
		throw '1';
	}

	cout << "the string is : " << p << endl;
}

//不抛出任何类型异常
void TestFunc3(const char* p) throw()
{
	cout << "the string is : " << p << endl;
}

int main()
{
	char buf1[] = "hello c++";
	char* buf2 = NULL;
	char buf3[] = "";

	try
	{
		//TestFunc1(buf2); //这里抛出异常,那么后面都不再执行,这是异常的跨函数特性
		//TestFunc1(buf3);
		TestFunc2(buf2);
		TestFunc3(buf1);
	}
	catch (int e) //异常严格按照类型进行匹配
	{
		switch(e)
		{
		case 1:
			cout << "the string is NULL" << endl;
			break;
		case 2:
			cout << "the string is space" << endl;
			break;
		default:
			cout << "do not know" << endl;
			break;
		}
	}
	catch (char) //可以只写类型
	{
		cout << "throw char test" << endl;
	}
	catch (...)
	{
		cout << "other err" << endl;
	}
	
	system("pause");
	return 0;
}

2. 栈解旋

栈解旋是指,在抛出异常的时候,在try语句块内部,抛异常前所有在栈上构造的对象都将会被析构。下面通过程序举例说明:

#include <iostream>
using namespace std;

class TestClass
{
public:
	TestClass()
	{
		cout << "构造函数" << endl;
	}
	~TestClass()
	{
		cout << "析构函数" << endl;
	}
};

void CreateObject()
{
	TestClass t1, t2;
	cout << "创建对象" << endl;

	throw TestClass();
}

int main()
{
	try
	{
		CreateObject();
	}
	catch (TestClass t)
	{
		cout << "TestClass 类型异常" << endl;
	}
	catch (...)
	{
		cout << "其他异常" << endl;
	}

	system("pause");
	return 0;
}

运行结果可以看到,抛异常后调用了两次析构函数

3. 异常遇上多态

构造函数和析构函数没有返回值,不能像C语言那样用不同的返回值来判断错误的情概况,所以可以通过异常机制来处理错误的情况。

下面给出一个综合案例,该案例包含了异常、多态、类内部包含类、深拷贝、操作符重载、虚函数重写、等综合性知识。(该综合案例将C++的很多语法结合在一起,综合性较强,值得反复练习调试

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
using namespace std;

class People
{
public: //构造析构函数
	People(int age, const char* name);
	~People();
public: //重载操作符
	char& operator[](int index);
	friend ostream& operator<<(ostream& out, People& p);
public: //提供外部访问私有属性的接口
	int get_age();
public: //内部类定义异常类型,实现捕获异常的多态行为
	class PeopleErrType
	{
	public:
		PeopleErrType(const char* p);
		~PeopleErrType();
	public:
		virtual void print_err_type() = 0; //People 类中异常的统一接口
	protected:
		char* err_type_str;
	};
	class PeopleAgeTooSmall : public PeopleErrType
	{
	public:
		PeopleAgeTooSmall(const char* p) : PeopleErrType(p) {}; //只用于初始化参数列表传给父类
	public:
		virtual void print_err_type();
	};
	class PeopleAgeTooBig : public PeopleErrType
	{
	public:
		PeopleAgeTooBig(const char* p) : PeopleErrType(p) {};
	public:
		virtual void print_err_type();
	};
	class PeopleNameIsNULL : public PeopleErrType
	{
	public:
		PeopleNameIsNULL(const char* p) : PeopleErrType(p) {};
	public:
		virtual void print_err_type();
	};
	class PeopleNameErr : public PeopleErrType
	{
	public:
		PeopleNameErr(const char* p) : PeopleErrType(p) {};
	public:
		virtual void print_err_type();
	};
private: //私有属性
	int age; //年龄
	char* name; //名称
};

// People 类的构造函数,会抛出异常
People::People(int age, const char* name)
{
	if (age <= 0)
	{
		throw PeopleAgeTooSmall("异常:年龄小于0");
	}
	if (age > 200)
	{
		throw PeopleAgeTooBig("异常:年龄大于200");
	}
	if (name == NULL)
	{
		throw PeopleNameIsNULL("异常:名称为空");
	}
	if ((name[0] < 'A') || (name[0] > 'Z'))
	{
		throw PeopleNameErr("异常:名称错误,(name[0] < 'A') || (name[0] > 'Z')");
	}

	this->age = age;
	this->name = new char[strlen(name) + 1];
	strcpy(this->name, name);
}

// People 类的析构函数
People::~People()
{
	if (this->name != NULL)
	{
		delete[] this->name;
	}
	this->name = NULL;
	this->age = 0;
}

//外部访问私有属性的接口
int People::get_age()
{
	return this->age;
}

//重载 [] 操作符
char& People::operator[](int index)
{
	return this->name[index];
}

//重载左移操作符
ostream& operator<<(ostream& out, People& p)
{
	out << "name: " << p.name << " age: " << p.age << endl;
	return out;
}

//People 类的内部类 构造函数,为输出字符串分配空间
People::PeopleErrType::PeopleErrType(const char* p)
{
	this->err_type_str = new char[strlen(p) + 1];
	strcpy(this->err_type_str, p);
}

People::PeopleErrType::~PeopleErrType()
{
	if (this->err_type_str != NULL)
	{
		delete[] this->err_type_str;
	}
	this->err_type_str = NULL;
}

//异常类型:年龄太小,小于0
void People::PeopleAgeTooSmall::print_err_type()
{
	cout << "err: PeopleAgeTooSmall -> " << err_type_str << endl;
}

//异常类型:年龄太大超出范围
void People::PeopleAgeTooBig::print_err_type()
{
	cout << "err: PeopleAgeTooBig -> " << err_type_str << endl;
}

//异常类型:名称为空
void People::PeopleNameIsNULL::print_err_type()
{
	cout << "err: PeopleNameIsNULL -> " << err_type_str << endl;
}

//异常类型:名称错误
void People::PeopleNameErr::print_err_type()
{
	cout << "err: PeopleNameErr -> " << err_type_str << endl;
}

int main()
{
	try
	{
        //通过以下4个测试例子测试4种异常
		//People p(0, "Aily");
		//People p(16, NULL);
		//People p(300, "Aily");
		//People p(16, "aily");
		People p(16, "Aily");
		cout << "人员信息:" << p << "\t代号:" << p[0] << p.get_age() << endl;
	}
	catch (People::PeopleErrType & e) //产生多态,自动识别异常类型
	{
		e.print_err_type();
	}
	catch (...)
	{
		cout << "其他未知异常" << endl;
	}

	system("pause");
	return 0;
}

将测试代码依次放开,逐步调试便可以观察到程序的每一步动态

//通过以下4个测试例子测试4种异常
//People p(0, "Aily");
//People p(16, NULL);
//People p(300, "Aily");
//People p(16, "aily");
//People p(16, "Aily");

4. 异常变量的生命周期

当我们throw出类对象时,使用catch捕获异常时有三种选择,分别是捕获对象元素、捕获引用和捕获指针,那么这三种情况下,捕获到的变量是如何分配内存,他的生命周期又是如何呢,首先结论如下:

  • 捕获类对象的元素:调用拷贝构造函数把抛出的对象元素拷贝给catch的参数对象元素,调用拷贝构造函数;
  • 捕获类对象的引用:catch语句中的对象直接使用抛出的对象;
  • 捕获类对象的指针:需要手动new和delete控制内存;

结论如上,下面通过一个程序详细探究(提示:因为catch严格按照类型匹配进行接异常,所以catch元素和catch引用不能同时出现)。

#include <iostream>
using namespace std;

class pIsNULL
{
public:
	pIsNULL()
	{
		cout << "pIsNULL 无参构造函数" << endl;
	}
	//pIsNULL(pIsNULL& p)
	//错误	C2440	“throw” : 无法从“pIsNULL”转换为“pIsNULL”
	//错误(活动)	E0334	类 "pIsNULL" 没有适当的复制构造函数	
	pIsNULL(const pIsNULL& p) //拷贝构造函数要加 const
	{
		cout << "pIsNULL 拷贝构造函数" << endl;
	}
	~pIsNULL()
	{
		cout << "pIsNULL 析构函数" << endl;
	}
public:
	void print_err_type()
	{
		cout << "异常原因:指针指向NULL" << endl;
	}
};

void print_str(char* str)
{
	if (str == NULL)
	{
		throw pIsNULL(); //调用无参构造函数
	}
	cout << str << endl;
}

void TestFunc1()
{
	char buf1[] = "hello";
	char* buf2 = NULL;

	try
	{
		print_str(buf2);
	}
	catch (pIsNULL e) //调用拷贝构造函数,将 throw 出的对象复制给 e
	{
		e.print_err_type();
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
}

void TestFunc2()
{
	char buf1[] = "hello";
	char* buf2 = NULL;

	try
	{
		print_str(buf2);
	}
	catch (pIsNULL& e) //不会调用拷贝构造函数
	{
		e.print_err_type();
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
}

void print_str2(char* str)
{
	if (str == NULL)
	{
		throw new pIsNULL;
	}
	cout << str << endl;
}

void TestFunc3()
{
	char buf1[] = "hello";
	char* buf2 = NULL;

	try
	{
		print_str2(buf2);
	}
	catch (pIsNULL* e)
	{
		e->print_err_type();
		delete e;
	}
	catch (...)
	{
		cout << "未知异常" << endl;
	}
}

int main()
{
	TestFunc1(); //用对象元素接异常
	//TestFunc2(); //用引用接异常
	//TestFunc3(); //用指针接

	system("pause");
	return 0;
}

分别在主函数中调用三个测试函数,观察打印结果:

①在主函数中调用第一个测试函数,用元素捕获异常

TestFunc1(); //用对象元素接异常

打印结果如下

图片转存失败,建议将图片保存下来直接上传转存失败,建议直接上传图片文件

可以看到,在catch的时候会将throw处构造的对象通过拷贝构造函数复制给catch语句中的元素e,因为这里一共有两个对象,所以在异常结束时会调用两次析构函数,分别析构两个对象。

②在主函数调用第二个测试函数,用引用捕获异常

TestFunc2(); //用引用接异常

运行结果如下

使用引用捕获异常的时候会直接使用throw处构造的对象,所以不会调用拷贝构造函数,只调用一次析构函数。

③在主函数调用第三个测试函数,用指针捕获异常

TestFunc3(); //用指针接

抛出指针类型的异常最好手动new和delete来管理内存。

5. 使用异常变量输出错误信息

通过抛异常可以直接把异常的信息抛出来,在捕获异常的时候直接打印异常信息,下面通过一个简单的例子说明。

#include <iostream>
using namespace std;

void print_str(const char* str)
{
	if (str == NULL)
	{
		throw "str is null"; //分配内存
	}

	cout << str << endl;
}

int main()
{
	const char* p = NULL;

	try
	{
		print_str(p);
	}
	catch (const char* pStr)
	{
		cout << "出现 const char* 异常:" << pStr << endl; //pStr是在哪分配内存的呢?在throw处
	}
	//catch (char* pStr) //无法捕获到 const cha* 类异常
	//{
	//	cout << "出现 char* 异常:" << pStr << endl;
	//}
	//catch (const char*) //捕获 const char* 类型异常但不使用异常变量
	//{
	//	cout << "出现const char*异常" << endl;
	//}
	catch (...)
	{
		cout << "其他异常" << endl;
	}

	system("pause");
	return 0;
}