StupidBeauty
Read times:383Posted at:Sun Mar 11 01:44:00 2018 - no title specified

《C++面向对象软件设计及构建》文章翻译:4.5 简单静态聚合 ,4.5 Simple Static Aggregation

此处,我们使用一个对矩形进行抽象的类来展示的 静态聚合 static aggregation )概念。 Rectangle类,负责维护一个矩形的位置信息,并且在特定的画布上绘制自身。对于一个Rectangle 对象来说,它在某个画布(Canvas)中的位置,是由一个Location 对象来定义的,这个对象指定的是该个Rectangle 的左上角的坐标。该个矩形的高度和宽度,是由一个Shape 对象来定义的,它会被作为参数传递到Rectangle 的构造函数中去。下表展示了Rectangle 类的接口。


Rectangle 类的接口

class Rectangle

{ private:

   Location upperLeft;

   Location upperRight;

   Location lowerLeft;

   Location lowerRight;

   Shape    area;

public:

   Rectangle (Location corner, Shape shape);

   void MoveUp   (int deltaY);

   void MoveDown (int deltaY);

   void MoveLeft (int deltaX);

   void MoveRight(int deltaX);

   void Draw(Canvas& canvas);

   void Clear(Canvas& canvas);

   ~Rectangle();

};


Rectangle类,会维护自身的状态信息,以加快自身的操作速度。向画布上绘制一个矩形,这个过程,是通过四次单独的DrawLine 操作来完成的。在每次DrawLine操作中,需要传入两个Location 对象作为参数,以指定要绘制的线段的两个端点。为了加快矩形的绘制操作,这四个Location 对象分别会指定该个矩形的其中一个角的坐标。Rectangle类,还提供了若干个公有方法,用于:相对于自己当前的位置来移动位置(MoveUpMoveDownMoveLeftMoveRight方法);在一个画布中绘制自身(Draw方法);以及,从画布中擦除自身(Clear方法)。由于CanvasClear方法需要一个Shape 参数,所以,在Rectangle中也会维护一个Shape 信息,作为自身状态信息的一部分。


Rectangle 类的实现

Rectangle::Rectangle(Location corner, Shape shape)

{ upperLeft  = corner;

  area = shape;

  upperRight = Location(upperLeft.Xcoord() + area.Width(),

                        upperLeft.Ycoord());

  lowerLeft  = Location(upperLeft.Xcoord() ,

                        upperLeft.Ycoord() + area.Height());

  lowerRight = Location(upperLeft.Xcoord() + area.Width(),

                        upperLeft.Ycoord() + area.Height());

}

void Rectangle::MoveUp(int deltaY)

{ upperLeft  = Location(upperLeft.Xcoord(),

                        upperLeft.Ycoord() + deltaY);

  upperRight = Location(upperLeft.Xcoord() + area.Width(),

                        upperLeft.Ycoord());

  lowerLeft  = Location(upperLeft.Xcoord() ,

                        upperLeft.Ycoord() + area.Height());

  lowerRight = Location(upperLeft.Xcoord() + area.Width(),

                        upperLeft.Ycoord() + area.Height())

}

// ... MoveDownMoveLeftMoveRightMoveUp类似

void Rectangle::Draw(Canvas& canvas)

{ canvas.DrawLine(upperLeft,  upperRight);

  canvas.DrawLine(upperRight, lowerRight);

  canvas.DrawLine(lowerRight, lowerLeft);

  canvas.DrawLine(lowerLeft,  upperLeft);

}

void Rectangle::Clear(Canvas& canvas)

{ canvas.Clear(upperLeft, area)

}

Rectangle::~Rectangle() {}


Rectangle类,是一个使用静态聚合的例子,因为,每个Rectangle 对象中所聚合的那四个Location 对象和那个Shape 对象,都是固定的、实际命名的,并在该个类被编写的时候就定义下来的。正如在所有的静态聚合中的那样,被聚合的对象,其生命周期,与进行聚合的对象的生命周期一致。因此,upperLeft 这个Location 对象(还有另外三个Location 对象,以及那个Shape 对象),它的生命周期,与那个Rectangle 对象的生命周期一致,它是后者的一部分:那些Location子对象,以及那个Shape子对象,会随着它们所从属的那个Rectangle 对象的构造而构造;那些LocationShape子对象,会随着它们所从属的那个Rectangle 对象的销毁而销毁。Rectangle 类的那些静态聚合属性,可从该个类的构造函数和析构函数中显式地观察到。

