《C++面向对象软件设计及构建》文章翻译:4.7 动态聚合 ,4.7 Dynamic Aggregation
动态聚合,它的显著不同之处,在于,在这样的聚合中,在那些被封装的对象中,最少有一部分是在运行时(通过new操作符)动态创建的。这样情况下,需要使用动态聚合:在这个类被定义的时候,就已经知道那些被封装的子对象的 类型 ,但是并不知道那些对象的 个数 。在运行时,动态聚合必须分配并管理新的子对象。
动态聚合中的子对象,并不会在外层对象被析构时自动地被析构,因为,那些子对象是动态分配的。于是,正确地析构这些子对象,以防止发生内存泄漏,这样一个责任,就落到了程序员身上。未能正确地管理好动态子对象,这是一个常见的错误来源;这些子对象天生的动态性,导致了,这种错误狠难判定。
有一个必须使用动态聚合来表示的抽象概念,即,一个封闭的、多边形形状,它具有不定数量的边。为了展示动态聚合的机制,此处,我们会定义及实现一个类,用于表示多边形形状。这个类,将会允许,在运行时动态地定义该个形状中的各个顶点。这个类必须维护一个顶点列表,该列表的长度在事先是不知道的。在要求向某个画布中绘制自身时,这个类就需要在每一对相邻的顶点之间绘制线段,并需要将最后一个顶点和第一个顶点看成是相邻的顶点。因此,如果有 n 个顶点,则需要绘制 n 条线段:第一条线段连接顶点0和顶点1,第二条线段连接顶点1和顶点2,依此类推,最后一条线段连接顶点n-1和顶点0。
以下展示了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 (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 类的定义中所展示的那样,每个PolyShape 对象,都维护着两个指针,即为head 和tail,它们表示由LocationNode所组成的链表中的第一个和最后一个元素。
以下展示LocationNode 类的代码。注意,在实现LocationNode 类的过程中,无需对Location 类进行任何修改。于是,通过对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 类的Mark和Draw方法,会对由LocationNode组成的链表进行操作,如以下代码所示。Mark方法,会使用 PolyShape 的当前位置来创建一个新的Location 对象,然后,创建一个新的LocationNode 对象,让它的contents 指向刚才创建的那个Location 对象,然后,将刚才创建的LocationNode 添加到由LocationNode组成的链表的末尾。
在Draw方法中,会使用Canvas 类的DrawLine 方法来绘制一系列的线段。每条线段,都会连接着从LocationNode链表中取出的两个相邻的Location 对象。以下展示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() { LocationNode *next = head; while (next) { LocationNode *node = next->Next(); delete next; next = node; } } |
PolyShape类的析构函数,会进行一个列表遍历,并依次析构它所遍历到的每个LocationNode。注意,当LocationNode 对象被析构时,会附带析构掉它所指向的Location 对象。因此,PolyShape会直接地析构掉所有的LocationNode 对象,并间接地析构掉所有的Location 对象。
任务
1.修改LocationNode 和PolyShape 类的析构函数,改成如下形式。给出解释,为何这砣代码能够正确地析构掉PolyShape 中的所有LocationNode 和Location 对象。
LocationNode::~LocationNode() { delete contents; delete next; } PolyShape::~PolyShape() { delete head; } |
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 opinionsHxLauncher: Launch Android applications by voice commands