zoukankan      html  css  js  c++  java
  • 第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标准库中的类和函数,提高可移植性

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

  • 相关阅读:
    PHP获取指定分钟数的下一个整数倍
    phpspreadsheet
    澳大利亚 主要城市列表
    db2编目抽取
    openssl实现CA自签证书和颁发数字证书
    基于Docker的redis集群搭建
    Python测试DB2连通性
    在Vim中查看文件编码
    搭建redis集群
    Python(十)之GUI编程
  • 原文地址:https://www.cnblogs.com/songhe364826110/p/9069739.html
Copyright © 2011-2022 走看看