Rectangle的构造函数,会使用构造函数的参数来初始化那四个被封装的Location 对象,以及那个被封装的Shape 对象。实际上,对于每个子对象,此处所进行的初始化,牵涉到两个步骤:构造和初始化。在进入Rectangle 的构造函数主体之前,对于那五个子对象,都会使用它们的默认构造函数(回忆一下,Location类和Shape类都拥有默认构造函数)来进行构造。在 Rectangle 的构造函数的主体中,会通过赋值的方式向那些子对象赋予新的值。这意味着,对子对象的构造及初始化,在狠多情况下都能正常工作,但是:

  • •.如果那些子对象不具有默认构造函数,则此方法不可行,

  • •.对于较大的对象来说,这样做是低效的,首先使用默认构造函数来构造它们,在这个过程中就会进行一次初始化,然后又立即通过赋值的方式替换掉它们的值。

还有另外一种对子对象进行构造的技巧,在4.6 小节的 定时器 示例中展示。

令人惊讶的是,我们定义了Rectangle类的析构函数,然而其中并无任何代码,尽管看起来它似乎应当包含一些代码以引起所包含的子对象的析构。那些子对象,会被自动析构,不需要显式写代码来做这件事。由于以下原因,会进行子对象的自动析构:

  • •.从静态聚合的结构即可明显地推导出,那些子对象必须在外面的包装对象被析构时也进行析构,因此,编译器和运行时系统没有任何理由不去安排这件事的自动完成,并且

  • •.它减轻了程序员的开发负担,无需对子对象进行显式的析构。

必须强调 的是,自动析构 ,只 会针 子对象( 即,那些通过声明来创建的对象 )进行 - 而不会针对任何动态创建的对象( 即,那些通过new 操作来创建的对象 )进行。对于 这条规则,换种方式来说,即是,通过名字 来访问的子对象,会被自动析构,而通过指针来访问的子对象,则不会被自动析构。对于动态分配 的对象,其析构函数的角色,将在 动态聚合 中说明。

构造/析构顺序

为了更好地理解构造和析构过程,此处,我们定义了Location 和Rectangle 类的两个简化版本。下面展示了这两个类的代码。这两个类,没有其它的任何方法 ,只有构造函数和析构函数,并且它们也只是在被调用时生成一行输出内容。简化版本的类,其结构,与原有的Location 和Rectangle 类是类似的:简化版本的Location中,拥有两个整数值,作为它的私有数据,而在Rectangle 类中,拥有四个简化版本的Location 类的子对象。


用于实验的简化版本的类

Location

Rectangle

class Location

{

  private:

        int X, Y;

  public:

        Location(int x, int y);

        Location();

       ~Location();

};

Location::Location(int x, int y)

{ X = x;

  Y = y;

  cout << "Location ("

       << X << "," << Y

       << ") constructor" << endl;

}

Location::Location()

{X = 0;

 Y = 0;

 cout << "Location default"

      << " constructor" << endl;

}

Location::~Location()

{cout << "Location ("

      << X << "," << Y

      << ") destructor" << endl;

}

class Rectangle

{

  private:

        Location loc1;

        Location loc2;

        Location loc3;

        Location loc4;

  public:

        Rectangle(Location& loc);

       ~Rectangle();

};

Rectangle::Rectangle(Location& loc)

{cout << "Rectangle constructor"

      << endl;

 loc1 = loc;

 loc2 = loc;

 loc3 = loc;

 loc4 = loc;

}

Rectangle::~Rectangle()

{ cout << "Rectangle destructor"

       << endl;

}

构造和析构动作的顺序,可通过使用简化版本的Location 和Rectangle 类来写一个小的测试程序,并观察程序的输出内容,来进行跟踪。以下展示了测试程序及其输出内容。

构造/析构顺序示例

测试程序

测试程序的输出内容

void main()

{

  cout << " Begin Test" << endl;

  Location location(10,10);

  Rectangle rect(location);

  cout << " End Test" << endl;

}

 Begin Test

Location (10,10) constructor

Location default constructor

Location default constructor

Location default constructor

Location default constructor

Rectangle constructor

 End Test

Rectangle destructor

Location (10,10) destructor

Location (10,10) destructor

Location (10,10) destructor

Location (10,10) destructor

Location (10,10) destructor

