We limited ourselves to non-pointer member variables in our previous discussion of classes and objects. Adding pointer members increases the power and flexibility of object-oriented programs and allows programmers to implement more UML class relationships. Nevertheless, allowing pointer members also increases the complexity and effort of programming with classes and objects.
The textbook uses two non-standard, working definitions to make it easier for us to talk about pointer and non-pointer class members:
A simple class or object is:
A class or object without any pointer member variables.
A complex class or object is:
A class or object with one or more pointer member variables.
class Person
{
private:
string* name;
int weight;
double height;
};
(a)
(b)
(c)
A class with a pointer member variable.
A UML class diagram illustrating the member variables in the Person class. UML diagrams are language agnostic; nevertheless, we include the asterisk to emphasize that the name member is a pointer.
The C++ specification of class Person. The member variable namepoints to an instance of the string class.
Making a member variable a pointer keeps the data separate from the object. In this example, the person's name is in a string object existing outside of the Person.
UML Classes
Aggregation Classes
Abstract Representation
class Engine
{
. . .
};
class Transmission
{
. . .
};
class Car
{
private:
Engine * cars_engine;
Transmission * cars_transmission;
. . .
};
Building a whole-part object with aggregation. In this example, a Car (the whole) consists of an Engine and a Transmission (the parts). We implement the whole-part relationship with two pointer member variables (highlighted with blue and red) in the Car class. When a program instantiates the Car class, it only creates pointers; the Engine and Transmission objects are created elsewhere in the program and exist outside the Car. As aggregation is a whole-part relationship, we can say that "a Car has an Engine" or that "a Transmission is part of a Car."
Although the Engine and Transmission details are not significant for this example, their class specifications must precede the Car specification. If we specify the classes in a single file, we must do so as illustrated here. Specifying the classes in separate header files requires the program to #include the part header files before specifying the whole. The next sections describe how to initialize and use part objects bound to the whole with pointers.
Syntactically, adding a pointer member variable to a class is seemingly trivial - adding an asterisk to one or more member variables. But conceptually, the change has significant consequences. When we change a member variable from a non-pointer to a pointer, we also change the relationship from composition to aggregation. Changing the relationship means changing a strong, tight binding to a loose, weak one. Aggregated objects may have different lifetimes, the whole can share its parts with other program objects, and the whole can discard or replace its parts whenever convenient. The increase in complexity requires us to explore five distinct concepts related to pointer member variables:
Initializing pointer members with constructors and setters
Destroying (i.e., deleting) objects with destructors
Using pointer members
Maintaining aggregation security by testing for a nullptr
Overriding the copy constructor
We explore these concepts in the following sections.
Aggregation Summary: Filling In The Table
Aggregation is a constructive relationship conveniently characterized by the phrase "has a." It has many property values in common with composition, but it differs in the strength or tightness of the binding between the objects. The following figure summarizes aggregation's property values. Use the summary to check and complete your entries in one of the blank Class Relationship Tables located at the end of the chapter.
Class Roles. The relationship forms a class hierarchy. The first class plays the role of the Whole class while the second is the Part.
Semantics. We can read aggregation in either direction depending on the phrase we use. We read it from the Whole to the Part as a has a relationship and from the Part to the Whole as a part of.
a Car has an Engine
an Engine is part of a Car
a Car has a Transmission
a Transmission is a part of a Car
Directionality. Aggregation is a unidirectional or a one-way relationship. Unidirectionality means that
(a)
(b)
(c)
(d)
The operations may only take place in one direction: from the whole to the part
The whole object can send a message to the part object, which can respond to the message but cannot initiate message sending
The whole "knows" about the part, but the part doesn't "know" about the whole
It is possible to navigate from the whole to the part object but not from the part to the whole
Binding Strength. The binding between the two objects is weak or loose because the part object exists outside the whole object and is only connected to it by a pointer (Figure 1). The strength of the binding implies the final two characteristics:
Lifetime. The two objects have an independent lifetime, which means that the two objects are created and destroyed at different times. It also means that the relationship between the two objects is changeable - the program can create or break it whenever it is convenient.
Sharing. The binding between the two objects is weak or loose enough that the whole can share the part with other objects in the program.
C++ implementation.
class Car
{
private:
Engine* my_engine;
Transmission* my_transmission;
};
Aggregation property values. Like all constructive relationships, C++ implements aggregation with class-scope (i.e., member) variables, but the variables are pointers.