10.3. Composition: Objects as Members of
Classes
An AlarmClock object needs to know when it is supposed to sound its
alarm, so why not include a Time object as a
member of the AlarmClock class? Such a
capability is called composition and is sometimes referred to as a has-a relationship—a class can have objects of
other classes as members.
Software Engineering Observation 10.5
|
A common form
of software reusability is composition, in which a class has objects of other
classes as members. |
When an object is created,
its constructor is called automatically. Previously, we saw how to pass
arguments to the constructor of an object we created in main. This section shows how an object's constructor can pass
arguments to member-object constructors, which is accomplished via member
initializers.
Software Engineering Observation 10.6
|
Member
objects are constructed in the order in which they are declared in the class
definition (not in the order they are listed in the constructor's member
initializer list) and before their enclosing class objects (sometimes called
host objects) are
constructed. |
The program of Figs. 10.10–10.14 uses class Date (Figs. 10.10–10.11) and class
Employee (Figs.
10.12–10.13) to demonstrate objects as members of other objects. The
definition of
class Employee (Fig. 10.12) contains private
data members firstName, lastName, birthDate and
hireDate. Members birthDate and hireDate are
const objects of class Date, which contains private
data members month, day and year. The
Employee constructor's header (Fig. 10.13, lines 18–21) specifies that the constructor has
four parameters (first, last, dateOfBirth and dateOfHire). The first two parameters are used in the
constructor's body to initialize the character arrays firstName and
lastName. The last two parameters are
passed via member initializers to the constructor for class Date. The
colon (:) in the header separates the member
initializers from the parameter list. The member initializers specify the
Employee constructor parameters being passed to
the constructors of the member Date objects. Parameter
dateOfBirth is passed to object birthDate's constructor (Fig. 10.13, line 20),
and parameter dateOfHire is passed to object hireDate's
constructor (Fig.
10.13, line 21). Again, member initializers are
separated by commas. As you study class Date (Fig. 10.10),
notice that the class does not provide a constructor that receives a parameter
of type Date. So, how is the member initializer
list in class Employee's constructor able
to initialize the birthDate and hireDate objects by passing
Date object's to their Date constructors? As we
mentioned in Chapter
9, the compiler provides each class with a
default copy constructor that copies each data member of the constructor's
argument object into the corresponding member of the object being initialized.
Chapter
11 discusses how you can define customized copy
constructors.
Fig. 10.10. Date class
definition.
1 // Fig. 10.10: Date.h
2 // Date class definition; Member functions defined in Date.cpp
3 #ifndef DATE_H
4 #define DATE_H
5
6 class Date
7 {
8 public:
9 Date( int = 1, int = 1, int = 1900 ); // default constructor
10 void print() const; // print date in month/day/year format
11 ~Date(); // provided to confirm destruction order
12 private:
13 int month; // 1-12 (January-December)
14 int day; // 1-31 based on month
15 int year; // any year
16
17 // utility function to check if day is proper for month and year
18 int checkDay( int ) const;
19 }; // end class Date
20
21 #endif
|
Fig. 10.11. Date class member-function
definitions.
1 // Fig. 10.11: Date.cpp
2 // Date class member-function definitions.
3 #include <iostream>
4 using std::cout;
5 using std::endl;
6
7 #include "Date.h" // include Date class definition
8
9 // constructor confirms proper value for month; calls
10 // utility function checkDay to confirm proper value for day
11 Date::Date( int mn, int dy, int yr )
12 {
13 if ( mn > 0 && mn <= 12 ) // validate the month
14 month = mn;
15 else
16 {
17 month = 1; // invalid month set to 1
18 cout << "Invalid month (" << mn << ") set to 1.\n";
19 } // end else
20
21 year = yr; // could validate yr
22 day = checkDay( dy ); // validate the day
23
24 // output Date object to show when its constructor is called
25 cout << "Date object constructor for date ";
26 print();
27 cout << endl;
28 } // end Date constructor
29
30 // print Date object in form month/day/year
31 void Date::print() const
32 {
33 cout << month << '/' << day << '/' << year;
34 } // end function print
35
36 // output Date object to show when its destructor is called
37 Date::~Date()
38 {
39 cout << "Date object destructor for date ";
40 print();
41 cout << endl;
42 } // end ~Date destructor
43
44 // utility function to confirm proper day value based on
45 // month and year; handles leap years, too
46 int Date::checkDay( int testDay ) const
47 {
48 static const int daysPerMonth[ 13 ] =
49 { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
50
51 // determine whether testDay is valid for specified month
52 if ( testDay > 0 && testDay <= daysPerMonth[ month ] )
53 return testDay;
54
55 // February 29 check for leap year
56 if ( month == 2 && testDay == 29 && ( year % 400 == 0 ||
57 ( year % 4 == 0 && year % 100 != 0 ) ) )
58 return testDay;
59
60 cout << "Invalid day (" << testDay << ") set to 1.\n";
61 return 1; // leave object in consistent state if bad value
62 } // end function checkDay
|
Fig. 10.12. Employee class definition showing
composition.
1 // Fig. 10.12: Employee.h
2 // Employee class definition showing composition.
3 // Member functions defined in Employee.cpp.
4 #ifndef EMPLOYEE_H
5 #define EMPLOYEE_H
6
7 #include "Date.h" // include Date class definition
8
9 class Employee
10 {
11 public:
12 Employee( const char * const, const char * const,
13 const Date &, const Date & );
14 void print() const;
15 ~Employee(); // provided to confirm destruction order
16 private:
17 char firstName[ 25 ];
18 char lastName[ 25 ];
19 const Date birthDate; // composition: member object
20 const Date hireDate; // composition: member object
21 }; // end class Employee
22
23 #endif
|
Fig. 10.13. Employee class member-function
definitions, including constructor with a member initializer
list.
1 // Fig. 10.13: Employee.cpp
2 // Employee class member-function definitions.
3 #include <iostream>
4 using std::cout;
5 using std::endl;
6
7 #include <cstring> // strlen and strncpy prototypes
8 using std::strlen;
9 using std::strncpy;
10
11 #include "Employee.h" // Employee class definition
12 #include "Date.h" // Date class definition
13
14 // constructor uses member initializer list to pass initializer
15 // values to constructors of member objects birthDate and hireDate
16 // [Note: This invokes the so-called "default copy constructor" which the
17 // C++ compiler provides implicitly.]
18 Employee::Employee( const char * const first, const char * const last,
19 const Date &dateOfBirth, const Date &dateOfHire )
20 : birthDate( dateOfBirth ), // initialize birthDate
21 hireDate( dateOfHire ) // initialize hireDate
22 {
23 // copy first into firstName and be sure that it fits
24 int length = strlen( first );
25 length = ( length < 25 ? length : 24 );
26 strncpy( firstName, first, length );
27 firstName[ length ] = '\0';
28
29 // copy last into lastName and be sure that it fits
30 length = strlen( last );
31 length = ( length < 25 ? length : 24 );
32 strncpy( lastName, last, length );
33 lastName[ length ] = '\0';
34
35 // output Employee object to show when constructor is called
36 cout << "Employee object constructor: "
37 << firstName << ' ' << lastName << endl;
38 } // end Employee constructor
39
40 // print Employee object
41 void Employee::print() const
42 {
43 cout << lastName << ", " << firstName << " Hired: ";
44 hireDate.print();
45 cout << " Birthday: ";
46 birthDate.print();
47 cout << endl;
48 } // end function print
49
50 // output Employee object to show when its destructor is called
51 Employee::~Employee()
52 {
53 cout << "Employee object destructor: "
54 << lastName << ", " << firstName << endl;
55 } // end ~Employee destructor
|
Figure
10.14 creates two Date objects (lines 11–12) and
passes them as arguments to the constructor of the Employee object
created in line 13. Line 16 outputs the Employee object's data. When
each Date object is created in lines 11–12, the Date
constructor defined in lines 11–28 of Fig. 10.11
displays a line of output to show that the constructor was called (see the first
two lines of the sample output). [Note: Line 13
of Fig. 10.14 causes
two additional Date constructor calls that do
not appear in the program's output. When each of the Employee's
Date member object's is initialized in the Employee
constructor's member initializer
list (Fig. 10.13,
lines 21–21), the default copy constructor for class Date is called. This constructor is defined implicitly by the
compiler and does not contain any output statements to demonstrate when it is
called. We discuss copy constructors and default copy constructors in detail in
Chapter
11.]
Fig. 10.14.
Demonstrating composition–an object with member objects.
1 // Fig. 10.14: fig10_14.cpp
2 // Demonstrating composition--an object with member objects.
3 #include <iostream>
4 using std::cout;
5 using std::endl;
6
7 #include "Employee.h" // Employee class definition
8
9 int main()
10 {
11 Date birth( 7, 24, 1949 );
12 Date hire( 3, 12, 1988 );
13 Employee manager( "Bob", "Blue", birth, hire );
14
15 cout << endl;
16 manager.print();
17
18 cout << "\nTest Date constructor with invalid values:\n";
19 Date lastDayOff( 14, 35, 1994 ); // invalid month and day
20 cout << endl;
21 return 0;
22 } // end main
|
|
|
Class Date and class Employee each include a destructor (lines 37–42 of Fig. 10.11 and lines
51–55 of Fig.
10.13, respectively) that prints a message when
an object of its class is destructed. This enables us to confirm in the program
output that objects are constructed from the inside out and destroyed in the
reverse order, from the outside in (i.e., the Date member objects are destroyed after the
Employee object that contains them). Notice
the last four lines in the output of Fig. 10.14. The last two lines are
the outputs of the Date destructor running on Date objects
hire (line 12) and birth (line 11), respectively. These
outputs confirm that the three objects created in main are destructed
in the reverse
of the order in which they were constructed. (The Employee destructor output is five lines from the bottom.) The
fourth and third lines from the bottom of the output window show the destructors
running for the Employee's member objects hireDate (Fig. 10.12, line 20)
and birthDate (Fig. 10.12, line 19). These outputs
confirm that the Employee object is destructed from the outside
in—i.e., the Employee destructor runs first
(output shown five lines from the bottom of the output window), then the member
objects are destructed in the reverse order from which they were constructed.
Again, the outputs in Fig. 10.14 did not show the constructors running for these
member objects, because these were the default copy constructors provided by the
C++ compiler.
A member object does not need to be initialized explicitly
through a member initializer. If a member initializer is not provided, the
member object's default constructor will be called implicitly. Values, if any,
established by the default constructor can be overridden by set functions. However, for
complex initialization, this approach may require significant additional work
and time.
Common Programming Error 10.6
|
A compilation error occurs if a member object is not
initialized with a member initializer and the member object's class does not
provide a default constructor (i.e., the member object's class defines one or
more constructors, but none is a default
constructor). |
Performance Tip 10.2
|
Initialize member objects explicitly through
member initializers. This eliminates the overhead of "doubly initializing"
member objects—once when the member object's default constructor is called and
again when set functions
are called in the constructor body (or later) to initialize the member
object. |
Software Engineering Observation 10.7
|
If a class
member is an object of another class, making that member object public
does not violate the encapsulation and hiding of that member object's
private members. However, it does violate the
encapsulation and hiding of the containing class's implementation, so member
objects of class types should still be private, like all other data
members. |
In line 26 of Fig. 10.11, notice the call to
Date member function print. Many
member functions of classes in C++ require no arguments. This is because each
member function contains an implicit handle (in the form of a pointer) to the
object on which it operates. We discuss the implicit pointer, which is
represented by keyword this, in Section
10.5.
Class Employee uses two 25-character arrays (Fig. 10.12, lines 17–18) to represent the first name and last name
of the Employee. These arrays may waste
space for names shorter than 24 characters. (Remember, one character in each
array is for the terminating null character, '\0',
of the string.) Also, names longer than 24 characters must be truncated to fit
in these fixed-size character arrays. Section
10.7 presents another version of class
Employee that dynamically creates the exact
amount of space required to hold the first and the last name.
Note that the simplest way to
represent an Employee's first and last name
using the exact amount of space required is to use two string objects
(C++ Standard Library class string was introduced in Chapter
3). If we did this, the Employee constructor would appear as
follows
Employee::Employee( const string &first, const string &last,
const Date &dateOfBirth, const Date &dateOfHire )
: firstName( first), // initialize firstName
lastName( last ), // initialize lastName
birthDate( dateOfBirth ), // initialize birthDate
hireDate( dateOfHire ) // initialize hireDate
{
// output Employee object to show when constructor is called
cout << "Employee object constructor: "
<< firstName << ' ' << lastName << endl;
} // end Employee constructor
Notice that data members firstName and
lastName (now string objects) are initialized through member
initializers. The Employee classes presented in Chapters
12–13
use string objects in this fashion. In
this chapter, we use pointer-based strings to give you additional exposure to
pointer manipulation.