对于构造和析构动作,我们要从测试程序的输出内容中注意的重要事项包括:

  • •.每个构造动作,都有一个对应的析构动作。Location类的构造函数被调用了五次,而Location类的析构函数也被调用了五次。Rectangle类的构造函数和析构函数各自被调用了一次。这表明,所有的对象都被回收了;没有发生内存泄漏。

  • •.析构函数是隐式调用的;没有对它们进行显式调用的语句。当执行到主(main)程序的末尾时,在主程序中声明的那些Rectangle 和Location 对象就离开了其作用域,于是就被自动销毁。

  • •.那四个Location 子对象的析构函数都被执行了。这表明,当包含着子对象的外层对象(在本示例中,即是那个Rectangle 对象)被销毁时,那些子对象也会被销毁。

  • •. 子对象,是在包含着它们的外层对象被构造 之前 进行构造的。从输出内容中,可以看到,那些Location 子对象的构造函数所输出的内容,位于由Rectangle 的构造函数所输出的内容之前。这样的顺序,是经过了精心设计的;它使得,外层的包装对象的构造函数,能够调用其子对象的方法(例如,查询它们的状态,或者将它们连接起来以组成关联)。为了做到这一点,允许外层包装对象的构造函数对它的子对象进行操作,就必须在外层包装对象的构造函数被执行之前,将子对象完整地构造出来。

  • •. 子对象,会在外层包装对象被析构 之后 ,再析构。从输出内容中,可以看到,由Location 子对象的析构函数所输出的内容,位于由Rectangle 的析构函数所输出的内容之后。这个顺序,也是经过精心设计的;它使得,外层包装对象的析构函数,能够调用它的子对象的方法(例如,在对子对象进行析构之前,调整它们的状态。在定时器(StopWatch)示例中,我们将会看到,在对 StopWatch 的子对象进行析构之前,必须先将那个 Clock 子对象停止。这样,就避免了,在析构过程中,那个Clock 子对象还在继续计时的可能性。)。为了做到这一点,允许外层包装对象的析构函数对它的子对象进行操作,就必须确保,在外层包装对象的析构函数执行完毕之后,再对子对象进行析构。


这个示例,展示了子对象的构造和析构顺序方面的主要概念。以下的这些练习,能够帮助妳了解更多细节。

任务

  1. 1.展示妳对于静态聚合的理解,具体任务是,定义、实现并测试一个Circle 类,它拥有一个Location子对象,用于定义这个圆的圆心,以及一个整数值,用于定义这个圆(Circle)的半径。妳所定义的这个Circle 对象,应当能够在某个画布(Canvas)上绘制自身。

  2. 2.展示妳对于静态聚合的理解,具体任务是,定义、实现并测试一个Triangle 类,它拥有三个Location 子对象,用于定义这个三角形的各个顶点。妳所定义的这个Triangle 对象,应当能够在某个画布(Canvas)上绘制自身。

  3. 3. 在前面所提到的测试程序中,无法通过输出内容来判断,主程序中的那个Location 对象,是在那些Location 子对象之前还是之后被析构的。重写那个测试程序,以确定这个顺序。

  4. 4.重写简化版本的Rectangle 类的构造函数,使得,那个Location 对象,是通过复制的方式来传递的,而不是通过引用的方式来传递的。做出这个改变之后,重新运行那个测试程序。 对于使用原始版本的Rectangle 类所写的测试程序与使用修改过的Rectangle 类所写的测试程序的输出内容中的任何差别,做出解释。

  5. 5.修改之前的主测试程序,以包含一个全局的Rectangle 对象(也就是说,在主(main)程序之外声明的一个对象)。做出这个修改之后,重新运行测试程序。对于原始版本的测试程序和带有全局变量的测试程序,它们的输出内容之间的任何差别,做出解释。

  6. 6.修改简化版本的Location 类,删除它的默认构造函数(不带参数的构造函数)。利用这个修改过的Location 类来重新编译测试程序。解释妳所遇到的任何错误消息。

  7. 7.修改简化版本的Rectangle 类,使得,它的私有数据中,包含一个指向某个Location 对象的指针。在Rectangle 类的构造函数中,使用某个通过new 操作符动态分配的Location 对象来初始化这个指针。使用这个修改过的简化版本的Rectangle 类来重新运行测试程序。观察测试程序的输出内容,确认一下,有多少个Location 对象被构造出来了,有多少个Location 对象被析构了。对妳的观察结果进行解释。

未知美人

未知美人

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

为OsoLinux用户提供的RPM包仓库

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