ITEEDU

6.5 用类实现Time抽象数据类型

类使程序员可以构造对象的属性(attribute,表示为数据成员)和行为(behavior)或操作(operation,表示为成员函数)。C++中用关键字class定义包含数据成员和成员函数的类型。

成员函数在有些面向对象编程语言中也称为方法(method),响应对象接收的消息(message)。消息对应于一个对象发给另一个对象或由函数发给对象的成员函数调用。

一旦定义了一个类,可以用类名声明该类的对象。图6.2显示了Time类的简单定义。

Time类定义以关键字class开始。类定义体放在左右花括号(C1)之间,类定义用分号终止。Time类定义和Time类结构定义各包含三个整型成员hour、minute和second。

class Time {
 public:
    Time();
    void setTime( int, int, int);
    void printMilitary();
    void printStandard();
 private:
    int hour;      // 0-23
    int minute;    // 0-59
   int second;    // 0-59
 };

图6. 2 Time类的简单定义

常见编程错误6.2

忘记类或结构定义结束时的分号是个语法错误。

类定义的其他部分是新内容。public:和private:标号称为成员访问说明符(member accessspecifier)。在程序能访问Time类对象的任何地方都可以访问任何在成员访问说明符public后面(和下一个成员访问说明符之前)声明的数据成员和成员函数。成员访问说明符private后面(和下一个成员访问说明符之前)声明的数据成员和成员函数只能由该类的成员函数访问。成员访问说明符总是加上冒号,可以在类定义中按任何顺序多次出现。本文余下部分使用不带冒号的成员访问说明符public和private。第9章还将介绍另一个成员访问说明符protected,并介绍继承及其在面向对象编程中的作用。

编程技巧6.1

每个成员访问说明符只在类定义中使用一次,这样可以增加清晰性与可读性。将public成员放在前面,便于寻找。

类定义中的访问说明符public后面是成员函数Time、setTime、printMihtary和printStandard的函数原型。这些函数是类的public成员函数(或public服务、public行为、类的接口)。类的客户(client,即程序中的用户部分)使用这些函数操作该类的数据。

注意与类名相同的成员函数,称为该类的构造函数(constructor)。构造函数是个特殊成员函数,该函数初始化类对象的数据成员。类的构造函数在生成这个类的对象时自动调用。一个类常常有几个构造函数,这是通过函数重载完成的。注意,构造函数不指定返回类型。

常见编程错误6.3

对构造函数指定返回类型或返回值是个语法错误。

成员访问说明符private后面有三个整型成员,表示类的这些数据成员只能让成员函数访问(下一章会介绍还可由类的友元访问)。这样,数据成员只能由类定义中出现函数原型的4个函数(和类的友元)访问。数据成员通常放在类的private部分,成员函数通常放在Public部分。稍后会介绍,也可以用private成员函数和public数据,但比较少见,这不是好的编程习惯。

定义类之后,可以在声明中将其当作类型,如下所示:

Time sunset, // object of type Time
arrayOfTimes[ 5 ], // array of Time objects
*pointerToTime, // pointer to a Time object
&dinnerTime = sunset; // reference to a Time object

类名成为新的类型说明符。一个类可以有多个对象,就像int类型的变量可以有多个。程序员可以在需要时生成新的类类型,因此C++是个可扩展语言(exlensible language)。

图6.3使用Time类。程序实例化Time类的一个对象t。当对象实例化时,Time构造函数自动调用,显式地将每个private数据成员初始化为0。然后按军用格式和标准格式打印时间,确保成员已经正确地初始化。然后用setTime成员函数设置时间,并再次按两种格式打印时间。接着用setTime成员函数设置时间为无效值.并再次按两种格式打印时间。

 // Fig. 6.3: fig06_03.cpp
 // Time class.
 #include < iostream.h>
 // Time abstract data type (ADT) definition
 class Time {
 public:
   Time();                   // Constructor
   void setTime( int, int, int ); // set hour, minute, second
  void printMilitary();          // print military time format
  void printStandard();          // print standard time format
 private:
  int hour;    // 0 - 23
  int minute;  // 0 - 59
  int second;  // 0 - 59
 };
 // Time  tructor initiali  ......  h data membertt~tzer~'-st t
 // Ensures all Time objects start in a conchs en s a e.
 // Set a new Time value using military time. Perform validity
 {
    hour=e ( h >= 0 && h < 24 ) ? h : 0;
          minut  = ( m >= 0 && m < 60 ) ? m  : 0;
   second = ( s >= 0 && s < 60 ) ? s : 0;
}
 // Print Time in military format
 void Time::printMilitary()
         << ( minute < 10 ? "0" : "" ) << minute;
 // Print Time in standard format
 void Time::printStandard()
{
   cout << ( ( hour == 0 || hour == 12 ) ? 12 : hour % 12 )
        << ":" << ( minute < 10 ? "0" :  ....  ) << mlnu e
        << ":" << ( second < 10 ? "0" :  ""  ) << second
        << ( hour < 12 ? "AM" : "PM" );
 }
 // Driver)trna (in  ......  imple class Time
 int main()
 {
     Time t;  // instantiate object t of class Time
   cout << "The initial military time is ";
          t.printMilitary();
   cout << "\nThe initial standard time is ";
   t.printStandard();
   t.setTime( 13, 27, 6 );
   cout << "\n\nMilitary time after setTime is ";
          t.printMilitary();
   cout << "\nStandard time after setTime is ";
   t.printStandard();
   t.setTime( 99, 99, 99 );  // attempt invalid settings
   cout << "\n\nAfter attempting invalid settings:"
        << "\nMilitary time: ";
   t.printMilitary();
   cout << "\nStandard time: ";
   t.printStandard();
   cout << endl;
   return 0;
 }

输出结果:

The initial military time is 00::00

The initial standard time is 12:00:00 AM

Military time after setTime is 13:27

Standard time after setTime is 1:27:06 PM

After attemping invalid settings:

Military time: 00::00

Standard time: 12:00:00 AM

图6.3 用类实现抽象数据类型Time

注意数据成员hour、minute和second前面使用成员访问说明符private。类的private数据成员通常只能在类中访问(下一章会介绍,还可由类的友元访问)。从本例中可以看出,类的客户不关心类中的实际数据表达。例如,类完全可以用从午夜算起的秒数表示时间,这时客户可以用相同的publie成员函数取得相同的结果而并不注意类中的变化。从这种意义上说,类的实现是向客户隐藏起来的。这种信息隐藏提高了程序的可修改性,简化了客户对类的理解。

软件工程视点6.3

类的客户使用类时不必知道类的内部实现细节。如果类的内部实现细节改变(例如为了提高性能),只要 类的接口保持不变,类的客户源代码就不必改变(但客户可能需要重新编译),这样就更容易修改系统。

在这个程序中,Time构造函数只是将数据成员初始化为0(即上午12时的军用时间格式),因此就保证对象生成时具有一致状态。Time对象的数据成员中不可能保存无效值,因为生成Time对象时自动调用构造函数,后面客户对数据成员的修改都是由setTime函数完成的。

软件工程视点6.4

成员函数通常比非面向对象编程中的函数更短,因为数据成员中存放的数据已由构造函数和保存新数据的成员函数验证。由于数据已经是对象,成员函数调用通常没有参数或比非面向对象语言中调用的典型函数的参数更少。这样,调用简化了,函数定义简化了,函数原型也简化了。

注意,类的数据成员无法在类体中声明时初始化,而要用类的构造函数初始化,也可以用给它们设值的函数赋值。

常见编程错误6.4

想在类定义中显式地将类的数据成员初始化是个语法错误。

与类同名而前面加上代字符(~)的函数称为类的析构函数(destructor)(本例没有显式地加上析构函数,系统会插入一个析构函数)。析构函数在系统收回对象的内存之前对每个类对象进行清理工作。析构函数不带参数,无法重载。本章稍后和第7章将详细介绍构造函数与析构函数。

注意,类向外部提供的函数要加上public标号。public函数实现类向客户提供的行为或服务,通常称为类的接口或Public接口。

软件工程视点6.5

客户能访问类的接口,但不能访问类的实现方法。

类定义包含类的数据成员和成员函数的声明。成员函数的声明就是本书前面介绍的函数原型。

成员函数可以在类的内部定义,但在类的外部定义函数是个良好的习惯。

软件工程视点6.6

在类定义中(通过函数原型)声明成员函数而在类定义外定义这些成员函数,可以区分类的接口与实现方法。这样可以实现良好的软件工程,类的客户不能看到类成员函数的实现方法。

注意图6.3类定义中每个成员函数定义使用的二元作用域运算符(::)。定义类和声明成员函数后,就要定义成员函数。类的每个成员函数可以直接在类定义体中定义(而不是包括类的函数原型),也可以在类定义体之后定义成员函数。在类定义体之后定义成员函数时,函数名前面要加上类名和二元作用域运算符(::)。由于不同类可能有同名成员,因此要用二元作用域运算符将成员名与类名联系起来,惟一标识某个类的成员函数。

常见编程错误6.5

在类的外部定义成员函数时,省略函数名中的类名和二元作用域运算符是个语法错误。

尽管类定义中声明的成员函数可以在类定义之外定义,但成员函数仍然在类范围(class'sscope)中,即只有该类的其他成员知道它的名称,除非通过类对象、引用类对象或类对象指针进行引用。稍后将详细介绍类范围。

如果在类定义中定义成员函数,则该成员函数自动成为内联函数。在类体之后定义成员函数时,可以用关键字inline指定其为内联函数。记住,编译器有权不把内联函数放进程序块中。

性能提示6.2

在类定义内定义小的成员函数将自动使该函数成为内联函数(如果编译器选择这么做),这样虽然可以提

高性能,但不能提高软件工程质量,因为类的客户能看到函数实现方法。

软件工程视点6. 7

只有最简单的成员函数才能在类的首部中定义。

有趣的是printMilitary和printStandard成员函数没有参数。这是因为成员函数隐式知道对调用 的特定Time对象打印数据成员。这样就使成员函数调用比过程式编程中的传统函数调用更为简练。

测试与调试提示6. 1

成员函数调用通常不带参数或比非面向对象语言中的传统函数调用参数少得多,从而减少传递错误谩参数、

错误参数类型或错误参数个数的机会。

软件工程视点6.8

利用面向对象编程方法通常能减少传递的参数个数,从而简化函数调用。这个面向对象编程好处是由于

在对象中封装数据成员和成员函数之后,成员函数有权访问数据成员。

类能简化编程,因为客户(或类对象用户)仅需关心对象中封装或嵌入的操作。这种操作通常是面向客户的,而不是面向实现方法的。客户不必关心类的实现方法(当然客户需要正确和有效的实现方法)。接口不是没有改变,只是不像实现方法那样经常改变而已。实现方法改变时,与实现方法有关的代码也要相应改变。通过隐藏实现方法,可以消除程序中与实现方法有关的代码。

软件工程视点6.9

本书的中心主题是“复用、复用、再复用”。我们将认真介绍几个提高复用性的技术,着重介绍”建立宝

贵的类”和建立宝贵的”软件资产”。

类通常不需要从头生成,可以从其他提供新类可用的属性和行为的类派生而来,类中可以包括其他类对象作为成员。这种软件复用可以大大提高程序员的工作效率。从现有类派生新类称为继承(inheritance),将在第9章介绍。把其他类对象作为类的成员称为复合(composition),将在第7章介绍。

不熟悉面向对象编程的人常常担心对象会很大,因为它们要包含数据和函数。逻辑上的确如此,程序员可以把对象看成要包含数据和函数,但实际中并不是这样。

性能提示6.3

实际对象只包含数据,因此要比包含函数的对象小得多。对类名或该类的对象来用sizeof运算符时,只得到该类的数据长度。编译器生成独立于所有类对象的类成员函数副本(只有一份)。自然,因为每个对象的数据是不同的,所以每个对象需要自已的类数据副本。该函数代码是不变的(或称为可重入码或纯过程),因此可以在一个类的所有对象之间的共享。