9.12 Composition: Objects as Members of Classes

An AlarmClock object needs to know when it’s supposed to sound its alarm, so why not include a Time object as a member of the AlarmClock class? Such a software-reuse capability is called composition (or aggregation) and is sometimes referred to as a has-a relationshipa class can have objects of other classes as members.5

You’ve actually been using composition since Chapter 3. In that chapter’s examples, class Account contained a string object as a data member.

Previously, we saw how to pass arguments to the constructor of an object we created in main. Now we show how a class’s constructor can pass arguments to member-object constructors via member initializers.

Software Engineering Observation 9.8

Data members are constructed in the order in which they’re declared in the class definition (not in the order they’re listed in the constructor’s member-initializer list) and before their enclosing class objects are constructed.

The next program uses classes Date (Figs. 9.189.19) and Employee (Figs. 9.209.21) to demonstrate composition. Class Employee’s definition (Fig. 9.20) 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 prototype (Fig. 9.20, lines 14–15) specifies that the constructor has four parameters (first, last, dateOfBirth and dateOfHire). The first two parameters are passed via member initializers to the string class constructor for the firstName and lastName data members. The last two are passed via member initializers to the Date class constructor for the birthDate and hireDate data members.

Fig. 9.18 Date class definition.

Alternate View

 1   // Fig. 9.18: Date.h
 2   // Date class definition; Member functions defined in Date.cpp
 3   #include <string>
 4
 5   #ifndef DATE_H
 6   #define DATE_H
 7
 8   class Date {
 9   public:
10      static const unsigned int monthsPerYear{12}; // months in a year
11      explicit Date(unsigned int = 1, unsigned int = 1, unsigned int = 1900);
12      std::string toString() const; // date string in month/day/year format
13      ~Date(); // provided to confirm destruction order
14   private:
15      unsigned int month; // 1-12 (January-December)
16      unsigned int day; // 1-31 based on month
17      unsigned int year; // any year
18
19      // utility function to check if day is proper for month and year
20      unsigned int checkDay(int) const;
21   };
22
23   #endif

Fig. 9.19 Date class member-function definitions.

Alternate View

 1   // Fig. 9.19: Date.cpp
 2   // Date class member-function definitions.
 3   #include <array>
 4   #include <iostream>
 5   #include <sstream>
 6   #include <stdexcept>
 7   #include "Date.h" // include Date class definition
 8   using namespace std;
 9
