|
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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
|