ITEEDU

7.3 复合:把对象作为类成员

AlarmClock类的对象需要知道何时响钤,因此可以将一个Time对象作为类成员这种功能称为复合(composition)。类可以将其他类对象作为自己的成员。

软件工程视点7.7

复合是软件复用的一种形式,就是一个类将其他类对象作为自己的成员。生成对象时,自动调用其构造函数,因此要指定参数如何传递给成员对象的构造函数。成员对象按声明的顺序(而不是在构造函数的成虽初始化值列表中列出的顺序)并在建立所包含的类对象(也称为宿主对象,host object)之前建立。

图7.4用Employee类和Date类演示一个类作为其他类对象的成员。Employee类包含private数据成员firstName、lastName、birthDate和hireDate。成员birthDate和hireDate是Date类的const类型的对象,该Data类包含private数据成员month、day和year。程序实例化一个Employee对象,并初始化和显示其数据成员。注意Employee构造函数定义中函数首部的语法:

Employee::Employee( char *fname, char *lname,

int bmonth, int bday, int byear,

int hmonth, int hday, int hyear )

:birthDate (bmonth, bday, byear),

hireDate (hmonth, hday, hyear )

该构造函数有八个参数(fname、lname、bmonth、bday、byear、hmonth、hday和hyear)。首部中的冒号(:)将成员初始化值与参数表分开。成员初始化值指定Employee的参数传递给成员对象的构造函数。参数bmonth、bday和byear传递给birthDate构造函数。参数hmonth、hday和hyear传递给hireDate构造函数。多个成员的初始化值用逗号分开。

  // Fig. 7.4: datel.h
 // Declaration of the Date class.
 // Member functions defined in datel.cpp
 #ifndef DATE1_H
 #define DATE1_H
 class Date {
 public:
    Date( int = 1, int = 1, int = 1900 ); // default constructor
   void print() const;  // print date in month/day/year format
   ~Date();  // provided to confirm destruction order
 private:
   int month;  // 1-12
   int day;
   int year;    // any year
   // utility function to test proper day for month and year
   int checkDay( int );
 };
 #endif
 // Fig. 7.4: date.cpp
 // Member function definitions for Date class.
 #include  <iostream.h>
 #include "date1.h"
 // Constructor: Confirm proper value for month;
 // call utility function checkDay to confirm proper
 // value for day.
 Date::Date( int mn, int dy, int yr )
 {
   if ( mn > 0 && mn <= 12 )      // validate the month
     month = mn;
   else {
     month = 1;
     cout << "Month "<< mn <<" invalid. Set to month 1.\n";
   }
   year = yr;                  // should validate yr
   day = checkDay( dy );         // validate the day
   cout << "Date object constructor for date ";
   print();       // interesting: a print with no arguments
   cout << endl;
 }
 // Print Date object in form month/day/year
 void Date::print() const
   { cout << month << '/' << day << '/' << year; }
 // Destructor: provided to confirm destruction order
 Date::~Date()
 {
   cout << "Date object destructor for date ";
   print();
   cout << endl;
 }
 // Utility function to confirm proper day value
 // based on month and year.
 // Is the year 2000 a leap year?
 int Date::checkDay( int testDay )
 {
   static const int daysPerMonth[ 13 ] =
     {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
   if ( testDay > 0 && testDay <= daysPerMonth[ month ] )
     return testDay;
 
     if ( month == 2 &&
        testDay == 29 &&
        ( year % 400 == 0 ||                  // year 2000?
        ( year % 4 == 0 && year % 100 != 0 ) ) ) // year 2000?
        return testDay;
     cout << "Day" << testDay << "invalid. Set to day 1.\n";
    return 1;  // leave object in consistent state if bad value
 }
 // Fig.7.4:emply1.h
 // Declaration of the Employee class.
 // Member functions defined in emplyl.cpp
 #ifndef EMPLY1_H
 #define EMPLY1_H
 #include "date1.h"
 class Employee {
 public:
   Employee( char *, char *, int, int, int, int, int, int );
   void print() const;
   ~Employee();  // provided to confirm destruction order
 private:
   char firstName[ 25 ];
   char lastName[ 25 ];
   const Date birthDate;
   const Date hireDate;
 };
 #endif
 // Fig. 7.4: emplyl.cpp
 #include "emplyl.h"
 Employee::Employee( char *fname, char *lname,
                 int bmonth, int bday, int byear,
                 int hmonth, int hday, int hyear )
   : birthDate( bmonth, bday, byear ),
         hireDate( hmonth, hday, hyear )
 {
      // copy fname into firstName and be sure that it fits
      int length = strlen( fname );
      length = ( length < 25 ? length : 24 );
      strncpy( firstName, fname, length );
      firstName[ length ] = '\0';
      // copy lname into lastName and be sure that it fits
      length = strlen( lname );
      length = ( length < 25 ? length : 24 );
      strncpy( lastName, lname, length );
      lastName[ length ] = '\0';
   cout << "Employee object constructor:"
        << firstName << ' ' << lastName << endl;
 }
 void Employee::print() const
 {
   cout << lastName << ", "<< firstName << "\nHired: ";
   hireDate.print();
   cout <<" Birth date: ";
   birthDate.print();
   cout << endl;
 }
 // Destructor: provided to confirm destruction order
 Employee::~Employee()
 {
   cout << "Employee object destructor:"
        << lastName << ", "<< firstName << endl;
 }
 // Fig. 7.4: figO7_O4.cpp
 #include < iostream.h>
 #include "emply1.h"
 int main()
 {
   Employee e( "Bob", "Jones", 7, 24, 1949, 3, 12, 1988 );
   cout << '\n';
   e.print();
   cout << "\nTest Date constructor with invalid values:\n";
   Date d( 14, 35, 1994 );  // invalid Date values
   cout << endl;
   return O;
 }

输出结果:

Date object constructor for date 7/24/1949

Date object Constructor for date 3/12/1988

Employee object constructor Bob Jones

Jones, Bob

Hired: 3/12/1988 Birth date 7/24/1949

Test Date constructor with invalid values:

Month 14 invalid. Set to month 1.

Day 35 invalid. Set to day 1.

Date object constructor for date 1/1/1994

Date object destructor for date 1/1/1994

Employee object destructor: Jones, Bob

Date object destructor for date 3/12/1988

Date object destructor for date 7/24/1949

图7.4使用成员对象的初始化值

记住,const成员和引用也在成员初始化值列表中初始化(第9章会讲到,派生类的基类部分也是这样初始化的)。Date类和Employee类各有一个析构函数,分别在删除Date和EmpLoyee对象时打印一个消息。这样就可以从程序输出中确认对象由内向外建立,由外向内删除(即先删除Employee对象,再删除其中包含的Date对象)。

成员对象不需要通过成员初始化值显式初始化。如果不提供成员初始化值,则隐式调用成员对象的默认构造函数。默认构造函数建立的值(若有)可以用set函数重定义。

常见编程错误7.6

没有为成员对象提供初始化值的情况下,也没有为成员对象提供默认的构造函数,这样就会产生语法错误。

性能提示7.2

通过成员初始化值显式初始化成员对象,这样可以消除两次初始化成员对象的开销,一次是在调用成员对象的默认构造函数时,一次是在用set函数初始化成员对象时。

软件工程视点7. 8

如果一个类用其他类对象作为成员,则将这个成员对象定为public不分破坏该成员对象private成员的封装与隐藏。

注意第43行调用Date成员函数print。C++中许多类的成员函数不需要参数。这是因为每个成员函数包含所操作对象的隐式句柄(指针形式)。7.5节将介绍隐式指针this。

在第一版的Employee类中(为了便于编程),我们用两个25字符数组表示EmpLoyee的姓和名。这些数组如果存储短名称可能浪费内存空间(记往每个数组中有一个字符是字符串的null终止符 '\0',),超过24个字符的姓名要截尾之后才能放得下。本章稍后将介绍另一种形式的Employee类,动态生成适合姓和名的数组长度,还可以用两个string对象表示姓名。第19章详细介绍string标准库类。