StupidBeauty
Read times:382Posted at:Sun Mar 18 10:06:54 2018 - no title specified

《C++面向对象软件设计及构建》文章翻译:4.7 动态聚合 ,4.7 Dynamic Aggregation

动态聚合,它的显著不同之处,在于,在这样的聚合中,在那些被封装的对象中,最少有一部分是在运行时(通过new操作符)动态创建的。这样情况下,需要使用动态聚合:在这个类被定义的时候,就已经知道那些被封装的子对象的 类型 ,但是并不知道那些对象的 个数 。在运行时,动态聚合必须分配并管理新的子对象。

动态聚合中的子对象,并不会在外层对象被析构时自动地被析构,因为,那些子对象是动态分配的。于是,正确地析构这些子对象,以防止发生内存泄漏,这样一个责任,就落到了程序员身上。未能正确地管理好动态子对象,这是一个常见的错误来源;这些子对象天生的动态性,导致了,这种错误狠难判定。

有一个必须使用动态聚合来表示的抽象概念,即,一个封闭的、多边形形状,它具有不定数量的边。为了展示动态聚合的机制,此处,我们会定义及实现一个类,用于表示多边形形状。这个类,将会允许,在运行时动态地定义该个形状中的各个顶点。这个类必须维护一个顶点列表,该列表的长度在事先是不知道的。在要求向某个画布中绘制自身时,这个类就需要在每一对相邻的顶点之间绘制线段,并需要将最后一个顶点和第一个顶点看成是相邻的顶点。因此,如果有 n 个顶点,则需要绘制 n 条线段:第一条线段连接顶点0和顶点1,第二条线段连接顶点1和顶点2,依此类推,最后一条线段连接顶点n-1和顶点0

以下展示了PolyShape 类的接口。


PolyShape 类的接口

class PolyShape

{

private:

        LocationNode *head;

        LocationNode *tail;

        int currentX, currentY;

        int length;

public:

        PolyShape(int x, int y);

        void Up   (int n);      

        void Down (int n);

        void Left (int n);

        void Right(int n);

        void Mark();

        void Draw(Canvas& canvas);

        ~PolyShape();

};

一个 PolyShape对象 ,在构造时, 需传入它的第一个顶点的位置(Location),即 x y 坐标。 这些坐标,用于初始化这个PolyShape 的当前位置,这个属性是由状态变量 currentX currentY 来表示的。对于构造函数 的其它部分,我们日后再说。 Up Down Left Right方法 ,会改变 当前位置 ,具体改变的程度取决于各个方法的参数。方法 的名字,表示了,要改变 当前位置 的哪个坐标,以及,按照哪种形式来改变。例如,假设 当前位置 是(100, 100),那么 ,调用方法 Up(10) ,会导致改变 Y坐标,使得 当前位置 变成(100, 90) 也就是变成一个较“上面”的位置,因为它离画布区域的顶部更近。类似 地,调用 Right(20) ,会导致 当前位置 从(100, 100)变成(120, 100), 即是一个离画布的右边缘更近20 个像素的位置。下面展示 了这些方法的实现。 Mark()方法 ,会将 当前位置 加入 该个PolyShape 对象所维护的位置(Location)链表 (linked list) 中去。 Draw方法 ,会在画布中绘制这个PolyShape。对于Mark () 和Draw ()方法的实现,则在日后说明。


PolyShape 类的基本方法

PolyShape::PolyShape (int x, int y)

{ currentX = x; currentY = y;

  Location *start = new Location(x,y);

  head = tail = new LocationNode(start);

  length = 1; }

void PolyShape::Up(int n)

{ currentY = currentY - n; }

void PolyShape::Down(int n)

{ currentY = currentY + n; }

void PolyShape::Left(int n)

{ currentX = currentX - n; }

void PolyShape::Right(int n)

{ currentX = currentX + n; }

对于PolyShape 类的设计者来说,有一个狠明显需要解决的问题,就是,在给出了它的各个顶点的位置(Location)之后,该如何维护这样一个有序的列表。由于顶点的个数是未知的(至少在设计时以及编译时是未知的),所以,需要使用某种形式的动态聚合。

用于实现PolyShape 的数据结构的第一种方案,即是,使用链表技术。Location类,提供了便利的手段,以维护一个(x,y)坐标对,但是,它并未提供任何其它手段,使得Location对象能够成为某个列表的成员。尽管我们可以修改Location类,向它加上必要的数据和方法,使得它能够具有与链表相关的能力,但是,此处,我们不这么做。有三个原因,导致我们不按照这种方式来修改Location 类:

  • •.对于Location 这个抽象来说,没有任何特性使得它应当成为某个由Location组成的列表的成员。于是,对Location 类进行修改的话,将会削弱这个类所对应的抽象概念。

  • •.这些附加的方法和数据,将会成为所有的Location 对象的负担,而无论它们是否真的需要这些东西。于是,对Location 类进行修改的话,将会在大量的场景下降低Location 对象的效率。在最坏的情况下,那些并不需要使用由Location组成的列表的应用程序,将无法从我们向Location 类加入的列表机制中获取任何好处。

  • •.这种实现方式,并不总是可行的。在某些情况下,我们想要放入列表中的对象,它们所对应的类无法被修改。例如,这个类是某个库中的类,是由某个软件开发厂商提供的,而其对应的源代码并未提供,因而无法修改。

既然不打算通过修改Location 类的方式来加入链表特性的话,我们就会采用另一种手段,在不修改Location 类的情况下,实现由Location 对象组成的列表。

PolyShape 类中所使用的链表技术,依赖于一个辅助类,即,LocationNode 类。LocationNode类,提供了一种能力,可以用来构造一个由Location 对象组成的链表。下图展示了,一个PolyShape对象、多个LocationNode对象和多个Location对象之间的关系。正如图中所展示的那样,每个LocationNode对象,都包含着一个指针(其内容(contents)),该指针用于标识那个与本LocationNode 对象所配对的Location 对象。同时,这个LocationNode对象,还包含着另外一个指针(下一个(next),该指针用于标识这个由LocationNode组成的列表中的下一个(如果有的话)LocationNode。在每个LocationNode 中,由这两个指针组合起来完成工作,通过使用next 指针,提供一种构造列表的能力,通过使用contents 指针,提供一种表示Location 对象的能力。


PolyShape LocationNode Location对象之间 的关系

正如上图以及PolyShape 类的定义中所展示的那样,每个PolyShape 对象,都维护着两个指针,即为head 和tail,它们表示由LocationNode所组成的链表中的第一个和最后一个元素。

以下展示LocationNode 类的代码。注意,在实现LocationNode 类的过程中,无需对Location 类进行任何修改。于是,通过对LocationNode 类的设计,就避免了之前所说的那三个问题。


LocationNode

class LocationNode

{private:

   LocationNode *next;

   Location *location;

 public:

    LocationNode(Location *loc);

        LocationNode* Next();

        void Next(LocationNode* nxt);

        Location& Contents();

   ~LocationNode();

};

LocationNode::LocationNode(Location *loc)

{ location = loc;

  next = (LocationNode*)0; }

LocationNode* LocationNode::Next()

{ return next; }

void LocationNode::Next(LocationNode* nxt)

{ next = nxt; }

Location& LocationNode::Contents()

{ return *location; }

LocationNode::~LocationNode()

{ delete location; }


注意,在LocationNode 类的析构函数中,会删除由这个LocationNode 自身的contents 指针所指向的Location 那个对象。另外,也需注意,并未对LocationNode 中的next 指针做任何操作。

PolyShape 类的MarkDraw方法,会对由LocationNode组成的链表进行操作,如以下代码所示。Mark方法,会使用 PolyShape 的当前位置来创建一个新的Location 对象,然后,创建一个新的LocationNode 对象,让它的contents 指向刚才创建的那个Location 对象,然后,将刚才创建的LocationNode 添加到由LocationNode组成的链表的末尾。


Draw方法中,会使用Canvas 类的DrawLine 方法来绘制一系列的线段。每条线段,都会连接着从LocationNode链表中取出的两个相邻的Location 对象。以下展示Mark 和Draw 方法的代码。


Mark Draw方法 的代码

void PolyShape::Mark()

{ Location *newPoint   = new Location(currentX, currentY);

  LocationNode *newNode = new LocationNode(newPoint);

  tail->Next(newNode);

  tail = newNode;

  length = length + 1;

}

void PolyShape::Draw(Canvas& canvas)

{ if (length == 1) return;

  LocationNode *node, *next;

  node = head;

  for(int i=0; i<length-1; i++)

  { next = node->Next();

    canvas.DrawLine(node->Contents(),

                    next->Contents());

    node = next;

  }

  canvas.DrawLine(head->Contents(),

                  tail->Contents());

}

在动态聚合中,析构函数,扮演着一个重要角色,它会确保,在这个外层包装(聚合)对象的生命周期中动态创建的子对象,被正确地回收。在PolyShape 对象的析构函数中,必须对该个PolyShape 对象的链表中包含的所有动态创建的LocationNode 和Location 对象进行析构。PolyShape 类的析构函数如下所示。


PolyShape 类的析构函数

PolyShape::~PolyShape()

{ LocationNode *next = head;

  while (next)

  { LocationNode *node = next->Next();

    delete next;

    next = node;

  }

}

PolyShape类的析构函数,会进行一个列表遍历,并依次析构它所遍历到的每个LocationNode。注意,当LocationNode 对象被析构时,会附带析构掉它所指向的Location 对象。因此,PolyShape会直接地析构掉所有的LocationNode 对象,并间接地析构掉所有的Location 对象。

任务

  1. 1.修改LocationNode 和PolyShape 类的析构函数,改成如下形式。给出解释,为何这砣代码能够正确地析构掉PolyShape 中的所有LocationNode 和Location 对象。

LocationNode::~LocationNode()

{ delete contents;

  delete next;

}

PolyShape::~PolyShape()

{ delete head;

}

  1. 2.利用本节中说明的链表技术,定义及实现一个针对Message 对象的链表,要求不要对Message 类的代码做任何修改。以下给出MessageList 类接口的部分规范。在MessageList::Draw 方法中,应当依次调用它的列表中每个Message 对象的Message::Draw 方法。同样地,对于MessageList::Clear 方法,也是如此。在MessageList 类的析构函数中,不应当析构掉那些Message 对象,而应当析构掉所有由MessageList 类创建的动态结构。

class MessageList

{...

  public:

   MessageList();

   void Add(Message& msg);

   void Draw();

   void Clear();

  ~MessageList();

};

续杯

Your opinions
Your name:Email:Website url:Opinion content:
- no title specified

为OsoLinux用户提供的RPM包仓库

 
??Like this article? Give us some tips.??