📄 06章 类与数据抽象(一).txt
字号:
65 << "\nMilitary time: ";
66 t.printMilitary();
67 cout << "\nStandard time: ";
68 t.printStandard();
69 cout << endl;
70 return 0;
71 }
输出结果:
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运算符时,只得到该类的数据长度。编译器生成独立于所有类对象的类成员函数副本(只有一份)。自然,因为每个对象的数据是不同的,所以每个对象需要自已的类数据副本。该函数代码是不变的(或称为可重入码或纯过程),因此可以在一个类的所有对象之间的共享。
6.6 类范围与访问类成员
类的数据成员(类定义中声明的变量)和成员函数(类定义中声明的函数)属于该类的类范围(class's scope)。非成员函数在文件范围(file scope)中定义。
在类范围中,类成员可由该类的所有成员函数直接访问,也可以用名称引用。在类范围外,类成员是通过一个对象的句柄引用,可以是对象名、对象引用或对象指针(第7章将介绍,每次引用对象中的数据成员和成员函数时,编译器插入一个隐式句柄)。
类的成员函数可以重载,但只能由这个类的其他成员函数重载。要重载成员函数,只要在类定义中提供该重载函数每个版本的原型,并对该重载函数每个版本提供不同的函数定义。
成员函数在类中有函数范围(function scope),成员函数内定义的变量只能在该函数内访问。如果成员函数定义与类范围内的变量同名的变量,则在函数范围内,函数范围内的变量掩盖类范围内的变量。这种隐藏变量可以通过在前面加上类名和作用域运算符(::)而访问。隐藏的全局变量可以用一元作用域运算符访问(见第3章)。
访问类成员的运算符与访问结构成员的运算符是相同的。圆点成员选择运算符(.)与对象名或对象引用组合,用于访问对象成员。箭头成员选择运算符(->)与对象指针组合,用于访问对象成员。
图6.4的程序用简单的Count,类和public数据成员x(int类型)以及public成员函数print演示如何用成员选择运算符访问类成员。程序实例化三个Count类型的变量--counter、counterRef(Count对象的引用)和counterPtr(Count对象的指针)。变量counterRef定义为引用Counter,变量countcrPtr定义为指向counter。注意,这里将数据成员x设置为public,只是为了演示public成员利用句柄(如名称、引用或指针)即可访问。前面曾介绍过,数据通常指定为private,第9章“继承”中将介绍有时可以将数据指定为protected。
1 // Fig. 6.4: fig06_04.cpp
2 // Demonstrating the class member access operators . and ->
3 //
4 // CAUTION: IN FUTURE EXAMPLES WE AVOID PUBLIC DATA!
5 #include <iostream.h>
6
7 // Simple class Count
8 class Count {
9 public:
10 int x;
11 void print() { cout << x << endl; }
12 };
13
14 int main()
15 {
16 Count counter, // create counter object
17 *counterPtr = &counter, // pointer to counter
18 &counterRef = counter; // reference to counter
19
20 cout << "Assign 7 to x and print using the object's name: ";
21 counter.x = 7; // assign 7 to data member x
22 counter.print(); // call member function print
23
24 cout << "Assign 8 to x and print using a reference: ";
25 counterRef.x = 8; // assign 8 to data member x
26 counterRef.print(); // call member ~unction print
27
28 cout << "Assign 10 to x and print using a pointer: ";
29 counterPtr->x = 10; // assign 10 to data member ~
30 counterPtr->print(); // call member function print
31 return 0;
32 }
输出结果:
Assign 7 to x and print using the object's name: 7
Assign 8 to x and print using a reference: 8
Assign 10 to x and pring using a pointer: 10
图 6.4 通过各种句柄访问对象的数据成员和成员函数
6.7 接口与实现方法的分离
良好软件工程的一个基本原则是将接口与实现方法分离,这样可以更容易修改程序。就类的客户而言,类实现方法的改变并不影响客户,只要类的接口保持不变即可(类的功能可能扩展到原接口以外)。
软件工程视点6.10
将类声明放在使用该类的任何客户的头文件中,这就形成类的Public接口(并向客户提供调用类成员函数所需的函数原型)。将类成员函数的定义放在源文件中,这就形成类的实现方法。
软件工程视点6.11
类的客户使用类时不需要访问类的源代码,但客户需要连挂类的目标码。这样就可以由独立软件供应商(ISV)提供类库进行销售和发放许可证。ISV只在产品中提供头文件和目标模块,不提供专属信息(例如源代码)。C++用户可以享用更多的ISV生产的类库。
实际上,任何事情都不是十全十美的。头文件中包含一些实现部分,并隐藏了实现方法的其他函数定义。private成员列在头文件的类定义中.因此客户虽然无法访问private成员,但能看到这些成员。第7章将介绍如何用代理类从类的客户中隐藏类的private数据。
软件工程视点6.12
对类接口很重要的信息应放在头文件中。只在类内部使用而类的客户不需要的信息应放在不发表的源文件中。这是最低权限原则的又一个例子。
图6.5将图6.3的程序分解为多个文件。建立C++程序时,每个类定义通常放在头文件中,类的成员函数定义放在相同基本名字的源代码文件(source-code file)中。在使用类的每个文件中包含头文件(通过#include),而源代码文件编译并连接包含主程序的文件。编译器文档中介绍了如何编译和连接由多个源文件组成的程序。
图6.5包含声明Time类的time1.h头文件、定义Time类成员函数的Time1.cpp文件和定义main函数的fig06_05.cpp文件。这个程序的输出与图6.3的输出相同。
1 // Fig. 6.5: timel.h
2 // Declaration of the Time class.
3 // Member functions are defined in timel.cpp
4
5 // prevent multiple inclusions of header file
6 #ifndef TIME1_H
7 #define TIME1_H
8
9 // Time abstract data type definition
10 class Time {
11 public:
12 Time(); // constructor
13 void setTime( int, int, int ); // set hour, minute, second
14 void printMilitary(); // print military time format
15 void printStandard(); // print standard time format
16 private:
17 int hour; // 0 - 23
16 int minute; // 0 59
19 int second; // 0 - 59
20 };
21
22 #endif
23 // Fig. 6.5: timel.cpp
24 // Member function definitions for Time class.
25 #include <iostream.h>
26 #include "time1.h"
27
28 // Time constructor initializes each data member to zero.
29 // Ensures all Time objects start in a consistent state.
30 Time::Time() { hour = minute = second = 0; }
31
32 // Set a new Time value using military time. Perform validity
33 // checks on the data values. Set invalid values to zero.
34 void Time::setTimm( int b, int m, int s )
35 {
36 hour = ( h >= 0 && h < 24 ) ? h : 0;
37 minute ( m >= 0 && m < 60 ) ? m : 0;
38 second = ( s >= 0 && s < 60 ) ? s : 0;
39 }
40
41 // Print Time in military format
42 void Time::printMilitary()
43 {
44 cout << (hourt< 10 ? "0" : "" ) << hour << ":"
45 ( minute < l0 ? "0" : "" ) << minute;
46 }
47
48 // Print time in standard format
49 void Time::printStandard()
50 {
51 cout << ( ( hour( == 0 || hour == 12 ) ? 12 : hourt% 12 )
52 << ":" << minute < l0 ? "0" : "" ) << mlnute
53 << ":" << ( second < l0 ? "0" : "" ) << second
54 << ( hour < 12 ? "AM" : "PM" );
55 }
56 // Fig. 6.5: fig06_05.cpp
57 // Driver for Timel class
58 // NOTE: Compile with timel.cpp
59 #include <iostream.h>
60 #include "time1.h"
61
62 // Driver to in main( test simple class Time
63 int main()
64 {
65 Time t; // instantiate object t of class time
66
67 cout << "The initial military time is";
68 t.printMilitary();
69 cout << "\nThe initial standard time is";
70 t.printStandardO;
71
72 t.setTime( 13, 27, 6 );
73 cout << "\n\nMilitary time after setTime is";
74 t.printMilitary();
75 cout << "%nStandard time after setTime is";
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -