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;
}