第4课 - 顶层父类的创建

在开始创建顶层父类之前,先补充两点知识,主要是为了解释为什么需要顶层父类,以及顶层父类在DTLib中的作用。

 

1. 软件架构实践经验

在面向对象软件架构实践中,总结出了以下三条经验:

 - 尽量使用单重继承的方式进行系统设计

 - 尽量保持系统中只存在单一的继承树

 - 尽量使用组合关系代替继承关系

但不幸的是:

 - C++语言的灵活性使得代码中可以存在多个继承树

 - C++编译器的差异使得同样的代码可能表现不同的行为,举个例子,new操作如果失败会发生什么?

2. new操作失败会发生什么

【常见的动态内存分配代码】

 1 /*C代码*/
 2 int *p = (int *)malloc(10 * sizeof(int));
 3 
 4 if (p != NULL)
 5 {
 6     //......
 7 }
 8 
 9 /*C++代码*/
10 int *p = new int(10);
11 
12 if (p != NULL)
13 {
14     //......
15 }

【必须知道的事实】

 - malloc函数申请失败时返回NULL

 - new申请失败时,根据编译器的不同,其结果可能为:

     1.返回NULL(早期C++编译器)

     2.抛出std::bad_alloc异常(现代C++编译器大多采取这种结方式)

【如何跨编译器统一new的行为,提高代码移植性】

 可以考虑的解决方案有:

 - 类层次范围:重载new/delete,不抛出任何异常。

 - 单次动态内存分配:使用nothrow参数,指明不抛出异常。

3. 顶层父类Object设计

【创建Object类的意义】

 - 遵循经典设计准则,DTLib中所有数据结构类都继承自Object,保证单一继承树

 - 在Object类中重载new/delete,统一动态内存申请的行为,提高代码移植性

【接口定义】

 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 #endif // OBJECT_H

【接口实现】

考虑到malloc()申请内存失败是固定返回NULL值,因此new和new[]的重载实现为调用malloc(),delete和delete[]的重载实现为调用free()。

 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 }

需要注意的是,重载的new、delete仅在同时满足以下条件时适用:
1.使用了DTLib命名空间 using namespace DTLib; 
2.动态申请Object的子类对象 Test *obj = new Test(); //class Test : public Object 

【测试示例】

先在new/delete、new[]/delete[]的重载函数中添加一行测试代码:

 1 void *Object::operator new(unsigned int size) throw()
 2 {
 3     cout << "Object::operator new: " << size << endl;
 4     return malloc(size);
 5 }
 6 
 7 void Object::operator delete(void *p)
 8 {
 9     cout << "Object::operator delete: " << p << endl;
10     free(p);
11 }
12 
13 void *Object::operator new[](unsigned int size) throw()
14 {
15     cout << "Object::operator new[]: " << size << endl;
16     return malloc(size);
17 }
18 
19 void Object::operator delete[](void *p)
20 {
21     cout << "Object::operator delete[]: " << p << endl;
22     free(p);
23 }

然后编写main.cpp测试代码:

 1 #include "Object.h"
 2 #include <iostream>
 3 
 4 using namespace DTLib;
 5 using namespace std;
 6 
 7 class Test1 : public Object
 8 {
 9 public:
10     int i;
11     int j;
12 };
13 
14 class Test2 : public Test1
15 {
16 public:
17     int k;
18 };
19 
20 int main()
21 { 
22     Test1 *obj1 = new Test1;
23     Test2 *obj2 = new Test2;
24 
25     cout << "obj1 = " << obj1 << endl;
26     cout << "obj2 = " << obj2 << endl;
27 
28     delete obj1;
29     delete obj2;
30 
31     cout << endl;
32 
33     Test1 *obj3 = new Test1[10];
34     Test2 *obj4 = new Test2[10];
35 
36     cout << "obj3 = " << obj3 << endl;
37     cout << "obj4 = " << obj4 << endl;
38 
39     delete[] obj3;
40     delete[] obj4;
41 
42     return 0;
43 }

运行结果:

PS:测试结果显示obj3和obj4的size分别为124和164,都比理论值多了4字节,这里引用https://blog.csdn.net/hazir/article/details/21413833一文中的部分内容进行解释。

 

如何申请和释放一个数组?

我们经常要用到动态分配一个数组,也许是这样的:

string *psa = new string[10];      //array of 10 empty strings
int *pia = new int[10];           //array of 10 uninitialized ints

上面在申请一个数组时都用到了 new [] 这个表达式来完成,按照我们上面讲到的 new 和 delete 知识,第一个数组是 string 类型,分配了保存对象的内存空间之后,将调用 string 类型的默认构造函数依次初始化数组中每个元素;第二个是申请具有内置类型的数组,分配了存储 10 个 int 对象的内存空间,但并没有初始化。

如果我们想释放空间了,可以用下面两条语句:

delete [] psa;
delete [] pia;

都用到 delete [] 表达式,注意这地方的 [] 一般情况下不能漏掉!我们也可以想象这两个语句分别干了什么:第一个对 10 个 string 对象分别调用析构函数,然后再释放掉为对象分配的所有内存空间;第二个因为是内置类型不存在析构函数,直接释放为 10 个 int 型分配的所有内存空间。

这里对于第一种情况就有一个问题了:我们如何知道 psa 指向对象的数组的大小?怎么知道调用几次析构函数?

这个问题直接导致我们需要在 new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了

还是用图来说明比较清楚,我们定义了一个类 A,但不具体描述类的内容,这个类中有显示的构造函数、析构函数等。那么 当我们调用

class A *pAa = new A[3];

时需要做的事情如下:

从这个图中我们可以看到申请时在数组对象的上面还多分配了 4 个字节用来保存数组的大小,但是最终返回的是对象数组的指针,而不是所有分配空间的起始地址。

这样的话,释放就很简单了:

delete [] pAa;

这里要注意的两点是:

  • 调用析构函数的次数是从数组对象指针前面的 4 个字节中取出;
  • 传入 operator delete[] 函数的参数不是数组对象的指针 pAa,而是 pAa 的值减 4。

4. 异常类改进

 根据所设计的顶层父类Object,对上次创建的异常类进行如下改进:

 【改进点一】

Exception类继承自Object类 class Exception : public Object ,使得在堆空间创建异常类对象失败时,返回NULL指针。

【改进点二】

新增InvalidOperationException非法操作异常类,当类的成员函数被调用时,如果状态不正确(如一些成员函数在类对象刚初始化时不允许使用)则抛出异常

 1 /*
 2  * 非法操作异常:类的成员函数被调用时,如果状态不正确(如一些成员函数在类对象刚初始化时不允许使用)则抛出异常
 3 */
 4 class InvalidOperationException : public Exception
 5 {
 6 public:
 7     InvalidOperationException() : Exception(0) { }
 8     InvalidOperationException(const char *message) : Exception(message) { }
 9     InvalidOperationException(const char *file, int line) : Exception(file, line) { }
10     InvalidOperationException(const char *message, const char *file, int line) : Exception(message, file, line) { }
11 
12     InvalidOperationException(const InvalidOperationException& e) : Exception(e) { }
13     InvalidOperationException &operator = (const InvalidOperationException &e)
14     {
15         Exception::operator =(e);
16         return *this;
17     }
18 };

5. DTLib的开发方式和注意事项

  1. 迭代开发:每次完成一个小的目标,持续开发,最终打造可复用类库
  2. 单继承,单一继承树:所有类都继承自Object,统一规范堆对象创建时的行为
  3. 只抛异常,不处理异常:使用THROW_EXCEPTION抛异常,提高可移植性
  4. 弱耦合性:尽量不使用std标准库中的类和函数,提高可移植性

 

注:本文整理于狄泰《数据结构开发实战教程》课程内容

posted @ 2018-05-27 18:31  原野追逐  阅读(474)  评论(0编辑  收藏  举报