4.5 Simple Static Aggregation


 

The concept of a static aggregation is illustrated by a class that captures the abstraction of a rectangle. The Rectangle class will be responsible for maintaining the position of the rectangle and drawing itself in a specified canvas. The position of a Rectangle object within a Canvas will be defined by a Location object that gives the coordinates of the upper left-hand corner of the Rectangle. The height and width of the rectangle are defined by a Shape object that is passed as an argument to the Rectangle's constructor. The interface of the Rectangle class is shown below.



Interface of the Rectangle Class
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();
};


The Rectangle class maintains its state information to expedite its operations. Drawing a rectangle on a canvas is accomplished by four separate DrawLine operations. Each DrawLine operation requires two Location object parameters that specify the endpoints of the line. To expedite the drawing of the rectangle, each of the four Location objects specifies the coordinates of one of the corners of the rectangle. The Rectangle class is also provided with public methods to change its position relative to its current location (the MoveUp, MoveDown, MoveLeft, and MoveRight methods), to draw itself on a canvas (the Draw method), and to erase itself from a canvas (the Clear method). Since the Canvas's Clear method requires a Shape parameter, the Rectangle maintains the Shape information as part of its state information as well.



Rectangle Class Implementation
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())
}
// ... MoveDown, MoveLeft, MoveRight similar to MoveUp

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() {}


The Rectangle class is an example of a static aggregation because the four Location objects and the Shape object aggregated in each Rectangle object are fixed, named, and defined at the time that the class is written. As in all static aggregations, the lifetime of the aggregated objects is identical to that of the aggregating object. Thus, the lifetime of the upperLeft Location object (and the other three Location objects and the Shape object as well) is exactly the same as the lifetime of the Rectangle object of which it is a part: the Location sub-objects and the Shape sub-object are constructed when the Rectangle object of which they are a part is constructed and the Location and Shape sub-objects are destructed when that Rectangle object is destructed. The static aggregation properties of the Rectangle class can be seen explicitly in the constructor and destructor of the class.

The Rectangle constructor initializes the four encapsulated Location objects and the encapsulated Shape object using the constructor's parameters. The initialization used here actually involves two steps for each sub-object: construction and initialization. Before the body of the Rectangle constructor is entered, each of the five sub-objects is constructed using their default constructors (recall that the Location class and the Shape class have default constructors). In the body of the Rectangle constructor, the sub-objects are given new values through assignment. This means of constructing and initializing the sub-objects will work in many cases but:

  • it is not possible in those cases where the sub-object does not have a default constructor,
  • it is inefficient for large objects to construct them using the default constructor, which does one initialization, and then immediately replace these values by assignment.

A different technique for constructing the sub-objects is shown in the StopWatch example in section 4.6.

Somewhat surprisingly, the Rectangle destructor is defined but has no code when it would seem that some actions should be taken to cause the destruction of the contained sub-objects. The sub-objects are destructed automatically, they do not have to be explicitly programmed to do so. The automatic destruction of the sub-objects is done because:

  • it is clear from the structure of a static aggregation that the sub-objects must be destructed when the containing object is destructed, so there is no reason for the compiler and run-time system not to arrange for this to be done automatically, and
  • it relieves the programmer of the task of programming the explicit destruction of the sub-objects.

It must be emphasized that the automatic destruction occurs only for sub-objects (objects created via a declaration) - not for any objects that were created dynamically (objects created via a new operation). Another way to state this rule is that subojects accessed by name are automatically destructed while sub-objects accessed via pointers are not automatically destructed. The role of the destructor in the case of dynamically allocated objects is seen in dynamic aggregations .

Construction/Destruction Sequence

To understand better the construction and destruction processes, two trivialized versions of the Location and Rectangle classes are defined here. The code of these two classes is shown below. Neither class has any methods except for their constructors and destructors, all of which generate a line of output when they are executed. The structures of the trivialized classes are similar to the original Location and Rectangle classes: the trivialized Location has two integer values as its private data and the Rectangle class has four trivialized Location sub-objects.



Trivialized Classes for Experimentation
Location class
Rectangle class
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;
}

The sequence of construction and destruction actions can be followed by using the trivialized Location and Rectangle classes in a small test program and observing the output of the program. The test program and its output are shown below.

Example of Construction/Destruction Sequence
Test Program
Output of Test Program
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 

The important things to notice from the output of the test program about the construction and destruction actions are:

  • Each constructor action is matched by a corresponding destructor action. The Location constructors were called five times and the Location destructor was called five times. The Rectangle class's constructor and destructor were each called once. This demonstrates that all objects have been reclaimed; there are no memory leaks.

  • The destructors are invoked implicitly; there are no explicit statements that invoke them. When the end of the main program is reached, the Rectangle and Location objects declared in the main program go out of scope and are automatically destructed.

  • The destructors for the four Location subobjects have been executed. This demonstrates that the destruction of the subobjects is performed when their containing object (in this case the Rectangle object) is destructed.

  • The subobjects are constructed before their containing object is constructed. In the output it can be seen that the output from the Location subobjects' constructors comes before the output from the Rectangle's constructor. This sequence is deliberate; it allows the constructor for the containing object to invoke the methods of its subobjects (e.g., to query their state or connect them in an association). To allow the constructor of the containing object to operate on its subobjects, it is necessary that the subobjects be fully constructed in advance of the execution of the containing object's constructor.

  • the subobjects are destructed after their containing object is destructed. In the output it can be seen that the output from the Location subobjects' destructor comes after the output from the Rectangle's destructor. This sequence is deliberate; it allows the destructor of the containing object to invoke methods of its subobjects (e.g., to adjust the state of the subobjects prior to their destruction. In the StopWatch example it will be seen that the Clock subobject must be stoped prior to the subobject of the StopWatch being destructed. This avoids the possibility of the Clock subobject ticking during the destruction process.) To allow the destructor of the containing object to operate on its subobjects, it is necessary that the subobjects be destructed only after the execution of the containing object's destructor has completed.


This example illustrates the main ideas about the sequence of subobject construction and sub-object destruction. The exercises below help to discover a few other details.

Tasks

  1. Demonstrate your understanding of a static aggregation by defining, implementing, and testing a Circle class that has a Location subobject defining the center of the circle and an integer value defining the radius of the Circle. Your Circle object should be able to draw itself on a Canvas.

  2. Demonstrate your understanding of a static aggregation by defining, implementing, and testing a Triangle class that has three Location subobjects defining the vertices of the triangle. Your Triangle object should be able to draw itself on a Canvas.

  3. In the output from the test program it is not possible to tell if the Location object in the main program is destructed before or after the Location subobjects. Rewrite the test program so that the order can be determined.

  4. Rewrite the constructor for the trivialized Rectangle class so that the Location object is passed by copy and not by reference. Rerun the test program with this change. Explain any differences that you see between the output of the test program using the original Rectangle class and the test program using the modified Rectangle class.

  5. Modify the main test program to include a global Rectangle object (i.e., one declared outside of the main program). Rerun the test program with this change. Explain any differences that you see between the output of the original test program and the the output of the test program with the global variable.

  6. Modify the trivialized Location class to remove the default constructor (the one with no arguments). Recompile the test program using this modified Location class. Explain any error messages that result.

  7. Modify the trivialized Rectangle class so that its private data includes a pointer to a Location object. In the constructor of the Rectangle class initialize this pointer to a Location object that has been dynamically allocated using the new operator. Rerun the test program using this modified trivialized Rectangle class. Examine the output of the test program and determine how many Location objects were constructed and how many Location objects were destructed. Explain your observations.

 




©1998 Prentice-Hall, Inc.
A Simon & Schuster Company
Upper Saddle River, New Jersey 07458

Legal Statement

 

 

 

ÿ