第3课 - 异常类构建
本篇开始正式进入数据结构的相关内容,目标是基于C++语言,设计一个可复用的数据结构类库DTLib。
1. C++异常类简介
- C++异常的类型可以是自定义类类型
- catch语句对于类类型异常的匹配,依旧是自上而下严格匹配
- 赋值兼容性原则(在出现父类对象的地方,可以用一个子类对象来代替)在异常类型匹配中依然适用
- 匹配子类异常的catch放在上面
- 匹配父类异常的catch放在下面
2. 异常类设计
现代C++库必然包含充要的异常类族,异常类是数据结构类所依赖的“基础设施”。
【异常类组成】
DTLib中的异常类由1个顶层父类和5个异常子类组成,其UML类图如下。
【异常类功能定义】
3. 顶层父类Exception设计
【接口设计】
1 class Exception 2 { 3 protected: 4 char *m_message; //抛异常时的提示消息 5 char *m_location; //抛异常的位置,格式为“file:line” 6 7 //私有辅助函数,用于简化三个构造函数实现 8 void init(const char *message, const char *file, int line); 9 public: 10 Exception(const char *message); 11 Exception(const char *file, int line); 12 Exception(const char *message, const char *file, int line); 13 14 Exception(const Exception &e); 15 Exception &operator = (const Exception &e); 16 17 /*在处理异常时,用来获取异常消息和异常发生位置*/ 18 virtual const char *message() const; 19 virtual const char *location() const; 20 21 virtual ~Exception() = 0; 22 };
【构造函数实现】
1. 先来实现init(),之所以设计这个私有函数,是因为重载的三个构造函数内部逻辑是差不多的,所以先在init()中完成该逻辑,然后分别在三个构造函数中调用,可以减小冗余代码。
1 void Exception::init(const char *message, const char *file, int line) 2 { 3 m_message = (message ? strdup(message) : NULL);
4 5 if (file != NULL) 6 { 7 char sline[16] = {0}; 8 9 itoa(line, sline, 10); //将行号转换为字符串 10 11 /*动态申请内存,拷贝文件名,并拼接上行号,最终组成m_loction*/ 12 m_location = static_cast<char *>(malloc(strlen(file) + strlen(sline) + 2)); 13 m_location = strcpy(m_location, file); 14 m_location = strcat(m_location, ":"); 15 m_location = strcat(m_location, sline); 16 } 17 else 18 { 19 m_location = NULL; 20 } 21 }
注意:参数message指向的字符串可能位于堆、栈或其他地方,如果代码第3行直接用m_message = message,会导致DTLib库无法控制字符串的生命周期,这是不安全的,因此使用strdup在库内部单独拷贝保存一份。
2. 接下来实现三个构造函数接口,可以看到有了init()之后,这三个函数的实现都变得非常简单了。
1 Exception::Exception(const char *message) 2 { 3 init(message, NULL, 0); 4 } 5 6 Exception::Exception(const char *file, int line) 7 { 8 init(NULL, file, line); 9 } 10 11 Exception::Exception(const char *message, const char *file, int line) 12 { 13 init(message, file, line); 14 }
【拷贝构造和赋值操作符重载实现】
从init()可以看出,构造异常类对象时在堆空间动态申请了内存,说明拷贝构造和赋值操作内部必须实现为深拷贝,而默认情况下它们都是浅拷贝,需要自行实现拷贝构造函数和重载赋值操作符。
1 Exception::Exception(const Exception &e) 2 { 3 m_message = strdup(e.m_message); 4 m_location = strdup(e.m_location); 5 } 6 7 Exception &Exception::operator = (const Exception &e) 8 { 9 if (this != &e) 10 { 11 free(m_message); 12 free(m_location); 13 14 m_message = strdup(e.m_message); 15 m_location = strdup(e.m_location); 16 } 17 18 return *this; 19 }
【异常消息和异常位置获取函数实现】
这两个函数的实现非常简单,直接将m_message和m_location返回就可以了。
1 const char *Exception::message() const 2 { 3 return m_message; 4 } 5 6 const char *Exception::location() const 7 { 8 return m_location; 9 }
【析构函数实现】
看到这里大家可能会有疑问,既然析构函数被设计为纯虚函数,而纯虚函数的实现应该在子类中完成,为什么还要在这里提供纯虚函数的函数体呢?
其实,这里是一个例外,C++规定:只要自定义了析构函数,不管析构函数是不是纯虚函数,都必须提供实现。否则其子类对象析构时,会因为找不到父类析构函数的实现而导致析构失败。
1 Exception::~Exception() 2 { 3 free(m_message); 4 free(m_location); 5 }
4. 异常子类实现
5个异常子类均继承自Exception,主要实现几个构造函数、拷贝构造函数和赋值操作符重载函数。
1 class ArithmeticException : public Exception 2 { 3 public: 4 ArithmeticException() : Exception(0) { } 5 ArithmeticException(const char *message) : Exception(message) { } 6 ArithmeticException(const char *file, int line) : Exception(file, line) { } 7 ArithmeticException(const char *message, const char *file, int line) : Exception(message, file, line) { } 8 9 ArithmeticException(const ArithmeticException& e) : Exception(e) { } 10 ArithmeticException &operator = (const ArithmeticException &e) 11 { 12 Exception::operator =(e); 13 return *this; 14 } 15 }; 16 17 class NullPointerException : public Exception 18 { 19 public: 20 NullPointerException() : Exception(0) { } 21 NullPointerException(const char *message) : Exception(message) { } 22 NullPointerException(const char *file, int line) : Exception(file, line) { } 23 NullPointerException(const char *message, const char *file, int line) : Exception(message, file, line) { } 24 25 NullPointerException(const NullPointerException& e) : Exception(e) { } 26 NullPointerException &operator = (const NullPointerException &e) 27 { 28 Exception::operator =(e); 29 return *this; 30 } 31 }; 32 33 class IndexOutOfBoundsException : public Exception 34 { 35 public: 36 IndexOutOfBoundsException() : Exception(0) { } 37 IndexOutOfBoundsException(const char *message) : Exception(message) { } 38 IndexOutOfBoundsException(const char *file, int line) : Exception(file, line) { } 39 IndexOutOfBoundsException(const char *message, const char *file, int line) : Exception(message, file, line) { } 40 41 IndexOutOfBoundsException(const IndexOutOfBoundsException& e) : Exception(e) { } 42 IndexOutOfBoundsException &operator = (const IndexOutOfBoundsException &e) 43 { 44 Exception::operator =(e); 45 return *this; 46 } 47 }; 48 49 class NoEnoughMemoryException : public Exception 50 { 51 public: 52 NoEnoughMemoryException() : Exception(0) { } 53 NoEnoughMemoryException(const char *message) : Exception(message) { } 54 NoEnoughMemoryException(const char *file, int line) : Exception(file, line) { } 55 NoEnoughMemoryException(const char *message, const char *file, int line) : Exception(message, file, line) { } 56 57 NoEnoughMemoryException(const NoEnoughMemoryException& e) : Exception(e) { } 58 NoEnoughMemoryException &operator = (const NoEnoughMemoryException &e) 59 { 60 Exception::operator =(e); 61 return *this; 62 } 63 }; 64 65 class InvalidParameterException : public Exception 66 { 67 public: 68 InvalidParameterException() : Exception(0) { } 69 InvalidParameterException(const char *message) : Exception(message) { } 70 InvalidParameterException(const char *file, int line) : Exception(file, line) { } 71 InvalidParameterException(const char *message, const char *file, int line) : Exception(message, file, line) { } 72 73 InvalidParameterException(const InvalidParameterException& e) : Exception(e) { } 74 InvalidParameterException &operator = (const InvalidParameterException &e) 75 { 76 Exception::operator =(e); 77 return *this; 78 } 79 };
5. 抛异常操作简化设计
至此,DTLib异常类构建基本全部完成,按照我们的设计,在抛异常时的代码是这么写的: throw Exception("Exception Test", __FILE__, __LINE__);
如果在每一个需要抛异常的地方都这么写,会比较繁琐,而且代码有冗余,看起来也不太美观,因此设计下面这个宏来简化抛异常的操作。
1 //抛异常,e为异常类类型,m为提示消息字符串 2 #define THROW_EXCEPTION(e, m) (throw e(m, __FILE__, __LINE__))
这样一来,上面的抛异常操作就简化为 THROW_EXCEPTION(Exception, "Exception Test");
6. 异常类完整源代码
最后,给出DTLib异常类的完整源代码。
【Exception.h】
1 #ifndef EXCEPTION_H 2 #define EXCEPTION_H 3 4 namespace DTLib 5 { 6 7 //抛异常,e为异常类类型,m为提示消息字符串 8 #define THROW_EXCEPTION(e, m) (throw e(m, __FILE__, __LINE__)) 9 10 class Exception 11 { 12 protected: 13 char *m_message; //抛异常时的提示消息 14 char *m_location; //抛异常的位置,格式为“file:line” 15 16 //私有辅助函数,用于简化三个构造函数实现 17 void init(const char *message, const char *file, int line); 18 public: 19 Exception(const char *message); 20 Exception(const char *file, int line); 21 Exception(const char *message, const char *file, int line); 22 23 Exception(const Exception &e); 24 Exception &operator = (const Exception &e); 25 26 /*在处理异常时,用来获取异常消息和异常发生位置*/ 27 virtual const char *message() const; 28 virtual const char *location() const; 29 30 virtual ~Exception() = 0; 31 }; 32 33 class ArithmeticException : public Exception 34 { 35 public: 36 ArithmeticException() : Exception(0) { } 37 ArithmeticException(const char *message) : Exception(message) { } 38 ArithmeticException(const char *file, int line) : Exception(file, line) { } 39 ArithmeticException(const char *message, const char *file, int line) : Exception(message, file, line) { } 40 41 ArithmeticException(const ArithmeticException& e) : Exception(e) { } 42 ArithmeticException &operator = (const ArithmeticException &e) 43 { 44 Exception::operator =(e); 45 return *this; 46 } 47 }; 48 49 class NullPointerException : public Exception 50 { 51 public: 52 NullPointerException() : Exception(0) { } 53 NullPointerException(const char *message) : Exception(message) { } 54 NullPointerException(const char *file, int line) : Exception(file, line) { } 55 NullPointerException(const char *message, const char *file, int line) : Exception(message, file, line) { } 56 57 NullPointerException(const NullPointerException& e) : Exception(e) { } 58 NullPointerException &operator = (const NullPointerException &e) 59 { 60 Exception::operator =(e); 61 return *this; 62 } 63 }; 64 65 class IndexOutOfBoundsException : public Exception 66 { 67 public: 68 IndexOutOfBoundsException() : Exception(0) { } 69 IndexOutOfBoundsException(const char *message) : Exception(message) { } 70 IndexOutOfBoundsException(const char *file, int line) : Exception(file, line) { } 71 IndexOutOfBoundsException(const char *message, const char *file, int line) : Exception(message, file, line) { } 72 73 IndexOutOfBoundsException(const IndexOutOfBoundsException& e) : Exception(e) { } 74 IndexOutOfBoundsException &operator = (const IndexOutOfBoundsException &e) 75 { 76 Exception::operator =(e); 77 return *this; 78 } 79 }; 80 81 class NoEnoughMemoryException : public Exception 82 { 83 public: 84 NoEnoughMemoryException() : Exception(0) { } 85 NoEnoughMemoryException(const char *message) : Exception(message) { } 86 NoEnoughMemoryException(const char *file, int line) : Exception(file, line) { } 87 NoEnoughMemoryException(const char *message, const char *file, int line) : Exception(message, file, line) { } 88 89 NoEnoughMemoryException(const NoEnoughMemoryException& e) : Exception(e) { } 90 NoEnoughMemoryException &operator = (const NoEnoughMemoryException &e) 91 { 92 Exception::operator =(e); 93 return *this; 94 } 95 }; 96 97 class InvalidParameterException : public Exception 98 { 99 public: 100 InvalidParameterException() : Exception(0) { } 101 InvalidParameterException(const char *message) : Exception(message) { } 102 InvalidParameterException(const char *file, int line) : Exception(file, line) { } 103 InvalidParameterException(const char *message, const char *file, int line) : Exception(message, file, line) { } 104 105 InvalidParameterException(const InvalidParameterException& e) : Exception(e) { } 106 InvalidParameterException &operator = (const InvalidParameterException &e) 107 { 108 Exception::operator =(e); 109 return *this; 110 } 111 }; 112 113 } 114 115 #endif // EXCEPTION_H
【Exception.cpp】
1 #include "Exception.h" 2 #include <cstring> 3 #include <cstdlib> 4 5 namespace DTLib 6 { 7 8 void Exception::init(const char *message, const char *file, int line) 9 { 10 /* 11 * message指向的字符串可能位于堆、栈或其他地方,如果直接用m_message = message, 12 * 会导致DTLib库无法控制字符串的生命周期,这是不安全的,因此使用strdup在库内部 13 * 单独拷贝保存一份。 14 */ 15 m_message = (message ? strdup(message) : NULL);
16 17 if (file != NULL) 18 { 19 char sline[16] = {0}; 20 21 itoa(line, sline, 10); //将行号转换为字符串 22 23 /*动态申请内存,拷贝文件名,并拼接上行号,最终组成m_loction*/ 24 m_location = static_cast<char *>(malloc(strlen(file) + strlen(sline) + 2)); 25 m_location = strcpy(m_location, file); 26 m_location = strcat(m_location, ":"); 27 m_location = strcat(m_location, sline); 28 } 29 else 30 { 31 m_location = NULL; 32 } 33 } 34 35 Exception::Exception(const char *message) 36 { 37 init(message, NULL, 0); 38 } 39 40 Exception::Exception(const char *file, int line) 41 { 42 init(NULL, file, line); 43 } 44 45 Exception::Exception(const char *message, const char *file, int line) 46 { 47 init(message, file, line); 48 } 49 50 //从init()的实现可以看出,拷贝构造和赋值操作必须为深拷贝 51 Exception::Exception(const Exception &e) 52 { 53 m_message = strdup(e.m_message); 54 m_location = strdup(e.m_location); 55 } 56 57 Exception &Exception::operator = (const Exception &e) 58 { 59 if (this != &e) 60 { 61 free(m_message); 62 free(m_location); 63 64 m_message = strdup(e.m_message); 65 m_location = strdup(e.m_location); 66 } 67 68 return *this; 69 } 70 71 const char *Exception::message() const 72 { 73 return m_message; 74 } 75 76 const char *Exception::location() const 77 { 78 return m_location; 79 } 80 81 /* 82 * C++规定,只要自定义了析构函数,不管是不是纯虚函数,都必须提供实现, 83 * 否则其子类对象析构时,会因为找不到父类析构函数的实现而导致析构失败。 84 */ 85 Exception::~Exception() 86 { 87 free(m_message); 88 free(m_location); 89 } 90 91 }
注:本文整理于狄泰《数据结构开发实战教程》课程内容