10   // constructor confirms proper value for month; calls
11   // utility function checkDay to confirm proper value for day
12   Date::Date(unsigned int mn, unsigned int dy, unsigned int yr)
13      : month{mn}, day{checkDay(dy)}, year{yr} {
14      if (mn < 1 || mn > monthsPerYear) { // validate the month
15         throw invalid_argument("month must be 1-12");
16      }
17
18      // output Date object to show when its constructor is called
19      cout << "Date object constructor for date " << toString() << endl;
20   }
21
22   // print Date object in form month/day/year
23   string Date::toString() const {
24      ostringstream output;
25      output << month << '/' << day << '/' << year;
26      return output.str();
27   }
28
29   // output Date object to show when its destructor is called
30   Date::~Date() {
31      cout << "Date object destructor for date " << toString() << endl;
32   }
33
34   // utility function to confirm proper day value based on
35   // month and year; handles leap years, too
36   unsigned int Date::checkDay(int testDay) const {
37      static const array<int, monthsPerYear + 1> daysPerMonth{
38         0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
39
40      // determine whether testDay is valid for specified month
41      if (testDay > 0 && testDay <= daysPerMonth[month]) {
42         return testDay;
43      }
44
45      // February 29 check for leap year
46      if (month == 2 && testDay == 29 && (year % 400 == 0 ||
47         (year % 4 == 0 && year % 100 != 0))) {
48         return testDay;
49      }
50
51      throw invalid_argument("Invalid day for current month and year");
52    }

Fig. 9.20 Employee class definition showing composition.

Alternate View

 1   // Fig. 9.20: Employee.h
 2   // Employee class definition showing composition.
 3   // Member functions defined in Employee.cpp.
 4   #include <string>
 5
 6   #ifndef EMPLOYEE_H
 7   #define EMPLOYEE_H
 8
 9   #include <string>
10   #include "Date.h" // include Date class definition
11
12   class Employee {
13   public:
14      Employee(const std::string&, const std::string&,
15         const Date&, const Date&);
16      std::string toString() const;
17      ~Employee(); // provided to confirm destruction order
18   private:
19      std::string firstName; // composition: member object
20      std::string lastName; // composition: member object
21      const Date birthDate; // composition: member object
22      const Date hireDate; // composition: member object
23   };
24
25   #endif

Fig. 9.21 Employee class member-function definitions.

Alternate View

 1   // Fig. 9.21: Employee.cpp
 2   // Employee class member-function definitions.
 3   #include <iostream>
 4   #include <sstream>
 5   #include "Employee.h" // Employee class definition
 6   #include "Date.h" // Date class definition
 7   using namespace std;
 8
 9   // constructor uses member initializer list to pass initializer
10   // values to constructors of member objects
11   Employee::Employee(const string& first, const string& last,
12      const Date &dateOfBirth, const Date &dateOfHire)
13      : firstName{first}, // initialize firstName         
14        lastName{last}, // initialize lastName            
15        birthDate{dateOfBirth}, // initialize birthDate   
16        hireDate{dateOfHire} { // initialize hireDate     
17      // output Employee object to show when constructor is called
18      cout << "Employee object constructor: "
19         << firstName << ' ' << lastName << endl;
20   }
21
22   // print Employee object
23   string Employee::toString() const {
24      ostringstream output;
25      output << lastName << ", " << firstName << " Hired: "
26         << hireDate.toString() << " Birthday: " << birthDate.toString();
27      return output.str();
28   }
29
30   // output Employee object to show when its destructor is called
31   Employee::~Employee() {
32      cout << "Employee object destructor: "
33         << lastName << ", " << firstName << endl;
34   }

Employee Constructor’s Member-Initializer List

The colon (:) following the constructor’s header (Fig. 9.21, line 13) begins the member-initializer list. The member initializers specify the Employee constructor parameters being passed to the constructors of the string and Date data members. Parameters first, last, dateOfBirth and dateOfHire are passed to the constructors for objects firstName, lastName, birthDate and hireDate, respectively. The order of the member initializers does not matter. They’re executed in the order that the member objects are declared in class Employee.

Good Programming Practice 9.2

For clarity, list the member initializers in the order that the class’s data members are declared.

Date Class’s Default Copy Constructor

As you study class Date (Fig. 9.18), notice that the class does not provide a constructor that receives a parameter of type Date. So, why can the Employee constructor’s member-initializer list initialize the birthDate and hireDate objects by passing Date objects to their Date constructors? As we mentioned in Section 9.10, 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 10 discusses how you can define customized copy constructors.

Testing Classes Date and Employee

Figure 9.22 creates two Date objects (lines 9–10) and passes them as arguments to the constructor of the Employee object created in line 11. There are actually five constructor calls when an Employee is constructed:

  • two calls to the string class’s constructor (lines 13–14 of Fig. 9.21),

  • two calls to the Date class’s default copy constructor (lines 15–16 of Fig. 9.21),

  • and the call to the Employee class’s constructor.

Line 13 outputs the Employee object’s data. When each Date object is created in lines 9–10, the Date constructor defined in lines 12–20 of Fig. 9.19 displays a line of output to show that the constructor was called (see the first two lines of the sample output). Note that line 11 of Fig. 9.22 causes two Date constructor calls that do not appear in this program’s output. When each of the Employee’s Date member objects is initialized in the Employee constructor’s member-initializer list (Fig. 9.21, lines 15–16), the default copy constructor for class Date is called. Since this constructor is defined implicitly by the compiler, it does not contain any output statements to demonstrate when it’s called.]

Fig. 9.22 Demonstrating composition—an object with member objects.

Alternate View

 1   // Fig. 9.22: fig09_22.cpp
 2   // Demonstrating composition--an object with member objects.
 3   #include <iostream>
 4   #include "Date.h" // Date class definition
 5   #include "Employee.h" // Employee class definition
 6   using namespace std;
 7
 8   int main() {
 9      Date birth{7, 24, 1949};
10      Date hire{3, 12, 1988};
11      Employee manager{"Bob", "Blue", birth, hire};
12
13      cout << "
" << manager.toString() << endl;
14   }

Date object constructor for date 7/24/1949
Date object constructor for date 3/12/1988
Employee object constructor: Bob Blue

Blue, Bob Hired: 3/12/1988   Birthday: 7/24/1949
Employee object destructor: Blue, Bob
Date object destructor for date 3/12/1988
Date object destructor for date 7/24/1949
Date object destructor for date 3/12/1988
Date object destructor for date 7/24/1949

Class Date and class Employee each include a destructor (lines 30–32 of Fig. 9.19 and lines 31–34 of Fig. 9.21, 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 destructed in the reverse order, from the outside in (i.e., the Date member objects are destructed after the Employee object that contains them).

Notice the last four lines in the output of Fig. 9.22. The last two lines are the outputs of the Date destructor running on Date objects hire (Fig. 9.22, line 10) and birth (line 9), respectively. The 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. 9.20, line 22) and birthDate (line 21). The last two lines of the output correspond to the Date objects created in lines 10 and 9 of Fig. 9.22.

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. Class string’s destructor does not contain output statements, so we do not see the firstName and lastName objects being destructed. Again, Fig. 9.22’s output did not show the constructors running for member objects birthDate and hireDate, because these objects were initialized with the default Date class copy constructors provided by the compiler.

What Happens When You Do Not Use the Member-Initializer List?

If a member object is not initialized through a member initializer, 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.

Performance Tip 9.4

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.

 

Common Programming Error 9.5

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

 

Software Engineering Observation 9.9

If a data 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. But, it does violate the encapsulation and hiding of the enclosing class’s implementation, so member objects of class types should still be private.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset