我们学习数据结构,为什么要创建顶层父类呢?
我们的目标是创建一个可复用的数据结构类库,可复用指的就是在不同的工程里面,我们可以使用本次创建的数据结构库,在不同的编译器、不同的工程、不同的背景下使用这个库都是可以的。既然涉及到了工程应用,我们就需要考虑在当今的软件架构方法中是如何创建一个库的。
当代软件架构实践中的经验:
尽量使用单重继承的方式进行系统设计,不要使用多继承,可以使用多个接口。不要让一个类继承多个父类
尽量保持系统中只存在单一的继承树,通过创建一个顶层的抽象父类来保证,我们要创建的DTLib要遵循这一条准则,使这个库只存在单一的继承树
尽量使用组合关系代替继承关系
不幸的事实:
C++代码的灵活性使得代码中可以存在多个继承树
C++编译器的差异使得同样的代码可能表现不同的行为,例如new操作失败会发生什么?
古代的编译器在new失败时会返回一个空指针,而现代的编译器在new失败时会抛出一个标准库中的异常。
不同的行为给我们创建库带来了挑战,如果new失败了,我们在库里怎么处理?既然是一个复用库,我们不能假设使用的编译器的类型,我们希望在任何编译器中new都有相同的行为。
创建DTLib::Object类的意义
遵循经典的设计准则,所有数据结构都继承自Object类,这样就保证了单一的继承树
定义动态内存申请的行为,提高代码的移植性。我们自己来规范动态内存申请时候的行为,不使用编译器默认提供的new的行为,我们自定义一个行为,这样就可以保证不同的编译器中的行为是一致的
顶层父类接口定义:
顶层父类中需要重载new和delete操作符,重载它们的意义在于,我们希望new或者delete这个库中的类对象时,有我们期望的行为,并且这个行为在不同的编译器下都是相同的。此外,将析构函数定义为纯虚函数,这样可以使得顶层父类是一个抽象类,这样的析构函数定义也可以保证这个类的所有子类当中都有虚函数表的指针了,这就可以让我们使用动态类型识别相关的技术了。
定义Object类,如下:
1 #ifndef OBJECT_H 2 #define OBJECT_H 3 4 namespace DTLib 5 { 6 7 class Object 8 { 9 public: 10 void* operator new (unsigned int size) throw(); 11 void operator delete (void* p); 12 void* operator new[] (unsigned int size) throw(); 13 void operator delete[] (void* p); 14 virtual ~Object() = 0; 15 }; 16 17 } 18 19 20 #endif // OBJECT_H
Object类中操作符重载的具体实现如下:Object.cpp
1 #include "Object.h" 2 #include <cstdlib> 3 #include <iostream> 4 5 using namespace std; 6 7 namespace DTLib 8 { 9 10 void* Object::operator new(unsigned int size) throw() 11 { 12 cout << "Object::operator new : " << size << endl; 13 return malloc(size); 14 } 15 16 void Object::operator delete(void* p) 17 { 18 cout << "Object::operator delete : " << p << endl; 19 free(p); 20 } 21 22 void* Object::operator new[](unsigned int size) throw() 23 { 24 return malloc(size); 25 } 26 27 void Object::operator delete[](void* p) 28 { 29 free(p); 30 } 31 32 Object::~Object() 33 { 34 35 } 36 37 }
上面程序中的throw()是异常规格说明,这个异常说明表示,这个new不会抛出任何的异常,如果真的无法在堆空间中申请内存的时候,直接返回空。这样就可以保证new在失败的时候返回一个空值,而不是抛出异常。
main函数测试程序如下:
1 #include <iostream> 2 #include "Exception.h" 3 #include "Object.h" 4 using namespace std; 5 using namespace DTLib; 6 7 class Test : public Object 8 { 9 public: 10 int i; 11 int j; 12 }; 13 14 class Child : public Test 15 { 16 public: 17 int k; 18 }; 19 20 int main() 21 { 22 Object *obj1 = new Test(); 23 Object *obj2 = new Child(); 24 25 cout << "obj1 = " << obj1 << endl; 26 cout << "obj2 = " << obj2 << endl; 27 28 delete obj1; 29 delete obj2; 30 31 return 0; 32 }
上面我们定义了Test类和Child类,它们最终都继承自Object。
执行结果如下:
可见在堆中申请Test类对象时,最终调用到了我们自己重载的new操作符,并且成功打印出了size的值。调用delete时,也成功调用到了我们重载的delete操作符。
因此,如果我们的数据结构库中的类都继承自这个Object类,我们就可以保证从堆空间创建数据结构库中对象的时候,必然是我们提供的new和delete的实现。这就是顶层父类的关键作用所在。
将不必要的打印删除,给出最终数据结构库中的Object.cpp如下:
1 #include "Object.h" 2 #include <cstdlib> 3 4 namespace DTLib 5 { 6 7 void* Object::operator new(unsigned int size) throw() 8 { 9 return malloc(size); 10 } 11 12 void Object::operator delete(void* p) 13 { 14 free(p); 15 } 16 17 void* Object::operator new[](unsigned int size) throw() 18 { 19 return malloc(size); 20 } 21 22 void Object::operator delete[](void* p) 23 { 24 free(p); 25 } 26 27 Object::~Object() 28 { 29 30 } 31 32 }
小结:
Object类是DTLib中数据结构类的顶层父类
Object类用于统一动态内存申请的行为
在堆中创建Object子类对象,失败时返回NULL指针
Object类为纯虚父类,所有子类都能进行动态类型识别