第一章 类与数据抽象 .......................................................................................................................... 2 1 . 1 简介 ........................................................................................................................................ 3 1 . 2 结构定义 ............................................................................................................................. 4 1 . 3 访问结构成员 ................................................................................................................... 4 1 . 4 用 struct 实现用户自定义类型 Time .................................................................. 5 1 . 5 用类实现 Time 抽象数据类 ....................................................................................... 7 1 . 6 类范围与访问类成员 .................................................................................................. 13 1.7 接口与实现方法的分离 ................................................................................................... 14 1.8 控制对成员的访问 ........................................................................................................... 18 1.9 访问函数与工具函数 ..................................................................................................... 20 1.10 初始化类对象:构造函数 ......................................................................................... 23 1.11 在构造函数中使用默认参数 .................................................................................... 24 1.12 使用析构函数 ................................................................................................................... 27 1.13 何时调用构造函数与析构函数 ............................................................................... 28 1.14 使用数据成员和成员函数 .......................................................................................... 31 1.15 微妙的陷阱:返回对 private 数据成员的引用 ............................................ 35 1.16 通过默认的成员复制进行赋值 ............................................................................... 38 1.17 软件复用性 ........................................................................................................................ 39 1.18 有关对象的思考:编写电梯模拟程序的 .......................................................... 40 第二章 运算符重载 ............................................................................................................................ 42 2 . 1 简介 ..................................................................................................................................... 42 2 . 2 运算符重载的基础 ..................................................................................................... 42 2 . 3 运算符重载的限制 ..................................................................................................... 44 8 . 4 用作类成员与友元函数的运算符函数 ............................................................ 45 2 . 5 重载流插入与流读取运算符 ................................................................................. 46 2 . 6 重载一元运算符 ........................................................................................................... 49 2 . 7 重载二元运算符 ........................................................................................................... 50 2 . 8 实例研究: Array 类 .................................................................................................. 50 2 . 9 类型之间的转换 ........................................................................................................... 61 2 . 10 实例研究: String 类 .............................................................................................. 62 2 . 11 重载 ++ 与 -- .................................................................................................................. 73 2 . 12 实例研究: Date 类 ................................................................................................... 74 第三章 继承 ............................................................................................................................................ 80 3 . 1 简介 ..................................................................................................................................... 80 3 . 2 继承:基类和派生类 ................................................................................................ 82 3 . 3 protected 成员 ............................................................................................................. 83 3 . 4 把基类指针强制转换为派生类指针 .................................................................. 83 3 . 5 使用成员函数 ................................................................................................................ 88 3 . 6 在派生类中重定义基类成员 ................................................................................. 88 3 . 7 public 、 protected 和 private 继承 ................................................................. 92 3 . 8 直接基类和间接基类 ................................................................................................ 93 3 . 9 在派生类中使用构造函数和析构函数 ............................................................. 93 3 . 10 将派生类对象隐式转换为基类对象 ................................................................ 97 9 . 11 关于继承的软件工程 ............................................................................................... 98 1 3 . 12 复合与继承的比较 .................................................................................................... 99 3 . 13 对象的“使用”关系和“知道”关系 ......................................................... 100 3 . 14 实例研究:类 Point 、 Circle 和 Cylinder ................................................ 100 3 . 15 多重继承 ...................................................................................................................... 107 第四章 虚函数和多态 ...................................................................................................................... 113 4 . 1 简介 ................................................................................................................................... 114 4 . 2 类型域和 switch 语句 ............................................................................................. 114 4 . 3 虚函数 .............................................................................................................................. 114 4 . 4 抽象基类和具体类 .................................................................................................... 115 4 . 5 多态性 .............................................................................................................................. 116 4 . 6 实例研究:利用多态性的工资单系统 ........................................................... 117 4 . 10 多态、虚函数和动态关联 .................................................................................. 136 第五章 模板 .......................................................................................................................................... 139 5 . 1 简介 ................................................................................................................................... 139 5 . 2 函数模板 ........................................................................................................................ 140 12. 3 重载模板函数 ................................................................................................................ 143 5 . 4 类模板 ............................................................................................................................. 143 5 . 5 类模板与无类型参数 .............................................................................................. 149 5 . 6 模板与继承 ....................................................................................................................... 149 5 . 7 模板与友元 ................................................................................................................... 150 5 . 8 模板与 static 成员 ................................................................................................... 151 第一章 类与数据抽象 教学目标 ●了解封装与数据隐藏的软件工程概念 ●了解数据抽象和抽象数据类型(ADT)的符号 ●生成 C++的 ADT(即类) ●了解如何生成、使用和删除类对象 ●控制对象数据成员和成员函数的访问 2 ●开始认识面向对象的价值 1.1 简介 下面开始介绍 C++中的面向对象。为什么把 C++中的面向对象推迟到第 6 章才开始介绍呢? 原因是我们要建立的对象是由各个结构化程序组件构成,因此先要建立结构化程序的基 础知识。 我们介绍了 C++中的面向对象编程的基本概念(即“对象思想”)和术语(即”对象语 言”)。在这些”有关对象的思考”小节中,介绍了面向对象设计(object- orienteddesign,OOD)的方法:我们分析了典型问题的陈述,要求建立一个系统(电梯模 拟程序),确定实现该系统所需的类,确定这些类对象的属性,确定这些类对象的行为, 指定对象之间如何通过交互以完成系统的总体目标。 下面简要介绍面向对象的一些关键概念和术语。OOP 将数据(属性)和函数(行为)封装 (encapsulate)到称为类(class)的软件包中,类的数据和成员是密切联系的。就像蓝图,建 筑人员通过蓝图建造房子,而程序虽则通过类生成对象。一个蓝图可以多次复用.建造多 幢房子;一个类也可以多次夏用,建立多个对象。类具有信息隐藏(information hiding) 属性,即类对象只知道如何通过定义良好的接口(interface)与其他类对象通信,但通常不 知道其他类的实现方法,实现细节隐藏在类中。我们可以熟练地开车,而不需要知道发动 机、传递系统和燃油系统的工作原理。我们可以看到信息隐藏对良好的软件工程是多么重 要。 在 C 语言和其他过程化编程语言(proceduralprogramminglanguage)中,编程是面向 操作的(action-oriented),而在 C++中,编程是面向对象的(object-oriented)。在 C 语言 中,编程的单位是函数(function),而在 C++中.编程的单位是类(class),对象最终要通 过类实例化。 C 语言程序员的主要工作是编写函数,完成某个任务的一组操作构成函数,函数的 组合则构成程序。数据在 C 语言中当然很重要,但这些数据只用于支持函数所要进行的操 作。系统指定中的动词帮助 C 语言程序员确定一组用于实现系统的函数。 C++程序员把重点放在生成称为类的用户自定义类型(user-definedtype),类也称为 程序员定义类型(programmer-defined type)。每个类包含数据和操作数据的一组函数。类 的数据部分称为数据成员(data member)。类的函数部分称为成员函数(member function, 有些面向对象语言中也称为方法)。int 等内部类型的实例称为变量(variable),而用户自 定义类型(即类)的实例则称为对象(object)。在 C++中,变量与对象常常互换使用,C++的 重点是类而不是函数。系统指定中的名词帮助 C++程序员确定实现系统所需的用来生成对 象的一组类。 C++中的类是由 C 语言中的 struct 演变而来的。介绍 C++类的开发之前.我们先使用 结构建立用户自定义类型,通过介绍这种方法的缺点从而说明类的优点。 3 1.2 结构定义 结构是用其他类型的元素建立的聚合数据类型。考虑下列结构定义: struct Time { int hour; // 0-23 int minute; // 0-59 int second; // 0-59 }; 结构定义用关键字 struct 引入。标识符 Time 是个结构标志(structure tag),命名结构 定义并声明该结构类型(structure type)的变量。本例中,新类型名为 Time。结构定义花括 号中声明的名称是结构的成员(member)。同一结构的成员应有惟一名称.但两个不同结构 可以包含同名成员而不会发生冲突。每个结构定义应以分号结尾。上述解释对后面要介绍 的类也适用,C++中的结构和类是非常相似的。 Time 的定义包含三个 int 类型的成员 hour、minute 和 second。结构成员可以是任何类 型,一个结构可以包含不同类型的成员。但是,结构不能包含自身的实例。例如,Time 类 型的成员不能在 Time 的结构定义中声明,但该结构定义中可以包含另一 Time 结构的指 针。当结构包含同一类型结构的指针时,称为自引用结构(self-referential structure)。 自引用结构用于形成链接数据结构,如链表、队列、堆栈和树等 上述结构定义并没有在内存中保留任何空间,而是生成新的数据类型,用于声明变 量。结构变量和其他类型的变量一样声明。下列声明: Time timeObject,timeArray[10] ,*timePtr. &timeRef=timeobject; 声明 timeObject 为 Time 类型变量,timeArray 为 10 个 Time 类型元素的数组,timePtr 为 Time 对象的指针,timeRef 为 Time 对象的引用(用 timeObject 初始化)。 1.3 访问结构成员 访问结构成员或类成员时,使用成员访问运算符(member access operator),包括圆 点运算符(.)和箭头运算符(—>)。圆点运算符通过对象的变量名或对象的引用访问结构和 类成员。例如,要打印 timeObject 结构的 hour 成员,用下列语句: cout<和大于号(>)组成,中间不能插空格,通过对象指针访问结构 和类成员。假没指针 timePtr 声明为指向 Time 对象,结构 timeObject 的地址赋给 timePtr。要打印指针为 timePtr 的 timeObjeet 结构的 hour 成员,用下列语句: tzmePtr=&timeObject; cout<hour; 表达式 timePtr->hour 等价于(*timePtr).hour,后者复引用指针并用圆点运算符访问 hour 成员。 4 这里的括号是必需的,因为圆点运算符的优先级高于复引用指针运算符(*)箭头运算符和 圆点运算符以及括号与方括号([])的优先级较高,仅次于作用域运算符,结合律为从左向 右。 常见编程错误 1.1 表达式(*timePtr).hour 指 timePtr 所指 struct 的 hour 成员。省略括号的 *timePtr.hour 是个语法错误,因为“.”的优先级高于“*”,表达式变成 *(timePtrhour)。这是个语法错误,因为指针要用箭头运算符引用成员。 1.4 用 struct 实现用户自定义类型 Time 图 1.1 生成用户自定义类型 Time,有三个整数成员 hour、minute 和 second。程序定义 一个 Time 类型的结构 dinnerTime,并用圆点运算符初始化结构成员 hour、minute 和 second 的值分别为 18、30 和 0。然后程序按军用格式(或所谓“通用格式”)和标准格式打印 时间。注意打印函数接收常量 Time 结构的引用,从而通过引用将 Time 类型的结构传递给 打印函数,避免了按值传人打印函数所涉及的复制开销.并用 const 防止打印函数修改 Time 结构。第 7 章将介绍 const 对象与 const 成员函数。 1 // Fig. 1.1: fig0601.cpp 2 // Create a structure, set its members, and print it. 3 #include 5 struct Time { // structure definition 6 int hour; // 0-23 7 int minute; // 0-59 8 int second; // 0-59 9 }; 10 11 void printMilitary( const Time & ); // prototype 12 void printStandard( const Time & ); // prototype 13 14 int main() 15 ( 16 Time dinnerTime; // variable of new type Time 17 18 // set members to valid values 19 dinnerTime.hour = 18; 20 dinnerTime.minute = 30; 21 dinnerTime.second = O; 22 23 cout << "Dinner will be held at "; 24 printMilitary( dinnerTime ); 25 cout << " military time, \nwhich is "; 26 printStandard( dinnerTime ); 5 27 cout << "standard time.\n"; 28 29 // set members to invalid values 30 dinnerTime.hour = 29; 31 dinnerTime.minute = 73; 32 33 cout << "\nTime with invalid values: "; 34 printMilitary( dinnerTime ); 35 cout << endl; 36 return 0; 37 ) 38 39 // Print the time in military format 40 void printMilitary( const Time &t ) 41 { 42 cout << ( t.hour < 10? "0" : "" ) << t.hour << ":" 43 << ( t.minute < 10? "0" : "" ) << t.minute; 44 } 45 46 // Print the time in standard format 47 void printStandard( const Time &t ) 48 { 49 cout << ( ( t.hour == 0 || t.hour == 12 ) ? 50 12 : t.hour % 12 ) 51 << ":" << ( t.minute < 10 ? "0" : "" ) << t.minute 52 << ":" << ( t.second < 10? "0" : "" ) << t.second 53 << ( t.hour < 12 ? "AM" : "PM" ); 54 } 输出结果: Dinner will be held at 18:30 military time, which is 6:30:00 PM standard time. Time with invalid values: 29:73 性能提示 1.1 结构通常按值调用传递。要避免复制结构的开销,可以按引用调用传递结构。 软件工程视点 1. 1 传递较大的结构体时,应该使用指针传递。 软件工程视点 1.2 要避免按值调用传递的开销而且保护调用者的原始数据不被修改.可以将长度很大 的参数作为 const 引用传递。 6 用这种方式通过结构生成新数据类型有一定的缺点。由于初始化并不是必须的,因此 就可能出现未初始化的数据,从而造成不良后果。即使数据已经初始化,也可能没有正确 地初始化。因为程序能够直接访问数据,所以无效数据可能赋给结构成员(如图 1.1)。在第 30 行和第 31 行,程序很容易向 Time 对象 dinnerTime 的 hour 和 minute 成员传递错值。如 果 struct 的实现方法改变(例如时间可以表示为从午夜算起的秒数),则所有使用这个 struct 的程序都要改变。这是因为程序员直接操作数据类型。没有一个”接口”保证程序 员正确使用数据类型并保持数据的一致状态。 一定要编写易于理解和易于维护的程序。不断改变是规则而不走例外。程序员应预料到代 码要经常改变。 可以看出,类能够提高程序的可修改性。 还有其他与 C 语言式结构相关的问题。在 C 语言中,结构不能作为一个单位打印,而 要一次一个地打印和格式化结构成员。可以编写一个函数,以某种格式打印结构成员。” 运算符重载”中演示了如何重载<<运算符,使结构类型或类类型的对象能够方便地打印。 在 C 语言中,结构不能整体进行比较,而只能一个成员一个成员地比较。演示如何重载相 等运算符与关系运算符,比较 C++结构类型或类类型的对象。 下一节重新将 Time 结构实现为 C++类,并演示用类生成抽象数据类型(abstract data type)的好处。从中将会看到,C++中类和结构的用法基本相同,差别在于各自的成 员相关的默认访问能力不同。 1.5 用类实现 Time 抽象数据类 类使程序员可以构造对象的属性(attribute,表示为数据成员)和行为(behavior)或操 作(operation,表示为成员函数)。C++中用关键字 class 定义包含数据成员和成员函数的 类型。 成员函数在有些面向对象编程语言中也称为方法(method),响应对象接收的消息 (message)。消息对应于一个对象发给另一个对象或由函数发给对象的成员函数调用。 一旦定义了一个类,可以用类名声明该类的对象。图 1.2 显示了 Time 类的简单定义。 Time 类定义以关键字 class 开始。类定义体放在左右花括号(C1)之间,类定义用分号 终止。Time 类定义和 Time 类结构定义各包含三个整型成员 hour、minute 和 second。 1 class Time { 2 public: 3 Time(); 4 void setTime( int, int, int); 5 void printMilitary(); 6 void printStandard(); 7 private: 8 int hour; // 0-23 9 int minute; // 0-59 10 int second; // 0-59 11 }; 7 图 1. 2 Time 类的简单定义 常见编程错误 1.2 忘记类或结构定义结束时的分号是个语法错误。 类定义的其他部分是新内容。public:和 private:标号称为成员访问说明符(member accessspecifier)。在程序能访问 Time 类对象的任何地方都可以访问任何在成员访问说明 符 public 后面(和下一个成员访问说明符之前)声明的数据成员和成员函数。成员访问说明 符 private 后面(和下一个成员访问说明符之前)声明的数据成员和成员函数只能由该类的 成员函数访问。成员访问说明符总是加上冒号,可以在类定义中按任何顺序多次出现。本 文余下部分使用不带冒号的成员访问说明符 public 和 private。 程中的作用。 编程技巧 1.1 每个成员访问说明符只在类定义中使用一次,这样可以增加清晰性与可读性。将 public 成员放在前面,便于寻找。 类定义中的访问说明符 public 后面是成员函数 Time、setTime、printMihtary 和 printStandard 的函数原型。这些函数是类的 public 成员函数(或 public 服务、public 行为、 类的接口)。类的客户(client,即程序中的用户部分)使用这些函数操作该类的数据。 注意与类名相同的成员函数,称为该类的构造函数(constructor)。构造函数是个特殊 成员函数,该函数初始化类对象的数据成员。类的构造函数在生成这个类的对象时自动调 用。一个类常常有几个构造函数,这是通过函数重载完成的。注意,构造函数不指定返回 类型。 常见编程错误 1.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)。 图 1.3 使用 Time 类。程序实例化 Time 类的一个对象 t。当对象实例化时,Time 构造 函数自动调用,显式地将每个 private 数据成员初始化为 0。然后按军用格式和标准格式 打印时间,确保成员已经正确地初始化。然后用 setTime 成员函数设置时间,并再次按两 种格式打印时间。接着用 setTime 成员函数设置时间为无效值.并再次按两种格式打印时 间。 8 1 // Fig. 1.3: fig06_03.cpp 2 // Time class. 3 #include 4 5 // Time abstract data type (ADT) definition 6 class Time { 7 public: 8 Time(); // Constructor 9 void setTime( int, int, int ); // set hour, minute, second 10 void printMilitary(); // print military time format 11 void printStandard(); // print standard time format 12 private: 13 int hour; // 0 - 23 14 int minute; // 0 - 59 15 int second; // 0 - 59 16 }; 17 18 // Time tructor initiali ...... h data membertt~tzer~'-st t 19 // Ensures all Time objects start in a conchs en s a e. 21 22 // Set a new Time value using military time. Perform validity 25 Time::Time(){ 26 hour=0; minute=0; second=0; 29} void Time::setTime(int h,int m,int s) { hour = h>23?0:h; minute=m>59?0:m; second=s>59?0:m; } 31 // Print Time in military format 32 void Time::printMilitary() 35 << ( minute < 10 ? "0" : "" ) << minute; 37 38 // Print Time in standard format 39 void Time::printStandard() 4O { 41 cout << ( ( hour == 0 || hour == 12 ) ? 12 : hour % 12 ) 9 42 << ":" << ( minute < 10 ? "0" : .... ) << mlnu e 43 << ":" << ( second < 10 ? "0" : "" ) << second 44 << ( hour < 12 ? "AM" : "PM" ); 45 } 46 47 // Driver)trna (in ...... imple class Time 48 int main() 49 { 50 Time t; // instantiate object t of class Time 51 52 cout << "The initial military time is "; 53 t.printMilitary(); 54 cout << "\nThe initial standard time is "; 55 t.printStandard(); 56 57 t.setTime( 13, 27, 6 ); 58 cout << "\n\nMilitary time after setTime is "; 59 t.printMilitary(); 60 cout << "\nStandard time after setTime is "; 61 t.printStandard(); 62 63 t.setTime( 99, 99, 99 ); // attempt invalid settings 64 cout << "\n\nAfter attempting invalid settings:" 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 图 1.3 用类实现抽象数据类型 Time 10 注意数据成员 hour、minute 和 second 前面使用成员访问说明符 private。类的 private 数据成员通常只能在类中访问(下一章会介绍,还可由类的友元访问)。从本例中 可以看出,类的客户不关心类中的实际数据表达。例如,类完全可以用从午夜算起的秒数 表示时间,这时客户可以用相同的 publie 成员函数取得相同的结果而并不注意类中的变 化。从这种意义上说,类的实现是向客户隐藏起来的。这种信息隐藏提高了程序的可修改 性,简化了客户对类的理解。 软件工程视点 1.3 类的客户使用类时不必知道类的内部实现细节。如果类的内部实现细节改变(例如为 了提高性能),只要 类的接口保持不变,类的客户源代码就不必改变(但客户可能需要 重新编译),这样就更容易修改系统。 在这个程序中,Time 构造函数只是将数据成员初始化为 0(即上午 12 时的军用时间格 式),因此就保证对象生成时具有一致状态。Time 对象的数据成员中不可能保存无效值, 因为生成 Time 对象时自动调用构造函数,后面客户对数据成员的修改都是由 setTime 函 数完成的。 软件工程视点 1.4 成员函数通常比非面向对象编程中的函数更短,因为数据成员中存放的数据已由构 造函数和保存新数据的成员函数验证。由于数据已经是对象,成员函数调用通常没有参数 或比非面向对象语言中调用的典型函数的参数更少。这样,调用简化了,函数定义简化了, 函数原型也简化了。 注意,类的数据成员无法在类体中声明时初始化,而要用类的构造函数初始化,也 可以用给它们设值的函数赋值。 常见编程错误 1.4 想在类定义中显式地将类的数据成员初始化是个语法错误。 与类同名而前面加上代字符(~)的函数称为类的析构函数(destructor)(本例没有显式 地加上析构函数,系统会插入一个析构函数)。析构函数在系统收回对象的内存之前对每 个类对象进行清理工作。析构函数不带参数,无法重载。本章稍后和第 7 章将详细介绍构 造函数与析构函数。 注意,类向外部提供的函数要加上 public 标号。public 函数实现类向客户提供的行 为或服务,通常称为类的接口或 Public 接口。 软件工程视点 1.5 客户能访问类的接口,但不能访问类的实现方法。 类定义包含类的数据成员和成员函数的声明。成员函数的声明就是本书前面介绍的函 数原型。 成员函数可以在类的内部定义,但在类的外部定义函数是个良好的习惯。 软件工程视点 1.6 在类定义中(通过函数原型)声明成员函数而在类定义外定义这些成员函数,可以区 11 分类的接口与实现方法。这样可以实现良好的软件工程,类的客户不能看到类成员函数的 实现方法。 注意图 1.3 类定义中每个成员函数定义使用的二元作用域运算符(::)。定义类和声明 成员函数后,就要定义成员函数。类的每个成员函数可以直接在类定义体中定义(而不是 包括类的函数原型),也可以在类定义体之后定义成员函数。在类定义体之后定义成员函 数时,函数名前面要加上类名和二元作用域运算符(::)。由于不同类可能有同名成员,因此 要用二元作用域运算符将成员名与类名联系起来,惟一标识某个类的成员函数。 常见编程错误 1.5 在类的外部定义成员函数时,省略函数名中的类名和二元作用域运算符是个语法错 误。 尽管类定义中声明的成员函数可以在类定义之外定义,但成员函数仍然在类范围 (class'sscope)中,即只有该类的其他成员知道它的名称,除非通过类对象、引用类对象或 类对象指针进行引用。稍后将详细介绍类范围。 如果在类定义中定义成员函数,则该成员函数自动成为内联函数。在类体之后定义成 员函数时,可以用关键字 inline 指定其为内联函数。记住,编译器有权不把内联函数放 进程序块中。 性能提示 1.2 在类定义内定义小的成员函数将自动使该函数成为内联函数(如果编译器选择这么 做),这样虽然可以提 高性能,但不能提高软件工程质量,因为类的客户能看到函数实现方法。 软件工程视点 1. 7 只有最简单的成员函数才能在类的首部中定义。 有趣的是 printMilitary 和 printStandard 成员函数没有参数。这是因为成员函数隐 式知道对调用 的特定 Time 对象打印数据成员。这样就使成员函数调用比过程式编程中 的传统函数调用更为简练。 测试与调试提示 1. 1 成员函数调用通常不带参数或比非面向对象语言中的传统函数调用参数少得多,从 而减少传递错误谩参数、 错误参数类型或错误参数个数的机会。 软件工程视点 1.8 利用面向对象编程方法通常能减少传递的参数个数,从而简化函数调用。这个面向对 象编程好处是由于 在对象中封装数据成员和成员函数之后,成员函数有权访问数据成员。 类能简化编程,因为客户(或类对象用户)仅需关心对象中封装或嵌入的操作。这种操 作通常是面向客户的,而不是面向实现方法的。客户不必关心类的实现方法(当然客户需 要正确和有效的实现方法)。接口不是没有改变,只是不像实现方法那样经常改变而已。实 12 现方法改变时,与实现方法有关的代码也要相应改变。通过隐藏实现方法,可以消除程序 中与实现方法有关的代码。 软件工程视点 1.9 本书的中心主题是“复用、复用、再复用”。我们将认真介绍几个提高复用性的技术, 着重介绍”建立宝 贵的类”和建立宝贵的”软件资产”。 类通常不需要从头生成,可以从其他提供新类可用的属性和行为的类派生而来,类 中可以包括其他类对象作为成员。这种软件复用可以大大提高程序员的工作效率。从现有 类派生新类称为继承(inheritance)。 不熟悉面向对象编程的人常常担心对象会很大,因为它们要包含数据和函数。逻 辑上的确如此,程序员可以把对象看成要包含数据和函数,但实际中并不是这样。 性能提示 1.3 实际对象只包含数据,因此要比包含函数的对象小得多。对类名或该类的对象来用 sizeof 运算符时,只得到该类的数据长度。编译器生成独立于所有类对象的类成员函数副 本(只有一份)。自然,因为每个对象的数据是不同的,所以每个对象需要自已的类数据副 本。该函数代码是不变的(或称为可重入码或纯过程),因此可以在一个类的所有对象之间 的共享。 1.6 类范围与访问类成员 类的数据成员(类定义中声明的变量)和成员函数(类定义中声明的函数)属于该类 的类范围(class's scope)。非成员函数在文件范围(file scope)中定义。 在类范围中,类成员可由该类的所有成员函数直接访问,也可以用名称引用。在类范 围外,类成员是通过一个对象的句柄引用,可以是对象名、对象引用或对象指针。 类的成员函数可以重载,但只能由这个类的其他成员函数重载。要重载成员函数,只 要在类定义中提供该重载函数每个版本的原型,并对该重载函数每个版本提供不同的函 数定义。 成员函数在类中有函数范围(function scope),成员函数内定义的变量只能在该函数 内访问。如果成员函数定义与类范围内的变量同名的变量,则在函数范围内,函数范围内 的变量掩盖类范围内的变量。这种隐藏变量可以通过在前面加上类名和作用域运算符(::)而 访问。隐藏的全局变量可以用一元作用域运算符访问。 访问类成员的运算符与访问结构成员的运算符是相同的。圆点成员选择运算符(.)与对 象名或对象引用组合,用于访问对象成员。箭头成员选择运算符(->)与对象指针组合,用 于访问对象成员。 图 1.4 的程序用简单的 Count,类和 public 数据成员 x(int 类型)以及 public 成员函 数 print 演示如何用成员选择运算符访问类成员。程序实例化三个 Count 类型的变量-- counter、counterRef(Count 对象的引用)和 counterPtr(Count 对象的指针)。变量 13 counterRef 定义为引用 Counter,变量 countcrPtr 定义为指向 counter。注意,这里将数 据成员 x 设置为 public,只是为了演示 public 成员利用句柄(如名称、引用或指针)即可访 问。前面曾介绍过,数据通常指定为 private, “继承”中将介绍有时可以将数据指定为 protected。 1 // Fig. 1.4: fig06_04.cpp 2 // Demonstrating the class member access operators . and -> 3 // 4 // CAUTION: IN FUTURE EXAMPLES WE AVOID PUBLIC DATA! 5 #include 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 图 1.4 通过各种句柄访问对象的数据成员和成员函数 14 1.7 接口与实现方法的分离 良好软件工程的一个基本原则是将接口与实现方法分离,这样可以更容易修改程序。 就类的客户而言,类实现方法的改变并不影响客户,只要类的接口保持不变即可(类的功 能可能扩展到原接口以外)。 软件工程视点 1.10 将类声明放在使用该类的任何客户的头文件中,这就形成类的 Public 接口(并向客户 提供调用类成员函数所需的函数原型)。将类成员函数的定义放在源文件中,这就形成类 的实现方法。 软件工程视点 1.11 类的客户使用类时不需要访问类的源代码,但客户需要连挂类的目标码。这样就可以 由独立软件供应商(ISV)提供类库进行销售和发放许可证。ISV 只在产品中提供头文件和目 标模块,不提供专属信息(例如源代码)。C++用户可以享用更多的 ISV 生产的类库。 实际上,任何事情都不是十全十美的。头文件中包含一些实现部分,并隐藏了实现方 法的其他函数定义。private 成员列在头文件的类定义中.因此客户虽然无法访问 private 成员,但能看到这些成员。 软件工程视点 1.12 对类接口很重要的信息应放在头文件中。只在类内部使用而类的客户不需要的信息应 放在不发表的源文件中。这是最低权限原则的又一个例子。 图 1.5 将图 1.3 的程序分解为多个文件。建立 C++程序时,每个类定义通常放在头文件 中,类的成员函数定义放在相同基本名字的源代码文件(source-code file)中。在使用类 的每个文件中包含头文件(通过#include),而源代码文件编译并连接包含主程序的文件。 编译器文档中介绍了如何编译和连接由多个源文件组成的程序。 图 1.5 包含声明 Time 类的 time1.h 头文件、定义 Time 类成员函数的 Time1.cpp 文件和 定义 main 函数的 fig06_05.cpp 文件。这个程序的输出与图 1.3 的输出相同。 1 // Fig. 1.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 { 15 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. 1.5: timel.cpp 24 // Member function definitions for Time class. 25 #include 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" ); 16 55 } 56 // Fig. 1.5: fig06_05.cpp 57 // Driver for Timel class 58 // NOTE: Compile with timel.cpp 59 #include 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"; 76 t.printStandard(); 77 78 t.setTime( 99, 99, 99 ); // attempt invalid settings 79 count << "\n\nAfter attempting invalid settings:\n" 80 << "Military time:"; 81 t.printMilitary(); 82 cout << "\.Standard time:"; 83 t.printStamdard(); 84 cout << endl; 85 return 0; 86 } 输出结果: 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 attempting invalid settings: Military time: 00:00 Standard time: 12:00:00 AM 17 图 1.5 将 Time 类的接口与实现方法分离 注意类声明放在下列预处理代码中: // prevent multiple inclusions of header file #ifndef TIME1_H #define TIME1_H ... #defint 建立大程序时,其他定义和声明也放在头文件中。上述预处理指令使得在定义了 TIME1_H 名字时不再包含#ifndef 和#endif 之间的代码。如果文件中原先没有包含头文件, 则 TIME1_H 名字由#define 指令定义,并使该文件包含头文件语句。如果文件中已经包含 头文件,则 TIME1_H 名字已经定义,不再包含头文件语句。多次包含头文件语句通常发生 在大程序中,许多头文件本身已经包含其他头文件。注意:预处理指令中符号化常量名使 用的规则是把头文件名中圆点(.)换成下划线。 测试与调试提示 1. 2 用#ifdef、#define 和#endif 预处理指令防止一个程序中多次包合相同的头文件。 编程技巧 1.2 头文件的#ifdef 和#define 顸处理指令中用头文件名,井将圆点换成下划线, 1.8 控制对成员的访问 成员访问说明符 public 和 private 可以控制类数据成员和成员函数的访问。类的默认 访问模式是 private,因此类的首部和第一个标号之间的所有成员的类型都是 private。每 个标号之后,采用该标号表示的方式,直到遇到下一个标号或遇到类定义的右花括号(})。 标号 public、private 和 protedted 可以重复,但这种情况不常用,容易造成混乱。 类的 private 成员只能由类的成员函数访问,public 成虽则可以由程序中的任何函 数访问。 public 成员的主要用途是向类的客户提供类的服务(行为),这组服务形成类的 public 接口。类的客户不必关心类如何完成任务。类的 private 成员和 public 成员函数的 定义是类的客户无法访问的。 这些组件形成类的实现方法(implementation)。 软件工程视点 1.13 C++提倡程序独立于实现方法。对于独立于实现方法的代码,改变类的实现方法时, 代码不需要改变,但可能需要重新编译。 常见编程错误 1.6 18 除了由类的成员函数访问外,其他函数想访问类的 private 成员是个语法错误。 图 1.6 演示了 private 类成员只能用 public 成员函数通过 public 类接口访问。编译这 个程序时,编译器产生两个错误,表示每个语句中指定的 private 成员无法访问。图 1.6 包含 time1.h 并和图 1.5 的 time1.cpp 一起编译。 编程技巧 1.3 如果在类定义中先列出 private 成员,尽管程序默认的访问模式为 private,但最好 还是显式使用 private 标号,这样可以使程序更清晰。我们喜欢先列出 public 成员以强调 类的接口。 1 // Fig. 1.6:fig06 06.cpp 2 // Demonstrate errors resulting from attempts 3 // to access private class members. 4 #include 5 #include "time1.h" 6 7 int main() 8 { 9 Time t; 10 11 // Error: 'Time::hour' is not accessible 12 t.hour = 7; 13 14 // Error: 'Time::minute' is not accessible 15 cout << "minute =" << t.minute; 16 17 return 0; 18 } 输出结果: Compilin9 FIG06 06.CPP: Error FIG06 06.CPP 12: 'Time::hour' is not accessible Error FIG06_06.CPp 15: 'Time::minute' is not accessible 图 1.6 访问类的 private 成员的错误 编程技巧 1.4 尽管 public 和 private 标号可以重复和混合,但最好先将所有 public 成员列成一组, 然后将所有 private 成 员列成一组,这样可以使客户集中注意类的 public 接口,而不是注意类的实现方法。 软件工程视点 1.14 让类的所有数据成员保持 private。让 Public 成员函数设置 private 数据成员的值并 取得 private 数据成员的 19 值。这种结构能隐藏类的实现方法,减少错误和提高程序的可修改性。 类的客户可能是另一类的成员函数,也可能是全局函数(即文件中类 c 语言的“松 散”函数,不是任何类的成员函数)。 类成员的默认访问方式为 private。类成员的访问方式可以显式设置为 public、protected 和 private。struct 成员的默认访问方式为 public。struct 的成员的访问 方式也可以设置为 Public、protected 或 private。 较件工程视点 1.15 类设计人员用 public、protected 或 private 成员实现信息隐藏和最低权限原则。 类数据为 private 并不表示客户不能改变这个数据。客户可以通过这个类的成员函数 或友元改变这个数据,但这些函数的设计应保证数据完整性。 访问类的 private 数据应当用称为访问函数 access funotion)或访问方法(access method)的成员函数严格控制。例如,要让客户读取 private 数据的值,类可以提供一个 get 函数。要让客户修改 private 数据的值,类可以提供一个 set 函数。这种修改似乎会破坏 private 数据的专用性,但 set 成员函数可以提供数据验证功能(如范围检查),保证数值 设置正确,set 函数也可以在接口使用的数据形式与实现方法使用的数据形式之间进行换 算。get 函数不必以原始形式显示数据,该函数可以编辑数据,限制客户可以看到的数据。 软件工程视点 1.16 类设计人员不必提供每个 private 数据成员的 get 和 set 函数,只在需要时才提供数 据成员的 get 和 set 函数。 测试与调试提示 1.3 将类的数据成员指定为 private、类的成员函数指定为 public 有助于调试,因为数据 操作问题局部化在类成员函数或类的友元中。 1.9 访问函数与工具函数 并非所有成员函数都要用 public 指定为类接口的一部分。有些成员函数保持 private,作为类中 其他函数的工具函数(utility function)。 软件工程视点 1.17 成员函数分为几大类:读取和返回私有数据成员值的函数、设置私有数据成员值的函 数、实现类特性的 函数和进行各种类操作的函数(如初始化类对象、指定类对象、将类与 内部类型或其他类进行相互转换以及处理奥对象内存)。 访问函数可以读取和显示数据。访问函数的另一常见用法是测试条件的真假,这种函 数称为判定函数(predicate funchon)。任何容器类都有的 isEmpty 函数(如链表、堆栈和队 列)就是判定函 数。程序先测试 isEmpty,再从容器对象中读取下一个项目。判定函数 isFull 于测试容器类对象还有没有多余的存储空间。Time 类的判定函数包括 isAM 和 isPM。 20 图 1.7 演示了工具函数(或称为帮助函数)的使用。工具函数不是类接口的一部分,而是 private 成员函数,支持类中其他函数的操作。类的客户不能使用工具函数。 1 // Fig. 1.7: salesp.h 2 // SalesPerson Class definition 3 // Member functions defined in salesp.cpp 4 #ifndef SALESPH 5 #define SALESPH 6 ? class SalesPerson { 8 public: 9 SalesPerson(); // constructor 10 void getSalesFromUser(); // get sales figures from keyboard 11 void setSales( int, double ); // User supplies one month's 12 // sales figures. 13 void printAnnualSales(); 14 15 private: 16 double totalAnnualSales(); // utility function 17 double sales[ 12 ]; // 12 monthly sales figures 18 }; 19 20 #endif 21 // Fig. 1.7: salesp.cpp 22 // Member functions for class SalesPerson 23 #include 24 #include 26 27 // Constructor function initializes array 28 29 { 30 for (int i = 0; i < 12; i++ ) 31 sales[ i ] = 0.0; 32 } 34 // Function to get 12 sales figures from tha user 35 // at the keyboard 37 { 38 double salesFigure; 40 for (int i = 0; i < 12; i++ ) { 41 cout << "Enter sales amountfor month" << i + 1 << ": "; 43 cin >> salesFigure; 44 setSales( i, salesFigure ); 45 } 21 46 } 47 48 // Function to set one of the 12 monthly sales figures. 49 Note that the month value must be from 0 to 11 50 void SalesPerson::setSales( int month, double amount ) 51 { 52 if ( month >= 0 && month < 12 && amount > 0 ) 53 sales[ month ] = amount; 54 else 55 cout << "Inalid month or sales figure" << endl; 56 } 57 58 // Print the total annual sales 59 void SalesPerson::printAnnualSales() 6O { 61 cout << setprecision( 2 ) 62 << setiosflags( ios::fixed I ios::showpoint ) 63 << "\nThe total annual sales are: $" 64 << totalAnnualSales() << endl; 65 } 67 // Private utility function to total annual sales 68 double SalesPerson::totalAnnualSales() 69 { 70 double total = 0.0; 71 72 for (int i = 0; i < 12; i++ ) 73 total += sales[ i ]; 74 75 return total; 76 } 77 // Fig.67:fig06_07.cpp 78 // Demonstrating a utility function 79 // Compile with salesp.cpp 80 #include "salesp.h" 81 82 int main() 83 { 84 SalesPerson s; // create SalesPerson object s 85 86 s.getSalesFromUser(); // note simple sequential code 87 s.printAnnualSales(); // no control structures in main 88 return 0; 89 } 22 输出结果: Enter sales amount for month 1:5314.76 Enter sales amount for month 2:4292.38 Enter sales amount for month 3:4589.83 Enter sales amount for month 4:5534.03 Enter sales amount for month 5:4376.34 Enter sales amount for month 6:5698.45 Enter sales amount for month 7:4439.22 Enter sales amount for month 8:5893.57 Enter sales amount for month 9:4909.67 Enter sales amount fez month 10:5123.45 Enter sales amount for month 11:2024.97 Enter sales amount for month 12:5923.92 The total annual sales are: $60120.58 图 1.7 使用工具函数 SalesPerson 类中的表示 12 个月销售数据的数组用构造函数初始化为 0,并用 setSales 函数设置为用户提供的值。public 成员函数 printAnnualSales 打印最近 11 个月 的总销售额。工具函数,TotalAnnualSales 为 PrintAnnualSales 计算 12 个月的总销售额。 成员函数 printAnnudSales 将销售数据转换为美元金额格式。 注意 main 中只有一个简单的成员函数调用,没有任何控制结构。 软件工程视点 1.18 面向对象编程的一个现象是定义类之后,生成和操作这个类的对象通常只要一个简 单的成员函数调用, 没有任何或只有少量控制结构。相反,类成员函数的实现则通常需要控制结构。 1.10 初始化类对象:构造函数 生成类对象时,其成员可以用类的构造函数初始化。构造函数是与类同名的成员函数。 程序员提供的构造函数在每次生成类对象(实例化)时自动调用。构造函数可以重载.提供 初始化类对象的不同方法。数据成员应在类的构造函数中初始化或在生成对象之后设置其 数值。 常见编程错误 1. 7 类的数据成员只能在类定义中初始化。 常见编程错误 1.8 试图声明构造函数的返回类型和返回植是个语法错误。 23 编程技巧 1.5 适当时候(通常都是)应提供一十构速函数,保证每个对象正确地初始化为有意义的 值。特别是指针数 据类型应初始化为合法指针值或 0。 测试与调试提示 1.4 每个修改对象的 private 数据成员的成员函数(和友元)应确保数据保持一致状态。 声明类对象时,可以在括号中提供初始化值,放在对象名后面和分号前面。这些初始 化值作为 参数传递给类的构造函数。稍后会举几个构造函数调用(constructor call)的例子(注意: 尽管程序 员不显式调用构造函数,但程序员仍然可以提供数据,作为参数传递给构造函数)。 1.11 在构造函数中使用默认参数 图 1.1time1.cpp 中的构造函数将 hour、minute 和 second 初始化为 0(即军用时间午 夜 11 时)。 构造函数可以包含默认参数。图 1.8 重新定义 Time 的构造函数,该函数中每个变量的默认 参数为 0。通过提供构造函数默认参数,即使在构造函数调用中不提供数值,对象也能利 用默认参数初始化为一致状态。程序员提供所有参数默认值(或显式不要求参数)的构造函 数也称为默认构造函数(default constnlctor),即可以不用参数而调用的构造函数。一个 类只能有一个默认构造函数。 1 // Fig. 1.8: time2.h 2 // Declaration of the Time class. 3 // Member functions are defined in time2.cpp 4 5 // preprocessor directives that 6 // prevent multiple inclusions of header file 7 #ifndef TIME2_H 8 #define TIME2_H 9 10 // Time abstract data type definition 11 class Time { 12 public: 13 Time( int = 0, int = 0, int = 0 ); // default constructor 14 void setTime( int, int, int ); // set hour, minute, second 15 void printMilitary(); // print military time format 16 void printStandard(); // print standard time format 24 17 private: 18 int hour; // 0 - 23 19 int minute; // 0 59 20 int second; // 0 - 59 21 }; 22 23 #endif 24 // Fig. 1.8: time2.cpp 25 // Member function definitions for Time class. 26 #include 27 #include "time2.h" 28 29 // Time constructor initializes each data member to zero. 30 // Ensures all Time objects start in a consistent state. 31 Time::Time(int hr,int min,int sec) 32 { setTime( hr, min, sec ); } 33 34 // Set a new Time value using military time. Perform validity 35 // checks on the data values. Set invalid values to zero. 36 void Time::setTime(int h,int m,int s) 37 { 38 hour = ( h >= 0 && h < 24 ) ? h : 0; 39 minute = ( m >0 && m < 60 )? m : 0; 40 second = ( s >= 0 && s < 60 ) ? s : 0; 41 } 42 43 // Print Time in military format 44 void Time::printMilitary() 45 46 cout << ( hour < 10 ? "O" : "" ) << hour << ":" 47 << ( minute < 10 ? "0" : "" ) << minute; 48 } 49 50 // Print Time in standard format 51 void Time::printStandard() 53 cout << ( ( hour == 0 || hour == 12 ) ? 12 : hour % 12 ) 54 << ":" <<( minute < 10 ? "0" : "" ) << minute 55 << ":" << ( second < 10 ? "0" : "" ) << second 56 << ( hour < 12 ? "AM" : "PM" ); 57 } 58 // Fig. 1.8: fig06_08.cpp 59 // Demonstrating a default constructor 60 // function for class Time. 61 #include 25 62 #include "time2.h" 63 64 int main() 65 { 66 Time t1, // all arguments defaulted 67 t2(2), // minute and second defaulted 68 t3(21, 34), // second defaulted 69 t4(12, 25, 42), // all values specified 70 t5(27, 74, 99); // all bad values specified 71 72 cout << "Constructed with:\n" 73 << "all arguments defaulted:\n "; 74 t1.printMilitary(); 75 cout << "\n "; 76 t1.printStandard(); 77 78 cout << "\nhour specified; minute and second defaulted:" 79 << "\n "; 80 t2.printMilitary{); 81 cout << "\n "; 82 t2.printStandard(); 83 84 cout << "\nhour and minute specified; second defaulted:" 85 << "\n "; 86 t3.printMilitary(); 87 cout << "\n "; 88 t3.printStandard(); 89 90 cout << "\nhour, minute, and second specified:" 91 << "\n "; 92 t4.printMilitaryO; 93 cout << "\n "; 94 t4.printStandard(); 95 96 cout << "\nall invalid values specified:" 97 << "\n "; 98 t5.printMilitary(); 99 cout << "\n "; 100 t5.printStandardO; 101 cout << endl; 102 103 return 0; 104 } 26 输出结果: Constructed with: all arguments defaulted 00:00 12:00:00 AM hour specified; minute and second defaulted: 02:00 2:O0:0O AM hour and minute specified; second defaulted: 21:34 9:34:00 PM hour, minute, and second specified: 12:25 12:25:42 PM all invalid values specified: 00:00 12:00:00 AM 图 1.8 构造函数使用默认参数 在这个程序中,构造函数调用成员函数 setTime,将数值传人构造函数(或用默认值) 保证 hour 值取 0 到 13、minute 和 second 值取 0 到 59。如果数值超界,则 setTime 将其设 置为 0(使数据成员保证一致状态)。 注意,Time 构造函数也可以写成包含与 setTime 成员函数相同的语句。这样可能会使 程序更有效,因为不必另外再调用 setTime 函数。但让 Time 构造函数和 setTime 成员函数 使用相同代码会使程序维护更加困难。如果 setTime 成员函数的实现方法改变,则 Time 构造函数的实现方法也要相应 改变。Time 构造函数直接调用 setTime 时,setTime 成员 函数的实现方法只要改变一次即可,这样就 可以在改变实现方法时减少错误。另外,显 式声明内联的构造函数或在类定义中定义构造函数也可以提高 Time 构造函数的性能(后 者隐含内联函数定义)。 软件工程视点 1.19 如果类的成员函数已经提供类的构造函数(或其他成员函数)所需功能的所有部分, 则从构造函数(或其他成员函数)调用这个成员函数。这样可以简化代码维护和减少修改代 码实现方法时的出错机会。因此就形成了一个一般原则:避免重复代码。 编程技巧 1.6 只在头文件内的类定义的函数原型中声明默认函数参数值。 常见编程错误 1.9 在头文件和成员函数定义中指定同一成员函数的默认初始化值。 图 1.8 的程序初始化五个 Time 对象:一个将三个参数指定为默认值,一个指定一个 参数,一个指定两个参数.一个指定三个参数,一个指定三个无效参数。每个对象数据成 27 员均显示实例化和初始化之后的内容。 如果类不定义构造函数,则编译器生成默认构造函数。这种构造函数不进行任何初始 化,因此生成对象时,不能保证处于一致状态。 软件工程视点 1.20 类不一定有默认构造函数。 1.12 使用析构函数 析构函数是类的特殊成员函数。类的析构函数名是类名前面加上代字符(~)这种命名 规则很直观,因为本章稍后将会介绍,代字运算符是按位取反符,从这个意义上,析构 函数是构造函数的反函数。 类的析构函数在删除对象时调用,即程序执行离开初始化类对象的范围时。析构函数 本身并不实际删除对象,而是进行系统放弃对象内存之前的清理工作,使内存可以复用 于保存新对象。 析构函数不接受参数也不返回数值。类只可能有一个析构函数,不能进行析构函数重 载。常见编程错误 1.10 向析构函数传递参数、指定析构函数的返回值类型(即使指定 void)、从析构函数返回 数值或重载析构函数都是语法错误。 注意,前面介绍的类都没有提供析构函数。下面要介绍几个使用析构函数的例子。第 8 章将介绍析构函数适用于动态分配内存的对象类(例如数组和字符串)。第 7 章将介绍如 何动态分配内存和释放内存。 软件工程视点 1.21 稍后会介招,构造函数和析构函数在 C++和面向对象编程中相当重要,不是这里的 介绍所能说清楚的。 1.13 何时调用构造函数与析构函数 构造函数与析构函数是自动调用的。这些函数的调用顺序取决于执行过程进入和离开 实例化对象范围的顺序。一般来说,析构函数的调用顺序与构造函数相反。但图 1.9 将介绍 对象存储类可以改变析构函数的调用顺序。 全局范围中定义的对象的构造函数在文件中的任何其他函数(包括 main)执行之前调用 (但不同文件之间全局对象构造函数的执行顺序是不确定的)。当 main 终止或调用 exit 函 数时调用相应的析构函数。 当程序执行到对象定义时,调用自动局部对象的构造函数。该对象的析构函数在对象 离开范围时调用(即离开定义对象的块时)。自动对象的构造函数与析构函数在每次对象进 人和离开范围时调用。 static 局部对象的构造函数只在程序执行首次到达对象定义时调用一次,对应的析 构函数在 main 终止或调用 exit 函数时调用。 28 图 1.9 的程序演示了 CreateAndDestroy 类型的对象在几种范围中调用构造函数与析 构函数的顺序。程序在全局范围中定义 first,其构造函数在程序开始执行时调用,其析 构函数在程序终止时删除所有其他对象之后调用。 1 // Fig. 6,9: create,h 2 // Definition of class CreateAndDestroy. 3 // Member functions defined in create.cpp, 4 #ifndef CREATE_H 5 #define CREATE_H 6 7 class CreateAndDestroy { 8 public: 9 CreateAndDestroy( int ); // constructor 10 ~CreateAndDestroy(); // destructor 11 private: 12 int data; 13 }; 14 15 #endif 16 // Fig, 1.9: create.cpp 17 // Member function definitions for class CreateAndDestroy 18 #include 19 #include "create.h" 20 21 CreateAndDestroy::CreateAndDestroy( int value ) 22 { 23 data = value; 24 cout << "Object "<< data <<" constructor"; 29 } 26 27 CreateAndDestroy::~CreateAndDestroyO 28 { cout << "Object "<< data <<" destructor "<< endl; } 29 // Fig, 1.9: fig0609.cpp 30 // Demonstrating the order in which constructors and 31 // destructors are called. 32 #include 39 #include "create.h" 34 35 void create( void ); // prototype 36 37 CreateAndDestroy first( 1 ); // global object 38 39 int main() 4O { 29 41 cout <<" (global created before main)" << endl; 42 43 CreateAndDestroy second( 2 ); 44 cout <<" (local automatic in main)" << endl; 45 46 static CreateAndDestroy third( 3 ); // local object 47 cout <<" (local static in main)" << endl; 48 49 create(); // call function to create objects 50 51 CreateAndDestroy fourth( 4 ); // local object 52 cout <<" (local automatic in main)" << endl; 53 return 0; 54 } 55 56 // Function to create objects 57 void create( void ) 58 { 59 CreateAndDestroy fifth( 5 ); 60 cout <<" (local automatic in create)" << endl; 61 62 static CreateAndDestroy sixth( 6 ); 63 cout <<" (local static in create)" << endl; 64 65 CreateAndDestroy seventh( 7 ); 66 cout <<" (local automatic in create)" << endl; 67 } 输出结果: Object 1 constructor (global creasted before main) Object 2 constructor (local automatic in main) Object 3 constructor (local static in main) Object 5 constructor (local automatic in create) Object 6 constructor (local static in create) Object 7 constructor (local automatic in create) Object 7 destructor Object 5 destructor Object 4 constructor (local automatic in main) Object 4 destructor Object 2 destructor Object 6 destructor Object 3 destructor Object 1 destructor 图 1.9 调用构造函数与析构函数的顺序 30 函数 main 声明三个对象。对象 second 和 fourth 是局部自动对象,对象 third 是 static 局部对象。这些对象的构造函数在程序执行到对象定义时调用。对象 fourth 和 second 的析构函数在到达 main 结尾时依次调用。由于对象 third 是 static 局部对象,因 此到程序结束时才退出,在程序终止时删除所有其他对象之后和调用 first 的析构函数 之前调用对象 third 的析构函数。 函数 create 声明三个对象。对象 fifth 和 seventh 是局部自动对象,对象 sixth 是 static 局部对象。对象 seventh 和 fifth 的析构函数在到达删 k 结尾时依次调用。由于对象 sixth 是 static 局部对象,因此到程序结束时才退出。sixth 的析构函数在程序终止时删 除所有其他对象之后和调用 third 和 first 的析构函数之前调用。 1.14 使用数据成员和成员函数 员函数调整客户的银行借贷(例如 BanLAccount 类的 private 数据成员)。 类通常提供 public 成员函数,让类的客户设置(写入)或读取(取得)private 数据成员 的值。这些函数通常称为 get 和 set。更具体地说,设置数据成员 interestRate 的成员函数 通常称为 setInterestRate,读取数据成员 IntersetRate 的值通常称为 getInterestRate。 读取函数也称为“查询”函数。 提供 get 和 set 函数与指定数据成员为 public 同样重要,这是 C++语言在软件工程 中的另一优势。如果数据成员为 public,则程序中的任何函数可以随意读取和写入这个数 据成员。如果数据成员为 private.则 public get 函数可以让其他函数读取数据,而且数 据的显示和格式化也可以用 get 函数控制。public set 函数通常用于检查数据成员的修改, 保证新值是适当的数据项目。例如,如果想把一个月的日期号数设置为 37 会被禁止,将 人的身高设置为负值也会被禁止,将数字量设置为字母值也会被拒绝,将一个人的成绩 设置为 185 分(取百分制时)同样也会被拒绝等等。 软件工程视点 1.22 指定 private 数据成员并通过 public 成员函数控制这些数据成员的访问(特别是写入 访问)可以保证数据的完整性。 测试与调试提示 1.5 指定 private 数据成员并不能自动保证数据完整性,程序员还要提供验证检查。但 C+ +提供了让程序员方便地设计更好的程序的框架。 编程技巧 1.7 设置 private 数据值的成员函数应验证所要新值是否正确,如果不正确,则 set 数应 将 Privte 数据成员设置为相应的一致状态。 试图要对数据成员指定无效值时,应当提醒类客户。类的 set 函数常写成返回一个值, 表示试图对数据成员指定无效值。这样就使类的客户可以测试 set 函数的返回值,确定其 操作的对象是否为有效对象,并在对象无效时采取相应操作。 图 1.1O 将 Time 类扩展成包括 private 数据成员 hour、minute 和 second 的 get 和 set 31 函数。set 函数严格控制数据成员的设置。如果想把数据成员设置为无效值,则会把数据成 员设置为 0(从而使数据成员保持一致状态)。每个 get 函数只是返回相应数据成员的值。程 序首先用 set 函数设置 Time 对象 t 的 private 数据成员为有效值,接着用 get 函数读取这 个值以便输出。然后 set 函数要将 hour 和 second 成员设置为无效值并将 minute 成员设置 为有效值,并用 get 函数读取这个值以便输出。输出表明,无效值使得数据成员设置为 0。 最后,程序将时间设置为 11:58:00 并用函数 incrementMinutes 增加 3 分钟。函数 incrementMinutes 是个非成员函数,它调用 get 和 set 成员函数增加 minute 成员的值。尽 管这样的方法实现了所需的功能,但是多次函数调用降低了程序的性能。下一章将介绍用 友元函数消除多次函数调用的性能负担。 常见编程错误 1.11 构造函数可以调用类的其他成员函数,如 set 和 get 函数,但由于构造函数初始化对 象,因此数据成员可能还处于不一致状态。数据成员在初始化之前使用可能造成逻辑错误。 1 // Fig. 1.10: time3.h 2 // Declaration of the Time class. 4 5 // preprocessor directives that 6 // prevent multiple inclusions of header file 7 #ifndef TIME3_H 8 #define TIME3_H 9 10 class Time { 11 public: 12 Time( int = 0, int = 0, int= 0 ); // constructor 13 14 // set functions 15 void setTime( int, int, int ); // set hour, minute, se 16 void setHour( iht ); // set hour 17 void setMinute( int ); // set minute 18 void setSecond( int ); // set second 19 20 // get functions 21 int getHourO; // return hour 22 int getMinute(); // return minute 23 int getSecond(); // return second 24 25 void printMilitary(); // output military time 26 void printStandard(); // output standard time 27 28 private: 29 int hour; // 0 - 23 30 int minute; // 0 - 59 31 int second; // 0 - 59 32 } 32 33 34 #endif 35 // Fig. 1.10: time3.cpp 36 // Member function defintions for Time class 37 #include "time3.h" 38 #include 39 40 // Constructor function to initialize private data. 42 // Default values are 0 (see class definition). 43 Time::Time( int hr, int min, int sec ) 44 { setTime( hr, min, sec ); } 45 46 // Set the values of hour, minute, and second. 47 void Time::setTime(int h,int m,int s) 48 { 49 setHour( h ); 50 setMinute( m ); 51 setSecond( s ); 52 } 53 54 // Set the hour value 55 void Time::setHour(int h) 56 {hour = (h>0 && h <24 )? h: 0;} 57 58 // Set the minute value 59 void Time::setMinute( int m ) 60 { minute = ( m >= 0 && m 60 ) ? m : 0; } 61 62 // Set the second value 63 void Time::setSecond( int s 64 { second = ( s >= 0 && s < 60 ) ? s : 0; } 65 66 // Get the hour value 67 int Time::getHour() { return hour;} 68 69 // Get the minute value 70 int Time::getMinute() { return minute; } 71 72 // Get the second value 73 int Time::getSecond() { return second; } 74 75 // Print time is military format 76 void Time::printMilitary() 77 { 33 78 cout << ( hour < 10 ? "0" : "" ) << hour << ":" 79 << ( minute < 10 ? "0" : "" ) << minute; 8O } 81 82 // Print time in standard format 83 void Time::printStandard{) 84 { 85 cout << ( { hour == 0 II hour == 12 ) ? 12 : hour % 12 ) 86 << ":" << ( minute < 10 ? "0" : "" ) << minute 87 << ":" << ( second < 10 ? "0" : "" ) << second 88 << ( hour < 12 ? "AM" : "PM" ); 89 } 90 // Fig. 1.10: fig06_lO.cpp 92 #include 91 // Demonstrating the Time class ser and get functions 92 #include 93 #include "time3.h" 94 95 void incrementMinutes( Time &, const iht ); 96 97 int main() 98 { 99 Time t; 100 101 t.setHour{ 17 ); 102 t.setMinute( 34 ); 103 t.setSecond( 25 ); 104 105 cout << "Result of setting all valid values:\n; 106 << our: << t.getHour() 107 << " Minute: " << t.getMinute() 108 <<" Second: "<< t.getSecond(); 109 110 t.setHour( 234 ); // invalid hour set to 0 111 t.setMinute( 43 ); 112 t.setSecond( 6373 ); // invalid second set to 0 113 114 cout << "\n\nResult of attempting to set invalid hour and" 115 << "second:\m Hour: "<< t.getHour() 116 <<" Minute: "<< t.getMinute() 117 <<" Second: "<< t.getSecond() << "\n\n"; 118 119 t.setTime( 11, 58, 0 ); 120 incrementMinutes( t, 3 ); 34 121 122 return 0; 123 } 124 125 void incrementMinutes(Time &tt, const int count) 126 { 127 cout << "Incrementing minute" << count 128 << "times:\nStart time: "; 129 tt.priatStandard(); 130 131 for (int i = 0; i < count; i++ ) { 132 tt.setMinute( (tt.getMinute() + 1 ) % 60); 133 134 if (tt.getMinute() == 0 ) 135 tt.setHour( ( tt.getHour() + 1 ) % 24); 136 137 cout << "\nminute + 1: "; 138 tt.printStandard(); 139 } 140 141 cout << endl; 142 } 输出结果: Result of setting all valid values: Hour: 17 Minute: 34 Second: 25 Result of attempting to set inv}id hour and second: Hour: 0 Minute: 43 Second: 0 Incrementing minute 3 times: Start time: 11:58:00 AM minute + 1; 11:59:00 AM mioute + 1:12:00:00 PM minute + 1:12:01:00 PM 图 1.10 使用 set 和 get 函数 从软件工程角度看,使用 set 函数非常重要,因为它们可以进行有效性检查。set 和 get 函数还有其他重要的软件工程优势。 软件工程视点 1.23 通过 set 和 get 函数访问 private 数据不仅能防止数据成员接受无效值,而且还使类 的客户 J 需要考虑数据成员的表达方式。这样,如果数据表达方式因故改变(通常是为了 减少所需存储量或提高性能),只要成员函数提 供的接口不变,那么只需改变成员函数 而不必改变客户,但客户可能需要重新编译。 35 1.15 微妙的陷阱:返回对 private 数据成员的引用 对象的引用是对象名的别名,因此可以放在赋值浯句左边,在这种情况中,引用可 以成为可接收赋值的左值。要使用这种功能,就要让类的 public 成员函数返回对该类 private 数据成员的非 const 引用。 图 1.11 用简化的 Time 类演示如何返回 private 数据成员的引用。调用 badSetHour 函数 返回的引用作为 private 数据成员 hour 的别名。函数调用可以按任何使用 private 数据成 员的方式使用,包括作为赋值语句的左值。 编程技巧 1.8 不要让类的 public 成员函数返回对该类 private 数据成员的非 const 引用(或指针), 返回这种引用会破坏类的封装。 1 // Fig. 1.11: time4.h 2 // Declaration of the Time class. 3 // Member functions defined in time4.cpp 4 5 // preprocessor directives that 6 // prevent multiple inclusions of header file 7 #ifndef TIME4_H 8 #define TIME4_H 9 10 class Time { 11 public 12 Time( int = 0, int = 0, int = 0 ); 13 void setTime( int, int, int ); 14 int getHour(); 15 int &badSetHour( int ); // DANGEROUS reference return 16 private: 17 int hour; 18 int minute; 19 int second; 20 }; 21 22 #endif 23 // Fig. 1.11: time4.cpp 24 // Member function definitions for Time class. 25 #include "time4.h" 26 #include 27 28 // Constructor function to initialize private data. 29 // Calls member function setTime to set variables. 36 30 // Default values are 0 (see class definition). 31 Time::Time( int hr, int min, int sec ) 32 { setTime( hr, min, sec ); } 33 34 // Set the values of hour, minute, and second. 35 void Time::setTime( int h, int m, int s ) 36{ 37 hour = ( h >= o && h < 24 ) ? h : 0; 38 minute ( m >= 0 && m < 60 ) ? m : 0; 39 second = ( s >= 0 && s < 60 ) ? s : 0; 40 } 41 42 // Get the hour value 43 int Time::getHour() { return hour; ] 44 45 // POOR PROGRAMMING PRACTICE: 46 // Returning a reference to a private data member. 47 int &Time::badSetHour( int hh ) 48 { 49 hour = ( hh >= 0 && hh < 24 ) ? hh : 0; 50 51 return hour; // DANGEROUS reference return 52 } 53 // Fig. 1.11: fig06_11.cpp 54 // Demonstrating a public member function that 55 // returns a reference to a private data member. 56 // Time class has been triced for this example. 57 #include 58 #include "time4.h" 59 60 int main() 61 { 62 Time t; 63 int &hourRef = t.badSetHour( 20 ); 64 65 cout << "Hour before modification: "<< hourRef; 66 hourRef = 30; // modification with invalid value 67 cout << "\nHour after modification: "<< t.getHour(); 68 69 // Dangerous: Function call that returns 70 // a reference can be used as an lvalue! 71 t.badSetHour(12) = 74; 72 cout << "\n\n*******************************************\n" 73 << "POOR PROGRkMMING PRACTICE!!!!!!!!\n" 37 74 << "badSetHour as an lvalue, Hour:" 75 << t.getHour() 76 << "\n*************************************** << endl; 77 78 return 0; 79 } 输出结果: Hour before modification: 20 Hour after modification: 30 ********************************* POOR PROGRAMMING PRACTICE!!!!!!!! badSetHour as an lvalue, Hour: 74 ********************************* 图 1.11 返回对 private 数据成员引用 程序首先声明 Time 对象 t 和引用 hourRef(把调用 t.badSetHour(20)返回的引用赋给 hourRef)。程序显示别名 hourRef 的值。然后用这个别名设置 hour 的值为 30(无效值)并再 次显示该值。最后,用函数调用本身作为左值并赋值 74(另一无效值),再次显示该值。 1.16 通过默认的成员复制进行赋值 赋值运算符(=)可以将一个对象赋给另一个相同类型的对象。这种赋值默认通过成员复制 (memberwisecopy)进行,对象的每个成虽一一复制给另一对象的同一成员(如图 1.L1) 对象可以作为函数参数传递并从函数返回。这种传递和返回默认按值调用进行,即传 递和返回对象的副本。 性能提示 1.4 按值调用传递对象的安全性较好,因为被调函数无法访问原始对象.但如果要复制 大对象,则按值调用可能使性能下降。对象按引用调用传递时可以按指针或对象引用传递。 按引用调用有性能优势,但安全性较差,因为被调函数可以访问原始对象。按常量引用调 用则既安全,又有性能优势。 1 // Fig. 1.12: fig06_12.cpp 2 // Demonstrating that class objects can be assigned 3 // to each other using default memberwise copy 4 #include 5 6 // Simple Date class 7 class Date ( 38 8 public: 9 Date( int = 1, int = 1, int = 1990 ); // default constructor 10 void print(); 11 private: 12 int month; 13 int day; 14 int year; 15 ); 16 17 // Simple Date constructor with no range checking 18 Date::Date( int m, int d, int y ) 19 { 20 month = m; 21 day = d; 22 year = y; 23 } 24 25 // Print the Date in the form mm-dd-yyyy 26 void Date::print() 27 { cout << month << '-' << day << '-' << year; } 20 29 int main() 3O { 31 Date date1( 7, 4, 1993 ), date2; // d2 defaults to 1/1/90 32 33 cout << "date1 = "; 34 datel.print(); 35 cout << "\ndate2 = "; 36 date2.print(); 37 38 date2 = date1; // assignment by default memberwise copy 39 cout << "\n\nAfter default memberwise copy, date2 = "; 40 date2.print(); 41 cout << endl; 42 43 return 0; 44 } 输出结果: aatel = 7-4-1993 aate2 = 1-1-1990 After default memberwise copy,date2 = 7-4-1993 39 图 1.12 通过默认的成员复制将对象赋给另一相同类型的对象 1.17 软件复用性 编写面向对象程序的目的是要实现有用的类。类可以通过大量机会获取和分类,让广大 程序员使用。许多类库(class library)已经存在,许多类库还在不断开发。人们正在不断 推广应用这些类库。软件越来越趋向于从现有的,定义良好、经过认真测试、文档齐全、可 移植的各种组件进行构造。这种软件复用性加速了强大的、高质量软件的开发速度。通过复 用组件实现快速应用程序开发(rapid applications development,RAD)已经成为一个重 要领域。 但还要先解决一些重要问题才能完全实现软件复用性。我们需要有分类机制、许可证 机制,用保护机制来保证类的主副本不被搞乱,用描述机制让新系统设计人员能够确定 现有对象是否满足其需求,用浏览机制确定有什么类及其与软件开发人员要求的接近程 度等等。许多有趣的研究和开发问题需要解决。人们正在积极解决这些问题.因为这种方 案的潜在价值是巨大的。 1.18 有关对象的思考:编写电梯模拟程序的 前面介绍了电梯模拟程序的面向对象设计。现在就可以开始编写电梯模拟程序的类。 电梯实验室任务 4 1.编写相应的 c++类定义。对每个类,应包括头文件和成员函数定义的源文件。 2.编写一个驱动程序,测试每个类,并运行完整的电梯模拟程序。注意,可能要等 学完第 7 章之后才能完成电梯模拟程序的工作版本,因此要有耐心,先利用第 6 章的知 识实现电梯模拟程序。第 7 章将介绍复合,即生成以另一个类为成员的类,这个方法可以 表示电梯中的按钮、电钤和门对象为电梯的成员。第 7 章还要介绍如何用 New 和 delete 动 态生成和删除对象,帮助生成新人的对象和删除离开的人的对象(在人来和人走时)。 3,在电梯模拟程序的第一个版本中,只设计简单的面向文本输出,对发生的每个重 要事件显示一个消息。程序中的消息可能包括下列字符串:“Person 1 arrives on Floor 1”、”Person Presses Button on Floor 1、“Elevator arrives on Floor 1”、“Person 1 enters Elevator”等等。注意,建议 将消息中表示对象的单词大写。 也可以在学完第 7 章之后再做这个工作。 4.有的学生还可以用动画图形输出,在屏幕上显示电梯上下移动。 小 结 ●结构是用其他类型的元素建立的聚合数据类型。 ●结构定义用关键字 struct 引入,结构体放在花括号({ })中,结构定义以分号结尾。 40 ●结构标志声明结构类型的变量。 ●结构定义并没有在内存中保留任何空间,而是生成新的数据类型,用于声明变量。 ●使用成员访问运算符(包括圆点运算符和箭头运算符)访问结构成员或类成员。圆点 运算符 通过对象的变量名或对象的引用访问结构和类成员。箭头运算符通过对象指针 访问结构和类成员。 ●通过结构生成新数据类型有一定的缺点,可能出现未初始化的数据。如果 struct 的实现方法改变,则所有使用这个 struct 的程序都要改变。没有保护机制保证数据的正 确和保持数据的一致状态。 ●类使程序员可以构造有属性和行为的对象。C++中用关键字 class 或 struct 定义类 的类型,通常用关键字 class, ●可以用类名声明该类的对象。 ●类定义以关键字 class 开始。类定义体放在左右花括号({ })之间。类定义用分号终止。 ●任何可以访问类的对象的函数可以访问任何在 public:后面声明的数据成员和成 员函数。 ●任何在 private:后面声明的数据成员和成员函数只能由该类的成员函数和友元访 问。 ●成员访问说明符总是加上冒号,可以在类定义中按任何顺序多次出现。 ●不能在类的外部访问私有数据。 ●类的实现方法向客户隐藏。 ●构造函数是个特殊成员函数,初始化类对象的数据成员。类的构造函数在生成这个 类的对象时自动调用。 ●与类同名而前面加上代字符(~)的函数称为类的析构函数。 ●类的 public 成员函数集称为类的接口或 public 接口。 ●在类定义以外定义成员函数时,函数名前面要加上类名和二元作用域运算符(::)。 ●尽管类定义中声明的成员函数可以在类定义之外定义,但成员函数仍然在类范围 中。 ●如果在类定义中定义成员函数,则该成虽函数自动成为内联函数,但编译器有权 决定其是否作为内联函数。 ●成员函数调用比过程式编程中的传统函数调用更简练,因为成员函数使用的大多 数数据可以直接在对象中访问。 ●在类范围中,类成员可以简单地用名字引用。在类范围外,类成员是通过对象名、 对象引用和对象指针来引用的。 ●成员选择运算符.和->用来访问类成员。 ●良好软件工程的一个基本原则是将接口与实现方法分离。 ●类定义通常放在头文件中,类的成员函数定义放在同一基本名字的源代码文件中。 ●类的默认访问模式是 private,因此在类名和第一个说明符(例如 public:)之间的 所有成员都是 private 类型。 ●public 成员的主要用途是向类的客户提供类的服务。 ●访问类的 private 数据应当用称为访问函数的成员函数进行控制。如果类允许客户 读取 Private 数据的值,可以提供一个 get 函数;如果类允许客户修改 private 数据的值, 可以提供一个 set 函数。 ●通常将类的数据成员指定为 private,将类的成员函数指定为 public,可以有助于 41 调试,因为数据操作问题局部化在类成员函数或类的友元中。有些成员函数保持 private,是供类中其他函数使用的工具函数。 ●类的数据成员不能在类定义中初始化,应在类的构造函数中初始化或在生成对象 之后设置其数值。 ●可以重载构造函数。 ●类对象初始化之后,操作该对象的成员函数应保证对象处于稳定状态。 ●声明类对象时可以提供初始化值,这些初始化值作为参数传递给类的构造函数。 ●构造函数可以指定默认参数。 ●如果类不定义构造函数,则编译器生成默认构造函数。这种构造函数不进行任伺初 始化,因此生成对象时,不能保证处于一致状态。 ●自动对象的析构函数在对象离开范围时调用,析构函数本身并不删除对象,而是 进行系统放弃对象内存之前的清理工作,使内存可以复用于保存新对象。 ●析构函数不接受参数也不返回数值。类只可能有一个析构函数(不能进行析构函数 重载)。 ●赋值运算符(=)可以将一个对象赋给另一个同类型的对象,这种赋值方式一般通过 默认的成员复制来完成。成员复制不适用于所有的类。 第二章 运算符重载 教学目标 ●了解如何重新定义(重载)运算符以处理新类型 ●了解如何将一个类的对象转换为另一个类的对象 ●了解重载运算符的时机 ●学习几个使用运算符重载的例子 ●生成 Array、String 和 Date 类 2.1 简介 对类的对象(即抽象数据类型的实例)的操作是通过向对象发送消息完成的(即调用成员 函数的形式)。对某些类(特别是数学类)来说,这种调用方式是繁琐的,而用 C++中的丰富 的内部运算符集来指定对对象的操作要更好。本章要介绍怎样把 C++中的运算符和类的对 象结合在一起使用,这个过程称为运算符重载。扩展 C++使它具有这些新的功能是理所当 然的。 运算符<<在 C++中有多种用途,既可以用作流插入运算符又可以用作左移位运算符, 这是运算符重载的一个范例。同样,运算符>>也是 C++中的一个重载运算符,它既可以用 作流读取运算符,也可以用作右移位运算符。这两个运算符都是在 C++类库中重载的。C++ 语言本身也重载了运算符+和-,这两个运算符在整数算术运算、浮点数算术运算和指针算 术运算等上下文中执行的操作是不同的。 为了使运算符在不同的上下文中具有不同的含义,C++允许程序员重载大多数运算 42 符。编译器根据运算符的使用方式产生合适的代码。某些运算符(特别是赋值运算符以及+ 和-等等的各种算术运算符)经常要被重载。虽然重载运算符所能够实现的任务也能够用明 确的函数调用完成,但是使用重载运算符能够使程序更易于阅读。 本章要讨论使用运算符重载的时机以及怎样重载运算符,还要介绍使用重载运算符 的许多完整程序。 2.2 运算符重载的基础 C++程序设计是对类型敏感的,并且程序设计的重点也是放在类型上。程序员可使用 内部的类型,也可以定义新的类型。内部的类型可以和 C++中丰富的运算符集一起使用。 运算符为程序虽提供了操作内部类型对象的简洁的表示方法。 程序员也可以把运算符和用户自定义的类型一起使用。尽管 C++不允许建立新的运算 符,但是允许重载现有的运算符,使它在用于类的对象时具有新类型的含义,这是 C++ 最强大的特点之一。 软件工程视点 2. 1 运算符重载提供了 C++的可扩展性,这也是 C++最吸引人的属性之一。 编程技巧 2. 1 在完成同样的操作的情况下,如果运算符重载能够比用明确的函数调用使程序更清 晰,则应该使用运算 符重载。 编程技巧 2.2 不要过度地或不合理地使用运算特重载,因为这样会使程序语义不清且难以阅读。 虽然运算符重载听起来好像是 C++的外部能力,但是多数程序员都不知不觉地使用 过重载的运算符。例如,加法运算符(+)对整数、单精度数和双精度数的操作是大不相同的。 但是,因为 C++语言本身已经重载了该运算符,所以它能够用于 int、float、double 和其 他内部定义类型的变量。 运算符重载是通过编写函数定义实现的。函数定义虽然也包括函数首部和函数体,但 是函数名是由关键字 operator 和其后要重载的运算符符号组成的。例如,函数名 operator+重载了运算符+。 用于类的对象的运算符必须重载,但是有两种例外情况。赋值运算符(=)无需重载就 可用于每一个类。在不提供重载的赋值运算符时,赋值运算符的默认行为是复制类的数据 成员。不久就会看到,这种默认的复制行为对于带有指针成员的类是危险的,对这种类通 常要显式重载赋值运算符。地址运算符&也无需重载就可以用于任何类的对象,它返回对 象在内存中的地址。地址运算符也可以被重载。 运算符重载最适合用于数学类。为了与在现实世界中操作这些数学类的方式一致,通 常要重载一组运算符。例如,对于复数类,通常不仅仅要重载运算符+,因为其他算术运 算符也经常用于复数。 43 C++语言的运算符很丰富。因为程序员对每个运算符的含义和使用的具体语境是理解 的,所以在重载用于新类的运算符时,程序员能够根据运算符的意义做出合理的选择。 C++为其内部类型提供了丰富的运算符集,重载这些运算符的目的是为用户自定义 的类型提供同样简洁的表达式。然而,运算符的重载不是自动完成的,程序员必须为所要 执行的操作编写运算符重载函数。有时最好把这些函数用作成员函数,有时最好用作友元 函数,在极少数情况下,他们可能既不是成员函数,也不是友元函数。 可能会发生重载误用的情况,例如重载加法运算符(+)使它执行类似于减法的运算, 或者重载除法运算符(/)以使它执行类似于乘法的运算。如此使用重载会使程序令人迷惑 不解。 编程技巧 2.3 在把重载运算符用于类的对象时,重载运算符的功能类似于该运算符作用于内部类 型的对象时所完成的功能,避免没有目的地使用重载运算符。 编程技巧 2. 4 在用重载运算符编写 C++程序之前.查阅编译器的手册,了解特定运算符的各种限 制和要求。 2.3 运算符重载的限制 C++中的大部分运算符都可以被重载。图 8.1 列出了可以被重载的运算符,图 8.2 列出了不能被重载的运算符。 常见编程错误 2.1 想重载不能重载的运算符是个语法错误。 可以重载的运算符 + - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= && || ++ -- ->* , -> [] () new delete new[] delete[] 图 8.1 可以被重载的运算符 不可以重载的运算符 . .* :: ?: sizeof 图 2.2 不能被重载的运算符 重载不能改变运算符的优先级。虽然重载具有固定优先级的运算符可能会不便于使用, 44 但是在表达式中使用圆括号可以强制改变重载运算符的计算顺序。 重载不能改变运算符的结合律。 重载不能改变运算符操作数的个数。重载的一元运算符仍然是一元运算符,重载的二 元运算符仍然是二元运算符,C++中的惟一的三元运算符(?:)也不能被重载。运算符&、*、 +和-既可以用作一元运算符,也可以用作二元运算符,可以分别把他们重载为一元运算 符和二元运算符。 不能创建新的运算符,只有现有的运算符才能被重载。因此,程序员不能使用一些流 行的表示方法,如 BASIC 中表示指数的运算符**。 常见编程错误 2.2 试图创建新的运算符是个语法错误。 运算符重载不能改变该运算符用于内部类型对象时的含义。例如,程序员不能改变运 算符+用于两个整数时的含义。运算符重载只能和用户自定义类型的对象一起使用,或者 用于用户自定义类型的对象和内部类型的对象混合使用时。 常见编程错误 2. 3 试图改变运算符对内部类型的对象的作用方式是个浯法错误。 软件工程视点 2.2 运算符函数的参数至少有一个必须是类的对象或者是对类的对象的引用。这种规定防 止了程序员改变运算符对内部类型的对象的作用方式。 重载了赋值运算符=和加法运算符+以后,虽然下列语句是允许的: object2 = object2 + object1; 但并不意味运算符+=也被自动重载了。因此,下面的语句是不允许的: object2 += object1; 然而,显式地重载运算符+=可使上述语句成立。 常见编程错误 2.4 认为重载了某个运算符(如“+”)可以自动地重载相关的运算符(如“+=”),或重载 了“==”就自动重载了“!=”,运算符只能被显式重载(不存在隐式重载)。 常见编程错误 2.5 想通过运算符重栽改变运算符的”数量”是个语法错误。 编程技巧 2.5 要保证相关运算符的一致性,可以用一个运算符实现另一个运算符(即用重载的运算 符“+”实现重载的运算符“+=”)。 45 8.4 用作类成员与友元函数的运算符函数 运算符函数既可以是成员函数,也可以是非成员函数。非成员函数通常是友元函数。 成员函数是用 this 指针隐式地访问类对象的某个参数,非成员函数的调用必须明确地列 出该参数。 在重载运算符()、[]、->,或者任何赋值运算符时,运算符重载函数必须声明为类的一 个成员。对于其他的运算符,运算符重载函数可以是非成员函数。 不管运算符函数是成员函数还是非成员函数,运算符在表达式中的使用方式是相同 的。哪种实现方式更好呢? 当运算符函数是一个成员函数时,最左边的操作数(或者只有最左边的操作数)必须 是运算符类的一个类对象(或者是对该类对象的引用)。如果左边的操作数必须是一个不同 类的对象,或者是一个内部类型的对象,该运算符函数必须作为一个非成员函数来实现 (正如 2.5 节中分别重载运算符<<和>>作为流插入运算符和流读取运算符一样)。运算符 函数作为非成员函数直接访问该类的 private 或者 protected 成员时,该函数必须是一个 友元。 重载的<<运算符必须有一个类型为 ostream&的左操作数(例如表达式 cout<>运算符必 须有一个类型为 istream&的左操作数(如表达式 cin >> classObject 中的 cin),所以它也 必须是一个非成员函数。此外,这两个重载的运算符函数都需要访问输出或输入的类对象 的 private 数据成员,因此出于性能考虑,这些重载的运算符函数通常都是类的友元函 数。 性能提示 2.1 可以把一个运算符作为一个非成员、非友元函数重载。但是,这样的运算符函数访问 类的 private 和 protected 数据时必须使用类的 public 接口中提供的“set”或者 “get'’函数(即设置数据和读取数据的函数)、调用这些函数的开销会降低性能,因此必 须内联这些函数以提高性能。 只有当二元运算符的最左边的操作数是该类的一个对象时,或者当一元运算符的操 作数是该类的一个对象时.才需调用特定类的运算符成员函数。 选择非成员函数重载运算符的另外一个原因是使运算符具有可交换性。例如:假定有 longint 类型的一个对象 number 和类 HugeInteger 的一个对象 bigInteger1(本章的练习中 开发了类 HugeInteger,该类中的整数可以是任意大小,不受机器字长的限制)。如果要求 加法运算符(+)生成一个临时的 HugeInteger 对象,它是 HugeInteger 和 longint 类型对象 的和(如表达式 bigInteger1+number),或者是 longint 和 HugeInteger 类型对象的和(如表 达式 number+bigIntegerl),那么上述的加法运算符就要具有可交换性(正如通常的加法一 样)。问题在于,如果把运算符作为成员函数重载,类的对象必须出现在运算符的左边, 所以要将运算符函数作为一个非成员的友元重载,这样才能允许 HugeInteger 对象出现在 加法运算符的右边。处理 HugeInteger 对象在左边的 operator+函数依然可以是一个成员 函数。记住,非成员函数不一定要是友元,只要类的 public 接口中有相应 set 和 get 函数, 有内联的 set 和 get 函数则更好。 46 2.5 重载流插入与流读取运算符 C++的流读取运算>>和流插入运算符<<可用来输入输出标准类型的数据。这两个运算 符是 C++编译器在类库中提供的,可以处理包括类 C 语言中的 char*字符串和指针在内的 每一种内部数据类型。也可以重载运两个运算符以输入输出用户自定义类型的数据。图 2.3 中的程序演示了重载的流读取运算符和流插入运算符,它们用来处理用户自定义的 电话号码类 PhoneNumber 的数据。程序假定输入的电话号码是正确的,错误检测留给读者 在练习中完成。 1 // Fig. 2.3: fig0S03.cpp 2 // Overloading the stream-insertion and 3 // stream-extraction operators. 4 #include 5 #include 6 7 class PhoneNumber { 8 friend ostream &operator<<( ostream&, const PhoneNumber & ); 9 friend istream &operator>>( istream&, PhoneNumber & ); 10 11 private: 12 char areaCode[ 4 ]; // 3-digit area code and null 13 char exchang[ 4 ]; // 3-digit exchange and null 14 char line[ 5 ]; // 4-digit line and null 15 }; 16 17 // Overloaded stream-insertion operator (cannot be 18 // a member function if we would like to invoke-ti with 19 // cout << somePhoneNumber;). 20 ostream &operator<<( ostream &output, const PhoneNumber &num) 21 { 22 output << "(" << num.areaCode << ")" 23 << num.exchange << "-" << num.line; 24 return output; // enables cout << a << b << c; 25 } 26 27 istream &operator>>( istream &input, PhoneNumber &num ) 28 { 29 input.ignore(); // skip ( 30 input >> setw( 4 ) >> num,areaCode; // input area code 31 // skip ) and space 32 input >> setw( 4 ) >> num.exchange; // input exchange 33 input.ignore(); 47 34 input >> setw( 5 ) >> num.line; // input line 35 return input; // enables cin >> a >> b >> c; 36 } 37 38 int main() 39 { 40 PhoneNumber phone; // create object phone 41 42 cout << "Enter phone number in the form (123) 456-7890:\n"; 43 44 // cin >> phone invokes operator>> function by 45 // issuing the call operator>>( ein, phone ). 46 cin >> phone; 47 48 // cout << phone invokes operator<< function by 49 // issuing the call operator<<( eout, phone ). 50 cout << "The phone number entered was: "<< phone << endl; 51 return 0; 52 } Enter phone number in the form (123) 456-7890: (800) 555-1212 The phone number entered was: (800) 555-1212 图 2.3 用户自定义的流插入和流读取运算符 流读取运算符函数 operator>>(第 27 行)含有两个参数,一个是对 istream 的引用(即 程序中的 input),另一个则是对用户自定义类型 PhoneNumer 的引用(即程序中的 num)。函 数返回一个对 istream 的引用。在图 2. 3 的程序中,运算符函数 operator>>用来把下述格 式的电话号码输入到类 PhoneNumber 的对象中: (800) 555 = 1212 当编译器遇到 main()函数中的表达式: cin >> phone 编译器将生成函数调用: operator >> (cin,phone); 当执行该调用时,引用参数 input 成为 cin 的一个别名,Num 成为 Phone 的一个别名。运 算符函数使用 istream 成员函数 getline,将电话号码的三部分作为字符串分别读到被引 用的 PhoneNumber 对象(运算符函数中的 num 和 main 函数中的 phone)的 areaCode、exchange 和 line 成员中。流操纵算子 sesetw 保证将正确的字符数读入到字符数 组中。回忆一下,我们曾经使用 cin 和 setw 限制读入的字符数比参数少 1(例如 setw(4)只 允许读入 3 个字符,留出一个位置保存 null 终止符)。通过调用 istream 的成员函数 ignore 跳过括号、空格、破折号等等(ignore 函数删除输入流中指定数目的字符,默认个数 为 1)。函数 operator>>返回对 isream 对象的引用 input(即 cin),因而能够在 PhoneNumber 对象的输入操作完成后,继续执行对 PhoneNumber 的其他对象或者其他数据类型对象的 48 输入操作。例如,可以像下面那样输入两个 PhoneNumber 对象: cin >> phone1 >> phone2; 首先是表达式 cin >> phone1 产生如下调用: operator >> (cin,phone1); 该调用返回 cin 并把它作为 cin >> phone1 的值,因此表达式的其余部分将被简单地解释 为 cin >> phone2,这将通过下列调用执行: operator >> (cin,phone2); 流插入运算符有两个参数,一个是对 ostream 的引用(即 output),另一个是对用户自 定义类型 PhoneNumber 的引用(即 num),函数返回一个对 ostream 的引用。函数 operator<<显示了 PhoneNumber 的对象。当编译器遇到 main 函数中的表达式: cout << phone 编译器生成非成员函数调用: operator << (cout,phone); 因为电话号码的各个部分是以字符串的格式存储的,所以函数 operator<<以字符串形式 显示它们。 注意,函数 operator<<和 operator>>在类 PhoneNumber 中被声明为友元函数而不是 成员函数。因为要把类 PhoneNumber 的对象作为运算符的右操作数,所以这些运算符函数 必须是非成员函数。要把运算符重载为成员函数,类的操作数(类的对象)必须出现在运算 符的左边,如果重载的输入和输出运算符必须直接访问类的非 public 成员,则必须把它 们声明为友元。另外,还要注意 operator<< 参数表中引用的 PhoneNumber 是 const 类型 (因为只输出 PhoneNumber),而 operator>>参数表中引用的 PhoneNumber 是非 const 类型 (由于 PhoneNumber 对象要修改成在该对象中存放输入的电话号码)。 软件工程视点 2. 3 无需修改类 ostream 和 istream 的声明和 private 数据成员就可以给用户自定义类 型添加新的输入/输出能力。这种方式提高了 C++语言的可扩展性,可扩展性是 C++的最 具吸引力的特点。 2.6 重载一元运算符 类的一元运算符可重载为一个没有参数的非 static 成员函数或者带有一个参数的非 成员函数,参数必须是用户自定义类型的对象或者对该对象的引用。实现重载运算符的成 员函数应为非 static,以便访问类的非 static 数据。记住,static 成员函数只能访问类 的 static 数据成员。 本章稍后要用重载的一元运算符“!”测试一个字符串是否为空并返回一个布尔值。 当把一元运算符(如“!”)重载为没有参数的非 static 成员函数时,如果 s 是 String 类的 对象或是对 String 类对象的引用,那么编译器在遇到表达式!s 时会生成函数调用 s.operator!()。操作数 s 是类的对象,它调用了 String 类的成员函数 operator!。类定义中 的函数声明如下: class String{ public: 49 bool operator!() const; 把一元运算符(如“!”)重载为带有一个参数的非成员函数时,参数有两种不同的情 况。一种情况是该参数是某个对象(需要对象的副本,因此函数不作用于原对象),另一种 情况是该参数是对某个对象的引用(不复制原对象,因此函数会作用于原对象)。如果参数 s 是 String 类的一个对象或对 String 类对象的引用,则!s 将被处理为 operator!(s),调用 String 类的非成员友元函数。String 类声明如下: class string { friend bool operator!(const String&); }; 编程技巧 2.6 重载一元运算符时,把运算符函数用作类的成员而不用作友元函数。因为友元的使用 破坏了类的封装,所以除非绝对必要,否则应尽量避免使用友元函数和友元类。 2.7 重载二元运算符 二元运算符可以重载为带有一个参数的非 static 成员函数,或者带有两个参数的非 成员函数(参数之—必须是类的对象或者是对类的对象的引用)。 本章稍后要重载运算符+=,当把它重载为带有一个参数的 String 类的非 static 成员 函数时,如果 y 和 z 是 String 类的对象,则 y == z 将被处理为 y.operator+=(z),调用成 员函数 operator+=,声明如下: class String{ public: const String&operator+=(const String &); }; 二元运算符+=也可以重载为带有两个参数的非成员函数,其中的一个参数必须是类 的对象或者是对类的对象的引用。如果 y 和 z 是 String 类的对象,则 y += z 将被处理为 operator+=(y,z),调用友元函数 operator+=,声明如下: class String{ friend const String&operator+=(String &, const String &); }; 2.8 实例研究:Array 类 在 C 和 C++中,数组是一种指针,因而数组存在许多导致错误的陷阱。例如,由于 C 和 C+ +不检测下标是否超出数组的边界而使程序导致越界错误;大小为 n 的数组的下标必须是 0、1、2…、 n-1,下标是不允许改变的;不能一次入输人或输出整个数组,而只能单独读 取或者输出每个数组元素;不能用相等运算符或者关系运算符比较两个数组(因为数组名 仅仅是指向内存中数组起始位置的指针);当把一个数组传递给一个能处理任意大小数组 50 的常用函数时,数组的大小也必须作为一个额外的参数传递给该函数;不能用赋值运算 符把一个数组赋给另一个数组(因为数组名是 const 类型指针,而常量指针不能用于赋值 运算符的左边)。尽管所有这些处理能力似乎应该是很自然的,但是 C 和 C++都没有提供这 种能力。然而,C++提供了实现这种能力的手段,这就是运算符重载。 本节的范例建立了一个数组类,它能检测范围以确保数组下标不会越界,允许用赋 值运算符把一个数组赋给另外一个数组。数组对象自动知道数组的大小,因而不用将数组 的大小传送给函数。可以用流读取运算符和流插入运算符输入输出整个数组。还可以用相 等运算符==和!=比较数组。范例程序中的数组类用一个 static 成员跟踪程序中实例化数组 对象的数目。 本例将加深读者对数据抽象的认识。当然,读者还可以增加数组类的其他功能,类的 开发是十分有趣并富有挑战性的。 图 2.4 中的程序演示了类 Array 和用于该类的重载运算符。首先来看一下 main 函数 中的驱动程序,然后再探讨类的定义以及类的每个成员和友元函数的定义。 1 // Fig. 2.4: arrayl.h 2 // Simple class Array (for integers} 3 #ifndef ARRAY1_H 4 #define ARRAY1_H 5 6 #include 7 8 class Array { 9 friend ostream &operator<<( ostream &, const Array & ); l0 friend istream &operator>>( istream &, Array & ); 11 public: 12 Array( int = 10 ); // default constructor 13 Array( const Array & ); // copy constructor 14 ~Array(); // destructor 15 int getSize() const; // return size 16 const Array &operator=( const Array & ); // assign arrays 17 bool operator==( const Array & ) const; // compare equal 18 19 // Determine if two arrays are not equal and 20 // return true, otherwise return false (uses operator==). 21 bool operator!=( const Array &right ) const 22 { return ! ( *this == right ); } 23 24 int &operator[] ( int ); // subscript operator 25 const int &operator[]( int ) const; // subscript operator 26 static int getArrayCount(); // Return count of 27 // arrays instantiated. 28 private: 29 int size; // size of the array 30 int *ptr; // pointer to first element of array 31 static int arrayCount; // # of Arrays instantiated 51 32 } ; 33 34 #endif 35 // Fig 2.4: arrayl.cpp 36 // Member function definitions for class Array 37 #include 38 #include 39 #include 40 #include 41 #include "array1.h" 42 43 // Initialize static data member at file scope 44 int Array::arrayCount = 0; // no objects yet 45 46 // Default constructor for class Array (default size 10) 47 Array::Array( int arraySize ) 48 { 49 size = ( arraySize > 0 ? arraySize : 10 ); 50 ptr = new int[ size ] ; // create space for array 51 assert( ptr != 0 ); // terminate if memory not allocated 52 ++arrayCount; // count one more object 53 54 for (int i = 0; i < size; i++ ) 55 ptr[ i ] = 0; // initialize array 56 } 57 58 // Copy constructor for class Array 59 // must receive a reference to prevent infinite recursion 60 Array::Array( const Array &init ) : size( intit.size ) 61 { 62 ptr = new int[ size ] ; // create space for array 63 assert( ptr != 0 ); // terminate if memory not allocated 64 ++arrayCount; // count one more object 65 66 for (int i = 0; i < size; i++ ) 67 ptr[ i ] init.ptr[ i ]; // copy init into object 68 } 69 70 // Destructor foi class Array 71 Array::~Array() 72 { 73 delete [] ptr; // reclaim space for array 74 --arrayCount; // one fewer objects 75 } 52 76 77 // Get the size of the array 78 int Array::getSize() const { return size; } 79 80 // Overloaded assignment operator 81 // const return avoids: ( al = a2 } = a3 82 const Array &Array::operator=( const Array &right ) 83 { 84 if ( &right != this ) { // check for self-assignment 85 86 // for arrays of different sizes, deallocate original 87 // left side array, then allocate new left side array. 88 if ( size != right.size ) { 89 delete [] ptr; // reclaim space 90 size = right.size; // resize this object 91 ptr = new int[ size ]; // create space for array copy 92 assert( ptr != 0 ); // terminate if not allocated 93 } 94 95 for (int i = 0; i < size; i++ ) 96 ptr[ i ] = right.ptr[ i ]; // copy array into object 97 } 98 99 return *this; // enables x = y = z; 100 } 101 102 // Determine if two arrays are equal and 103 // return true, otherwise return false. 104 bool Array::oprator==( const Array &right )const 105 { 106 if ( size != right.size ) 107 return false; // arrays of different sizes 108 109 for (int i =0; i < size; i++ ) 110 if ( ptr[ i ] != right.ptr[ i ] ) 111 return false; // arrays are not equal 113 return true; // arrays are equal 114 } 117 // reference return creates an lvalue 118 int &Array::operator[] ( int subscript ) 119 { 120 // check for subscript out of range error 121 assert( 0 <= subscript && subscript < size ); 122 53 123 return ptr[ subscript ]; // reference return 124 } 125 126 // Overloaded subscript operator for const Arrays 127 // const reference return creates an value 128 const int &Array::operator[ ] (int subscript ) const 129 { 130 // check for subscript out of range error 131 assert( 0 <= subscript && subscript < size ); 132 133 return ptr[ subscript ]; // const reference return 134 } 135 136 // Return the number of Array objects instantiated 137 // static functions cannot be const 138 int Array::getArrayCount() { return arrayCount; } 139 140 // Overloaded input operator for class Array; 141 // inputs values for entire array. 142 istream &operator>>( istream &input, Array &a ) 143 { 144 for ( int i = 0; i < a.size; i++ ) 145 input >> a.ptr[ i ]; 146 147 return input; // enables cin >> x >> y; 148 } 149 150 // Overloaded output operator for class Array 151 ostream &operator<<( ostream &output, const Array &a ) 152 { 153 int i; 154 155 for ( i = O; i < a.size; i++ ) { 156 output << setw( 12 ) << a.ptr[ i ]; 157 158 if ( ( i + 1 ) % 4 == 0 ) // 4 numbers per row of output 159 output << endl; 160 } 161 162 if( i % 4 != 0 ) 163 output << endl; 164 165 return output; // enables cout << ~ << y; 166 } 54 167 // Fig. 8.4:fig08 04.cpp 168 // Driver for simple class Array 169 #include 170 #include "arrayl.h" 171 172 int main() 173 { 174 // no objects yet 175 cout << "# of arrays instantiated = " 176 << Array::getArrayCount() << '\n'; 177 178 // create two arrays and print Array count 179 Array integers1( 7 ), integers2; 180 cout << "# of arrays instantiated = " 181 << Array::getArrayCount() << "\n\n"; 182 183 // print integersl size and contents 184 cout << "Size of array integers1 is" 185 << integers1.getSize() 186 << "\nArray after initialization:\n" 187 << integersl << '\n'; 188 189 // print integers2 size and contents 190 cout << "Size of array integers2 is " 191 << integers2.getSize() 192 << "\nArray after initialization:\n" 193 << integers2 << '\n'; 194 195 // input and print integersl and integers2 196 cout << "Input 17 integers:\n"; 197 cin >> integers1 >> integers2; 198 cout << "After input, the arrays contain:\n" 199 << "integersl:\n" << infegers1 200 << "integers2:\n" << integers2 << '\n'; 201 202 // use overloaded inequality (!=) operator 203 cout << "Evaluating: integers1 != integers2\n"; 204 if ( integers1 != integers2 ) 205 cout << "They are not equal\n"; 206 207 // create array integers3 using integers1 as an 208 // initlizer; print size and contente 209 Array integers3( integers1 ); 210 55 211 cout << "\nSize of array integers3 is" 212 << integers3.getSize() 213 << "\nArray after initialization:\n" 214 << integers3 << '\n'; 215 216 // use overloaded assignment (=) operator 217 cout << "Assigning integers2 to integers1:\n"; 218 integers1 = integers2; 219 cout << "integersl:\n" << integers1 220 << "integers2:\n" << integers2 << '\n'; 221 222 // use overloaded equality (==) operator 223 cout << "Evaluating: integers1 == integers2\n"; 224 if ( integers1 == integers2 ) 225 cout << "They are equal\n\n"; 226 227 // use overloaded subscript operator to create rvalue 228 cout << "integers1[ 5 ] is "<< integers1[ 5 ] << '\n'; 229 230 // use overloaded subscript operator to create lvalue 231 cout << "Assigning 1000 to integers1[ 5 ]\ n"; 232 << integers1[ 5 ] = 1000; 233 cout << "integers1:\n" << integers1 << '\n'; 234 235 // attempt to use out of range subscript 236 237 integers1[ 15 ] = 1000; // ERROR: out of range 238 239 return 0; 240 } 输出结果: # of arrays instantiated = 0 # of arrays instantiated 2 Size of array integersl is 7 Array after initialization: 0 0 0 0 0 0 0 0 Size of array integers2 is 10 Array after initialization: 0 0 0 0 0 0 0 0 0 0 56 Input 17 integers: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 After input, the arrays contain: integersl: 1 2 3 4 5 6 7 integers2: 8 9 10 11 12 13 14 15 16 17 Evaluating:integers1 != integers2 They are not equal Size of array integers3 is 7 Array after initialization: 1 2 3 4 5 6 7 Assigning integers2 to inteqersl: integersl: 8 9 10 11 12 13 14 15 16 17 integers2: 8 9 10 11 12 13 14 15 16 17 Evaluating: integersl == integers2 They are equal integersl[ 5] is = 13 Assigning 1000 to integersl[ 5 ] integersl: 8 9 10 11 12 1000 14 15 16 17 Attempt to assign 1000 to integersl[15] Assertion failed: 0 <= subscript && subscript < size, file Arrayl.cpp, line 87 abnormal program termination 图 2.4 用重载运算符演示 Array 类 57 类 Array 的 static 类变量 arrayCount 包含了程序执行过程中实例化的 Arrayy 对象 的个数,该值由第 176 行的 static 成员函数 getArrayCount 返回。程序实例化子类 Array 的两个对象(第 179 行),对象 integees1 有 7 个元素.对象 integers2 有 10 个元素(默认的 元素个数由类 Array 的构造函数指定)。 第 181 行再次调用 getArrayCount,取得类变量 arrayCount 的值。第 184 到第 187 行用 成员函数 getSize 确定 arrayintegers1 的长度,并使用 Array 重载流插入运算符输出 integer1,以证实构造函数正确地初始化了数组的元素。接下来,第 190 到第 193 行的程序 先输出数组 integers2 的长度,然后用重载的流插入运算符输出 integer2 数组。 完成上述工作后,程序提示用户输入 17 个整数,Array 重载流读取运算符并使用下 列语句(第 197 行): cin >> integers1 >> integers2; 把这些值读入到两个数组中,前 7 个整数保存在 integers1 中,其余的则保存在 intergers2 中。为了证实输入操作的正确性,程序用流插入运算符输出了这两个数组(第 198 到第 200 行)。 接下来,程序通过测试条件(第 204 行): integers1 != integers2 来验证重载的不相等运算符!=,输出结果表明这两个数组确实不相等。 第 209 行程序实例化第三个数组 integers3 并用数组 integers1 对其初始化,这将调 用 Array 复制构造函数将 integers1 复制到 integers3 中。我们将在后面详细讨论复制构造 函数。 程序在第 211 到第 214 行输出 integers3 的长度,并使用 Array 重载流插入运算符输 出 integers3,以证实构造函数正确地初始化数组。 接下来,第 218 行通过下列语句测试重载的赋值运算符(=): integers1 = integers2; 然后打印出这两个数组来验证赋值的正确性。原来的 integere1 中只有 7 个整数,现在必 须要使其能容纳 integers2 中的 10 个元素的副本。重载的赋值运算符可以改变原先的 integers1 的大小并复制 integer2 中的元素。 接下来,第 224 行用重载的运算符==;测试赋值后 integers1 和 integers2 是否相等。 接下来,第 228 行用重载的下标运算符引用 integers1[5](integers1 数组范围内的一 个元素),这个带下标的数组名作为右值来打印 integersl[5]的值,第 232 行将 integers[5]放在赋值语句左边并赋给其一个新值 1000,注意,operator[]返回引用并作为 左值使用(在确定了 5 是在 integers1 的长度范围内)。 第 237 行程序试图将 1000 赋给 integers[15](越界的元素)。Array 重载了[]运算符捕获 到该错误并中止程序。 有意思的是,数组下标运算符不仅仅可用于数组,还可以用于从其他各种容器类(如 链表、字符串、字典等等)中选择元素。此外,下标不仅仅是整数,还可以是字符、字符串、 浮点数甚至是用户自定义的对象。 上面介绍了程序是如何执行的。下面再分析—下类的首部和成员函数的定义。第 29 行 到第 3l 行 int size;// size Of the array int *ptr;// pointer to first element of array static int arrayCount;// # of Arrays instantiated 是类的 private 数据成员,包括一个 int 类型指针 ptr(指向 Array 对象中存储整型的动 58 态分配数组)、一个表示数组元素个数的 size 成员以及一个表示已经实例化的数组对象数 目的 static 成员 arrayCount。 第 9 行和第 10 行: friend ostream &operator<<(ostream &,const Array &); friend istream &operator<<(istream &,Array &); 声明了重载的流插入、流读取运算符是类 Array 的友元。当编译器遇到表达式: cout << arrayObject 通过生成 operator<<(cout,arrayObject)来调用函数 operator<(ostream &, constArray &)。当编译器遇到表达式: cin >> arrayObject 通过生成 operator>>(cin,arrayObjeet)来调用函数 operatpr>>(istream &, Array &)。 因为 Array 对象总是在流插入运算符和流读取运算符的右边,所以这两个运算符函 数不能是 Array 的成员函数。如果这些运算符函数是 Array 的成员函数,则可以用下列的 语句(可能会出现意外情况)输入和输出 Array: arrayObject << cout; arrayObject >> cin; 函数 operator<<(在第 151 行定义)打印由 size 指定的存储在 ptr 中的数组元素的个数, 而函数 operator>>(在第 142 行定义)则把数据直接输入到 ptr 所指向的数组中。为了能够 分别实现连续的输入输出,这两个运算符都返回了一个合适的引用。 代码行: Array(int= 1O); // default Constructor 声明了类的默认构造函数,并且指定数组元素的默认大小为 10。当编译器遇到如下声明: Array integers1(7); 或与之等价的形式: Array integers1 = 7; 编译器将调用默认构造函数(本例中默认构造函数实际上接收一个 int 参数,默认值为 10)。默认构造函数(第 47 行定义)验证参数并赋值给 Size 数据成员,用 new 分配数组所需 的空间,将 new 返回的指针赋给数据成员 ptr,然后用 assert 测试 new 操作是否成功, 并递增 arrayCount 的值,最后用 for 循环将数组的所有元素初始化为 0。如果没有将 Array 初始化,也可以在以后读取相应的值,但这样做会降低程序的可执行性。Array 和 任何对象都应随时保持正确初始化和一致的状态。 第 13 行: Array(const Array &); // copy Constructor 是一个复制构造函数(第 60 行定义),它通过建立现有 Array 对象的副本来初始化 Array 对 象。必须要小心对待这种复制操作,避免两个 Array 对象指向同一块动态分配的存储区, 默认的成员复制更容易发生这种问题。不论何时需要复制对象时都会调用复制构造函数, 如在传值调用时、从被调用函数返回一个对象时、或把某个对象初始化为同类的另外一个 对象的副本时。当声明创建类 Array 的一个对象并用另外一个对象对它初始化时,调用复 制构造函数。例如下列声明: Array integers3(integers1); 或者与之等价的声明: Array integers3 = integersl; 59 常见程错误 2.6 注意复制构造函数要按引用调用,而不是按值调用,否则复制构追函数调用会造成 无穷递归(这是个致命逻辑错误),因为对于按值调用,建立传入复制构造函数的对象副 本会造成复制构造函数的递归调用。 复制构造函数 Array 使用成员初始化值将数组的 size 值复制到新数组的数据成员 size 中,用 new 分配新数组所需的空间,把 new 返回的指针赋给数据成员 Ptr,然后用 assert 测试 new 操作是否成功,并递增 arrayCount 的值,最后用 for 循环将数组的所有 元素作为初始值复制到新数组中。 常见编程错误 2. 6 如果构造函数简单地将源对象的指针复制到目标对象的指针,则这两个对象将指向 同一块动态分配的内 存块,执行析构函数时将释放该内存决,从而导致另外一个对象的 Ptr 没有定义,这 种情况可能令引起严重的 运行时错误。 软件工程视点 2.4 通常要把构造函数、析构函数、重载的赋值运算符以及复制构这造函数一起提供给使 用动态内存分配的类。 第 14 行: ~Array(); // destructor 声明了类的析构函数(第 71 行定义)。当撤消类 Array 的某个对象时,自动调用析构函数。 析构函数用 delete[]释放在构造函数中用 new 动态分配的内存块,然后递减 arraycount 的值。 第 15 行: int getSize() const; // return size 声明了读取数组大小的函数。 第 16 行: const Array &operator= ( const Array &); // assign arrays 声明了重载的赋值运算符函数。当编译器遇到表达式: integers1 = integers2; 就会通过产生如下代码调用函数 operator=: integers1.operator=(integers2) 成员函数 operator=(第 82 行定义)测试了这种赋值是否是自我赋值。如果是,则跳过赋值 操作(即对象已经是其自身,无需再赋值)。如果不是,则成员函数确定两个数组长度是否 相同,如果是,则左边 Array 对象的原始整数数组不重新分配。否则成员函数 operator= 用 delete 释放目标数组原先动态分配的空间,将源数组的数据成员 size 复制到目标数组 的 size,用 new 分配目标数组所需的空间并将 new 返回的指针赋给数组的 Ptr 成员,用 assert 测试 new 操作是否成功,最后再用 for 循环将源数组的每一个元素复制到目标数 组中。不管这种操作是否是自我赋值,成员函数都返回当前对象 (即*this),这种处理方式允许诸如 x=y=z 这样的连续赋值。 60 常见编程错误 2.7 类的对象包含指向动态分配的内存的指针,但如果没有为它提供重载的赋值运算符 和复制构造函数则会 造成逻辑错误。 软件工程视点 2. 5 把赋值运算符定义为类的 private 成员可以防止将一个类对象赋给另外一个类对象。 软件工程视点 2.6 只要重载的赋值运算符和复制构造函数为 private,就可以防止复制类对象。 第 17 行: bool operator=(const Array &)const; // compare equal 声明了重载的相等运 算符。当编译器遇到 malll 函数中的如下表达式时: integers1 == integeis2 编译器通过生成如下代码来调用 operator == 成员函数: integers1.operator==(integers2) 如果数组的 size 成员不相等,则 operator==成员函数立即返回 false,否则,成员 函数开始成对比较相应的元素。如果它们全都相等,则返回 true,一旦发现某一对元素不 同则立即返回 false。 第 21 到第 22 行: bool operator!=(const Array &right)const { return ! ( *this == right);} 声明了重载的不相等运算符(!=)。成员函数中 oprator!=根据重载的相等运算符定义。该函 数定义用重载 operator==函数确定一个 Array 是否等于另一个 Array,然后返回结果的 相反值。这样编写 oprator!=函数使程序员可以复用 operator==函数,减少类中需要编写 的代码量。另外,operator!=的完整函数定义在 Array 头文件中,使编译器可以内联 operator!=的定义,消除额外函数调用的开销。 第 24 到第 25 行: int &operator[](int); // subscript operator const int &operator[](int)const; // subscript operator 声明了两个重载的下标运算符(分别在第 118 和 128 行定义)。当编译器遇到 main 函数中的 如下表达式时: integers1[ 5] 编译器通过生成下列代码来调用重载的 operator[]成员函数: integers1.operator[] (5) constArray 对象使用下标运算符时,编译器调用 operator 的[]的 const 版本。operator[] 成员函数首先测试下标是否越界。如果越界,则程序异常中止。如果没有越界,则对 operator==的非 const 版本返回相应的数组元素作为引用,以便使它能用作左值(如用在 赋值语句的左边)。而对 operator[]的 const 版本返回右值。 第 26 行: static int getArrayCount(); // return count of Arrays 声明了 static 成员函数 getArrayCount。即使在不存在类 Array 的对象中,该成员函数 也返回静态数据成员 arrayCount 的值。 61 2.9 类型之间的转换 大多数程序能处理各种数据类型的信息。有时候所有的操作还会集中于某一种类型上, 例如,整数加整数还是整数(只要结果不是太大,能用整数表示出来)。但是,常常需要将 一种类型的数据转换为另外一种类型的数据,赋值、计算、给函数传值以及从函数返回值 都可能会发生这种情况。对于内部的类到,编译器知道如何转换类型。程序员也可以用强 制类型转换运算符实现内部类型之间的强制转换。 但是怎样转换用户自定义类型呢?编译器不知道怎样实现用户自定义类型和内部类型 之间的转换,程序员必须明确地指明如何转换。这种转换可以用转换构造函数实现,也就 是使用单个参数的构造函数,这种函数仅仅把其他类型(包括内部类型)的对象转换为某 个特定类的对象。本章梢后要用一个转换构造函数把正常的 char*类型的字符串转换为类 Siring 的对象。 转换运算符(也称为强制类型转换运算符)可以把一种类的对象转换为其他类的对象 或内部类型的对象。这种运算符必须是一个非 static 成员函数,而不能是友元函数。 函数原型: A::operator char *() const; 声明了一个重载的强制类型转换运算符函数,它根据用户自定义类型 A 的对象建立一个 临时的 char*类型的对象。重载的强制类型转换运算符函数不能指定返回类型(返回类型是 要转换后的对象类型)。如果 s 是某个类对象,当编译器遇到表达式(char*),时,会产生 函数调用 s.operator char*(),操作数 s 是调用成员函数 operator char*的类对象 s。 为了把用户自定义类型的对象转换为内部类型的的对象或用户自定义的其他类型的 对象,我们可以定义重载的强制类型转换运算符函数。函数原型: A::operator int()const; A::operator otherClass()const; 声明了两个重载的强制类型转换运算符函数,分别用来把用户自定义类型 A 的对象转换 为一个整数和用户自定义类型 otherClass 的对象。 强制类型转换运算符和转换构造函数一个很好的特点就是:当需要的时候,编译器 可以为建立一个临时对象而自动地调用这些函数。例如,如果用户自定义的类 String 的 某个对象 s 出现在程序中需要使用 char*类型的对象的位置上,例如: cout << s; 编译器调用重载的强制类型转换运算符函数 operator char*将对象转换为 char*类型,并 在表达式中使用转换后的 char*类型的结果。String 类提供该转换运算符后,不需要重载 流插入运算符用 cout 输出 String。 2.10 实例研究:String 类 作为学习重载的练习,本节要建立一个能够处理字符串的建立和操作的类(图 2.5)。string 类已是 C++标准库中的一部分。现在我们用运算符重载建立一个 String 类。 我们首先列出 String 类的首部,并讨论表示 String 的对象的 private 数据。然后,分析 62 类的 Public 接口,讨论该类提供的每一种服务。 接着分析了 main 函数中的驱动程序。讨论了令人感兴趣的编码风格,也就是用新 String 类的对象和该类的重载运算符集编写的各种运算符表达式。 然后我们讨论了类 String 的成员函数的定义。对于每个重载的运算符函数,驱动程 序都有调用重载的运算符的代码,并解释了这些函数的工作原理。 1 // Fig. 2.5: string1.h 2 // Definitien of a String class 3 #ifndef STRING1_H 4 #define STRING1_H 5 6 #include 7 8 class String { 9 friend ostream &operator<<( ostream &, const String & ); 10 friend istream &operator>>( istream &, String & ); 11 12 public: 13 String( const char * ="" ); // conversion/default ctor 14 String( const String & ); // copy constructor 15 ~String(); // destructor 16 const String &operator=( const String & ); // assignment 17 const String &operator+=( const String & ); // concatenation 18 bool operator!() const; // is String empty? 19 bool operator==( const String & ) const; // test sl - s2 20 bool operator<( coost String & ) const; // test sl < s2 21 22 // test s1 != s2 23 bool operator!=( const String & right ) const 24 { return !( *this == right ); } 25 26 // test si > S2 27 bool operator>( const String &right ) const 28 { return right < *this; } 29 30 // test s1 <= s2 31 bool operator<=( const String &right ) const 32 { return !( right < *this ); ( 33 34 // test s1 >= s2 35 bool operator>=( const String &right ) const 36 { return !( *this < right ); } 37 38 char &operator[] ( int ); // subscript operator 63 39 const char &operator[]( int ) const; // subscript operator 40 String &operator()( int, int ); // return a substring 41 int getLength() const; // return string length 42 43 private: 44 int length; // string length 45 char *sPtr; // pointer to start of string 46 47 void setString( const char * ); // utility function 48 }; 50 #endif 51 // Fig. 8.5: string1.cpp 52 // Member function definitions for class String 53 #include 54 #include 55 #include 57 #include "string1.h" 58 59 // Conversion constructor: Convert char * to String 60 String::String( const char *s ) : length( strlen( s )) 61 { 62 cout << "Conversion constructor: "<< s << '\n'; 63 setString( s ); // call utility function 64 } 65 66 // Copy constructor 67 String::String( const String © ) : length( copy.length ) 68 { 69 cout << "Copy constructor: " << copy.sPtr << '\n'; 70 setString( copy.sPtr ); // call utility function 71 } 72 73 // Destructor 74 String::~string() 75 { 76 cout << "Destructor: "<< sPtr << '\n'; 77 delete [] sPtr; // reclaim string 78 } 79 80 // Overloaded = operator; avoids self assignment 81 const String &String::operator=( const String &right ) 82 { 83 cout << "operator= called\n"; 85 if ( &right != this ) { // avoid self assignment 64 86 delete [] sPtr; // prevents memory leak 87 length = right.length; // new String length 88 setString( right.sPtr ); // call utility function 89 } 90 else 91 cout << "Attempted assignment of a String to itself\n"; 92 93 return *this; // enables cascaded assignments 94 } 95 96 // Concatenate right operand to this object and 97 // store in this object. 98 const String &String::operator+=( const String &right ) 99 { 100 char *tempPtr = sPtr; // hold to be able to delete 101 length += right.length; // new String length 102 sPtr= new char[ length + 1 ]; // create space 103 assert( sPtr != 0 ); // terminate if memory not allocated 104 strcpy( sPtr, tempPtr ); // left part of new String 105 strcat( sPtr, right.sPtr ); // right part of new String 106 delete [] tempPtr; // reclaim old space 107 return *this; // enables cascaded calls 108 } 109 110 // Is this String empty? 111 bool String::operator!() const { return length == 0; } 112 113 // Is this String equal to right String? 114 bool String::oprator==( const String &right ) const 115 { return strcmp( sPtr, right.sPtr ) == 0; } 116 117 // Is this String less than right String? 118 bool String::oprator<( const String &right ) const 119 { return strcmp( sPtr, right.sPtr ) < 0; } 120 121 // Return a reference to a character in a String as an lvalue. 122 char &String::operator[] ( int subscript ) 123 { 124 // First test for subscript out of range 125 assert( subscript >= 0 && subscript < length ); 126 127 return sPtr[ subscript ]; // creates lvalue 128 } 129 65 130 // Return a reference to a character in a String as an rvalue. 131 const char &String::oprator[]( int subscript ) const 132 { 133 // First test for subscript out of range 134 assert( subscript >= 0 && subscript < length ); 135 136 return sPtr[ subscript ]; // creates rvalue 137 } 138 139 // Return a substring beginning at index and 140 // of length subLength as a reference to a String object. 141 String &String::operator()( int index, int subLength ) 142 { 143 // ensure index is in range and substring length >= 0 144 assert( index >= 0 && index < length && subLength >= 0 ); 145 146 String *subPtr = new String; // empty String 147 assert( subPtr != 0 ); // ensure new String allocated 148 149 // determine length of substring 150 if ( ( subLength == 0 ) || ( index + subLength > length ) ) 151 subPtr->length = length - index + 1; 152 else 153 subPtr->length = subnength + 1; 154 155 // allocate memory for substring 156 delete subPtr->sPtr; // delete character array from object 157 subPtr->sPtr = new char [ subPtr->length ]; 158 assert( subPtr -> sPtr != 0 ); // ensure space allocated 159 160 // copy substring into new String 161 strncpy( subPtr->sPtr, &sPt[ index ], subPtr->length ); 162 subPtr->sPtr[ subPtr -> length ] = '\0'; // terminate String 163 164 return *subPtr; // return new String 165 } 166 167 // Return string length 168 int String::getLength() const { return length; } 169 170 // Utility function to be called by constructors and 171 // assignment operator. 172 void String::setString( const char *string2 ) 173 { 66 174 sPtr = new char[ length + 1 ]; // allocate storage 175 assert( sPtr != 0 ); // terminate if memory not allocated 176 strcpy( sptr, string2 ); // copy literal to object 177 } 178 179 // Overloaded output operator 180 ostream &operator<<( ostream &output, const String &s ) 181 { 182 output << s.sPtr; 183 return output; // enables cascading 184 } 185 186 // Overloaded input operator 187 istream &operator>>( istream &input,String &s ) 188 { 189 char temp[ 100 ]; // buffer to store input 190 191 input >> setw( 100 ) >> temp; 192 s = temp; // use String class assignment operator 193 return input; // enables cascading 194 } 195 // Fig. 2.5:fig08 05.cpp 196 // Driver for class String 197 #include 198 #include "string1.h" 199 200 int main() 201 { 202 String s1( "happy" ), s2( "birthday" ), s3; 203 204 // test overloaded equality and relational operators 205 cout << "s1 is \"" << s1 << "\"; s2 is \"" << s2 206 << "\"; s3 is \"" << s3 << '\"' 207 << "\nThe results of comparing s2 and s1:" 208 << "\ns2 == s1 yields" 209 << ( s2 == s1 ? "true" : "false" ) 210 << "\ns2 != s1 yields" 211 << ( s2 != s1 ? "true" : "false" } 212 << "\ns2 > s1 yields" 213 << ( s2 > s1 ? "true" : "false" ) 214 << "\ns2 < s1 yields " 215 << ( s2 < s1 ? "true" : "false" ) 216 << "\ns2 >= s1 yields 217 << ( s2 >= s1 ? "true" : "false" ) 67 218 << "\ns2 <= s1 yields" 219 << ( s2 <= s1 ? "true" : "false" ); 220 221 // test overloaded String empty (!) operator 222 cout << "\n\nTesting !s3:\n"; 223 if ( !s3 ) { 224 cout << "s3 is empty; assigning s1 to s3;\n"; 225 s3 = s1; // test overloaded assignment 226 cout << "s3 is \"" << s3 << "\""; 227 } 228 229 // test overloaded String concatenation operator 230 cout << "\n\ns1 += s2 yields s1 = "; 231 s1 += s2; // test overloaded concatenation 232 cout << s1; 233 234 // test conversion constructor 235 cout << "\n\ns1 += \" to you\" yields\n"; 236 s1 +=" to you"; // test conversion constructor 237 cout << "s1 = "<< s1 << "\n\n"; 238 239 // test overloaded function call operator () for substring 240 cout << "The substring of s1 starting at\n" 241 << "location 0 for 14 characters, s1(0, 14), is:\n" 242 << s1( 0, 14 ) << "\n\n"; 243 244 // test substring "to-end-of-String" option 245 cout << "The substring of s1 starting at\n" 246 << "location 15, s1(15, 0), is:" 247 << s1( 15, 0 ) << "\n\n"; // 0 is "to end of string" 248 249 // test copy constructor 250 String *s4Ptr = new String(s1); 251 cout << "*s4Ptr = "<< *s4Ptr << "\n\n"; 252 253 // test assignment (=) operator with self-assignment 254 cout << "assigning *s4Ptr to *s4Ptr\n"; 255 *s4Ptr = *s4Ptr; // test overloaded assignment 256 cout << "*s4Ptr = "<< *s4Ptr << '\n'; 257 258 // test destructor 259 delete s4Ptr; 260 261 // test using subscript operator to create lvalue 68 262 s1[ 0 ] = 'H'; 263 s1[ 6 ] = 'B'; 264 cout << "\nsl after s1[ 0 ] = 'H' and s1[ 6 ] = 'B' is:" 265 << s1 << "\n\n"; 266 267 // test subscript out of range 268 cout << "Attempt to assign 'd' to s1[ 30 ] yields:" << endl; 269 s1[ 30 ] = 'd'; // ERROR: subscript out of range 270 271 return 0; 272 } 输出结果: Conversion constructor: happy Conversion constructor: birthday Conversion constructor: sl is "happy"; s2 is "birthday"; s3 is "" The results of comparing s2 and s1: s2 == s1 yields false s2 != s1 yields true s2 > sl yields false s2 < sl yields true s2 >= s1 yields false s2 <= s1 yields true Testing !s3: s3 is empty; assigning s1 to s3; operator = called s3 is "happy" s1 += s2 yields s1 = happy birthday s1 +=" to you" yields Conversion constructor: to you Destructor: to you s1 = happy birthday to you Conversion constructor: The substring of sl starting at location 0 for 14 characters, sl(0, 14), is: happy birthday Conversion constructor: The substring of sl starting at 69 location 15, s1(15,0}, is: to you copy constructor: happy birthday to you *s4Ptr = happy birthday to you assigning *s4Ptr to *s4Ptr operator = called Attempted assignment of a string to itself *s4Ptr = happy birthday to you destructor: happy birthday to you s1 after s1[ 0] = 'H' and si[ 6] = 'B' is: Happy Birthday to you Attempt to assign 'd' to s1[30] yields: Assertion failed: subscript >= 0 && subscript < length, file String1.cpp,line 76 abnormal program termination 图 2.5 定义基本的 String 类 我们从 String 的内部表示开始讨论。第 44 行到第 45 行: int length; // Strzng length char*sPtr; // pointer to start of string 声明了类的 private 数据成员。String 的对象有一个 length 字段(表示字符串中除字符 串终止符以外的字符个数)和一个指向动态分配内存(表示字符串)的指针 sPtr。 现在分析一下图 2.5 中定义 String 类的头文件。下面的两行代码(第 9 行到第 10 行): friend ostream &operator<<( ostream &,const String &); friend istream &operator>>( istream &, String & ); 把重载的流插入运算符函数 operator<<(第 180 行定义)和流读取运算符函数 operator>>(第 187 行定义)声明为类的友元。这两个函数的实现是显而易见的。 第 13 行: String(const char * = "");// conversion/default ctor 声明了一个转换构造函数,该构造函数(第 60 行定义)有一个 const char*类型的参数 (默认值是空字符串)。该函数实例化了 String 的一个对象,该对象包含了与参数相同的字 符串。任何只带一个参数的构造函数都可以认为是一种转换构造函数。稍后就会看到,当 使用 char*参数对 String 类做任何操作时,转换构造函数是很有用的。转换构造函数把一 个 char*字符串转换为 String 的对象(然后该对象要赋给目标 String 对象)。使用这种转换 构造函数意味着不必再为将字符串赋给 String 的对象提供重载的赋值运算符,编译器先自 动地调用该函数建立一个包含该字符串的临时 String 对象, 然后再调用重载的赋值运算符将临时 String 对象赋给另一个 String 对象。 软件工程视点 2. 7 当使用转换构造函数实现隐式转换时,C++只会使用一个隐式的构造函数调用来试 70 图满足重载赋值运算符的需要。通过执行一系列隐式的、用户自定义的类型转换来满足重 载运算符的需要是不可能的。 在做出像 String s1("happy")这样的声明时,调用 String 的转换构造函数。转换构造 函数计算了字符串的长度并将该长度赋给 private 数据成员 length,然后调用 private 工 具函数 setString。函数 setString(第 172 行定义)使用 new 为 private 数据成员 sPtr 分配 足够的空间,并用 assert 来测试内存分配操作是否成功。如果成功,则用函数 strcpy 把 字符串复制到对象中。 第 14 行: String(const String &); // copy constructor 是一个复制构造函数(第 67 行定义),它通过复制已存在的 String 对象来初始化一个 String 对象。必须要小心对待这种复制操作,避免使两个 String 对象指向同一块动态分 配的内存区,默认的成员复制更容易发生这种问题。复制构造函数除了将源 String 对象 的 length 成员复制到目标 String 对象外,其余操作和转换构造函数类似。注意,复制构 造函数为目标对象的内部字符串分配了新的存储空间,如果它只是简单地将源对象中的 sPtr 复制到目标对象的 sptr,则这两个对象将指向同一块动态分配的内存块。执行一个 对象的析构函数将释放该内存块,从而使另一个对象的 sPtr 没有定义,这种情况可能会 引起严重的运行时错误。 第 15 行: ~String(); // destructor 声明了类 String 的析构函数(第 74 行定义)。该析构函数用 delelte 回收构造函数中用 new 为字符串分配的动态内存。 第 16 行: const String &operator=(const String &); // assignment 声明了重载的赋值运算符函数 operator=(第 81 行定义)。当编译器遇到像 string1=string2 这样的表达式时,就会生成函数调用: string1.operator=(string2); 重载的赋值运算符函数 operator 测试了这种赋值是否为自我赋值(正如在复制构造函数中 所做的那样)。如果是自我赋值运算,由于该对象已存在,函数就简单地返回。如果忽略自 我赋值测试,那么函数就会立即释放目标对象所占用的空间,这样会丢失字符串。假如不 是自我赋值,那么函数就释放目标对象所占用的内存空间,将源对象中的 length 字段复 制到目标对象并调用 setString(第 172 行)为目标对象建立新空间,用 assert 测试 new 操 作是否成功,最后用函数 strcpy 将源对象的字符串复制到目标对象中。不管上述赋值是 否为自我赋值,函数都返回*this 以确保可以连续赋值。 第 17 行: const String &operator+=( const String & ); // concatenation 声明了重载的字符串连接运算符(第 98 行定义)。当编译器遇到 main 函数中的表达式 s1+=s2 时,生成函数调用 s1.operator+=(s2)。函数 operator+=建立一个临时指针,该指针 用来存放当前对象的字符串指针,直到可以撤消该字符串的内存为止,该函数还计算了 连接后的字符串长度,用 new 为字符串分配空间,用 assert 测试 new 操作是否成功。如果 成功,则用函数 strcpy 将原先的字符串复制到分配的空间中,然后用函数 strcat 将源对 象的字符串连接到所分配的空间中,最后再用 delele 释放该对象原来的字符串占据的空 间,返回*this 作为 String&以确保运算符+=可以连续执行。 连接 String 类型的对象和 char*类型的对象不需要再重载一个连接运算符,const 71 char*转换构造函数将传统的字符串转换为临时的 String 类型的对象,然后由该对象匹配 现有的重载连接运算符。C++为实现匹配只能在一层之内执行这样的转换。在执行内部类型 和类之间的转换前,C++还能在内部类型之间执行编译器隐式定义的类型转换。注意,生 成临时 String 对象时,调用转换构造函数和析构函数(见图 2.5 中 s1 += "to you" 产生 的输出)。这是隐式转换期间生成和删除临时类对象时向类客户隐藏的函数调用开销的一 个例子。复制构造函数按值调用传递参数和按值返回类对象时也 产生类似开销。 性能提示 2.2 与先执行隐式类型转换然后再执行连接操作相比,使重载的连接运算符+=只有一个 const char*类型参数的执行效率更高。隐式类型转换需要较少的代码,出错也较少。 第 18 行: bool operator!()const; // is String empty? 声明了重载的取非运算符(第 111 行定义)。该运算符通常与字符串类一起使用,测试字符串 是否为空。例如,当编译器遇到表达式!string1 时,就会生成函数调用: strlng1.operator!() 该函数仅仅返回 length 是否等于 0 的测试结果: 代码行: bool operator ==( const String & ) cOnst; // test s1 == s2 bool operator<( const String & ) const; // test s1 < s2 为类 String 声明了重载的相等运算符(第 114 行定义)和关系运算符(第 ll8 行定义)。其工作 原理是相似的,因此我们只以重载的运算符==为例。当编译器遇到表达式 string1==string2 时,就会生成如下的函数调用: string1.operator==(string2) 如果 string1 等于 string2,则返回 true。上述运算符都用函数 strcmp 比较 String 对象中 的字符串。注意我们使用 C 语言标准库中的函数 strcmp。许多 C++程序员提倡用一些重载 运算符函数实现另外一些重载运算符函数,因此!=、>、<=和>=运算符都可以用 operator== 和 operator<实现(第 23 行到第 36 行)。例如,重载函数 operator>=在头文件中的实现(第 33 行)如下所示: bool String::operator>=(const String &right) const { return ! ( *this=定义用重载的运算符<确定一个 String 对象是否大于或等于另一个 String 对象。注意!=、>、<=和>=运算符函数都在头文件中定义。编译器将这些定义内联起来, 消除多余函数调用的开销。 软件工程视点 2. 8 通过用前面定义的成员函数实现成员函数,程序员复用代码,从而减少要编写的代 码量。 第 38 行到第 39 行: char &operator[](int); // subscript operator const char &operator[](int) const; // subscript operator 声明了重载的下标运算符(在第 122 行和第 131 行定义)。一个用于 const String,一个用于 72 非 const String。当编译器遇到 string1[O]这样的表达式时,就会生成函数调用 string1,operator[](O)(根据 String 是否为 const 类型而使用相应的 operator[]版本)。函 数 operator[]首先用 assert 检查下标范围。如果下标越界,则打印一个出错信息井使程序 异常中止。如果下标没有越界,则非 const 版本的 operator[]返回一个 char&类型的值, 它是对 String 对象相应字符的引用,可用作左值,修改 String 对象中指定的字符。而 const 版本的 operator[]返回 String 对象的相应字符,这里 char&可以作为右值,读取该 字符值。 测试与调试提示 2.1 从 String 类的重载下标运算符返回 char 引用是危险的。例如,客户可以用这个引用 在字符串中任何位置插入 null 终止符('\0')。 第 40 行: String &operator()( int,int ); // return a substring 声明了重载的函数调用运算符(第 141 行定义)。在字符串类中,为了从 String 对象中选择 一个子串,经常要重载该运算符。两个整数参数指定了所选定子串的起始位置和长度。如 果起始位置越界或者子串长度为负,则发出错误信息。习惯上,如果子串长度为 0,则选 择的子串为从选定的开始位置一直到 String 对象的末尾。例如,假设 string1 是一个包含 字符串”AEIOU'’的 String 对象,当编译器遇到表达式 string1(2,2)时,生成函数调用 string1.operator()(2,2)。执行该函数调用时,产生一个包含串“IO”,的动态分配的 新 String 对象,并返回对该对象的引用。 因为函数可能会有一个冗长而复杂的参数表,所以重载的函数调用运算符()可以有 很强大的功能,从而可以完成很多有意义的操作。函数调用运算符的另外一个用途是用作 数组的下标符号。例如,有的程序员不愿意用 C 的两个方括号表示二维数组(如 a[b][c]), 他们更喜欢重载函数调用运算符,用 a[b][c])表示二维数组。只有当“函数名”是类 String 的对象时才能使用该运算符。 第 41 行: int getLength()const; // return string length 声明了返回 String 对象长度的函数。该函数(第 168 行定义)是通过返回类 String 的 private 数据值而获得字符串的长度。 读者现在应该深入到 main 函数的代码中,研究输出结果,了解每种重载运算符的用 法。 2.11 重载++与-- 所有四种自增和自减运算符(即前置和后置的自增及自减运算符)都可以被重载。本节 介绍编译器如何识别前置和后置的自增及自减运算符。 要重载既能允许前置又能允许后置的自增运算符,每个重载的运算符函数必须有一 个明确的特征以使编译器能确定要使用的++版本。重载前置++的方法与重载其他前置一 元运算符一样。 例如,假设要给 Date 对象 d1 增加一天,当编译器遇到前置自增表达式: ++d1 编译器就会生成成员函数调用: 73 d1.operator++() 该函数的函数原型为: Date &operator++(); 如果前置自增运算符函数是一个非成员函数,则当编译器遇到表达式: ++d1 编译器就会生成函数调用: operator++(d1) 该函数的函数原型在类 Date 中的声明形式为: friend Date &0peratOr++(Date &); 由于编译器必须能区分重载的前置和后置自增运算符函数,所以重载后置自增运算符遇 到了一点儿困难。C++中所采用的方法是,当编译器遇到后置自增表达式: d1++ 编译器就会生成成员函数调用: d1.operator++(0) 该函数的函数原型为: Date operator++(int) 严格说来,0 是一个伪值,它使运算符函数 operator++在用于后置自增操作和前置自增 操作时的参数表有所区别。 如果后置自增运算符函数是一个非成员函数,则当编译器遇到表达式: d1++ 编译器就生成函数调用: operator++(d1,0) 该函数的函数原型为: friend Date operator++(Date &,int); 再重复一遍,编辑器使用参数。区别后置自增操作和前置自增操作所用到的 operator++ 函数的参数表。 本节所讲述的重载前置和后置自增运算符的内容同样可以用来重载前置和后置自减 运算符,下一节探讨了使用重载的前置和后置自增运算符的 Date 类。 2.12 实例研究:Date 类 图 2.6 声明了类 Date。类 Date 用重载的前置和后置自增运算符将一个 Date 对象增 加 1 天,必要时使年、月递增。 类 Date 的 Public 接口提供了以下成员函数:一个重载的流插入运算符,一个默认的 构造函数、一个 setDate 函数、一个重载的前置自增运算符函数、一个重载的后置自增运算 符函数、一个重载的加法赋值运算符(+=)、一个检测闰年的函数和一个判断是否为每月最 后一天的函数。 1 // Fig. 2.6: datel.h 2 // Definition of class Date 3 #ifndef DATE1_H 4 #define DATE1_H 74 5 #include 6 7 class Date { 8 friend ostream &operator<<( ostream &, const Date & ); 9 10 public: 11 Date( int m = 1, int d = 1, int y = 1900 ); // Constructor 12 void setDate( int, int, int ); // set the date 13 Date &operator++(); // preincrement operator 14 Date operator++( int ); // postincrement operator 15 const Date &operator+=( int ); // add days,modify object 16 bool leapYear( int ); // is this a leap year? 17 bool endOfMonth( int ); // is this end of month? 18 19 private: 20 int month; 21 int day; 22 int year; 23 24 static const int days[]; // array of days per month 25 void helpIncrement(); // utility function 26 }; 27 28 #endif 30 // Member function definitions for Date class 31 #include 32 #include "date1.h" 33 34 // Initialize static member at file scope; 35 // one class-wide copy. 36 const int Date::days[] = { 0, 31, 28, 31, 30, 31,30, 37 31, 31, 30, 31, 30, 31 } ; 38 39 // Date constructor 40 Date::Date( int m, int d, int y ) { setDate( m, d, y ); } 41 42 // Set the date 43 void Date::setDate{ int mm, int dd, int yy ) 44 { 45 month = ( mm >= 1 && mm <= 12 ) ? mm : 1; 46 year = ( yy >= 1900 && yy <= 2100 ) ? yy : 1900; 47 48 // test for a leap year 49 if ( month == 2 && leapYear( year ) ) 75 50 day = ( dd >= 1 && dd <= 29 ) ? dd : 1; 51 else 52 day = ( dd >= 1 && dd <= days[ month ] ) ? dd : 1; 53 } 54 55 // Preincrement operator overloaded as a member function. 56 Date &Date::operator++() 57 { 58 helpIncrement(); 59 return *this; // reference return to create an lvalue 6O } 61 62 // Postincrement operator overloaded as a member function. 63 // Note that the dummy integer parameter does net have a 65 Date Date::operator++( int) 66 { 67 Date temp = *this; 68 helpIncrement(); 69 70 // return non-incremented, saved, temporary object 71 return temp; // value return; not a reference return 72 } 73 74 // Add a specific number of days to a date 75 const Date &Date::operator+=( int additionalDays ) 76 { 77 for ( int i = 0; i < additionalDays; i++ ) 78 helpIncrement(); 79 80 return *this; // enables cascading 81 } 82 83 // If the year is a leap year, return true; 84 // otherwise,return false 85 bool Date::leapYear( int y ) 86 { 87 if ( y % 400 == 0 || ( y % 100 != 0 && y % 4 == 0 ) ) 88 return true; // a leap year 89 else 90 return false; // not a leap year 91 } 92 93 // Determine if the day is the end of the month 94 bool Date::endOfMonth( int d ) 76 95 { 96 if ( month == 2 && leapYear( year ) ) 97 return d == 29; // last day of Feb. in leap year 98 else 99 return d == days[ month ]; 100 } 101 102 // Function to help increment the date 103 void Date::helpIncrement() 104 { 105 if ( endOfMonth( day ) && month == 12 ) { // end year 106 day = 1; 107 month = 1; 108 ++year; 109 } 110 else if ( endOfMonth( day ) ) { // end 111 day = 1; 112 ++month; 113 } 114 else // not end of month or year; increment day 115 ++day; 116 } 117 118 // Overloaded output operator 119 ostream &operator<<( ostream &output, const Date &d ) 120 { 121 static char *monthName[ 13 ] = { "", "January", 122 "February", "March", "April", "May", "June", 123 "July", "August", "September", "October", 124 "November", "December"} ; 125 126 output << monthName[ d.month ] << ' ' 127 << d.day << ", "<< d.year; 128 129 return output; // enables cascading 130 } 131 // Fig. 8.6: fig08_06.cpp 132 // Driver for class Date 133 #include 134 #include "date1.h" 135 136 int main() 137 { 138 Date d1, d2( 12, 27, 1992 ), d3( 0, 99, 8045 ); 77 139 cout << "d1 is "<< d1 140 << "\nd2 is" << d2 141 << "\nd3 is << d3 << "\n\n"; 142 143 cout << "d2 + 7 is "<< ( d2 += 7 ) << "\n\n"; 144 145 d3.setDate( 2, 28, 1992 ); 146 cout <<" d3 is "<< d3; 147 cout << "\n++d3 is" << ++d3 << "\n\n"; 148 149 Date d4( 3, 18, 1969 ); 150 151 cout << "Testing the preincrement operator:\n" 152 <<" d4 is" << d4 << '\n'; 153 cout << "++d4 is "<< ++d4 << '\n'; 154 cout <<" d4 is << d4 << "\n\n"; 155 156 cout << "Testing the postincrement operator:\n" 157 <<" d4 is "<< d4 << '\n'; 158 cout << "d4++ is " << d4++ << '\n'; 159 cout << "d4 is " << << d4 << endl; 160 161 return O; 162 } 输出结果: dl is January 1, 1900 d2 is December 27, 1992 d3 is January 1,1900 d2 += 7 is January 3,1993 d3 is February 28, 1992 ++d3 is February 29, 1992 Testing the preincrement operator: d4 is March 18,1969 ++d4 is March 19, 1969 d4 is March 19, 1969 Testing the preincrement operator: d4 is March 19, 1969 d4++ is March 19, 1969 d4 is March 20, 1969 78 图 2.6 重载自增运算符的 Date 类 main 函数中的驱动程序建立了几个日期对象,包括:初始化为 1990 年 1 月 1 日的 d1,初始化为 1992 年 12 月 27 日的 d2 以及初始化为一个非法日期的 d3。Date 的构造函数 调用函数 setDate 检测月、日和年的合法性。如果月是非法的则置为 1,年是非法的则置为 1900,日是非法的则置为 1。 驱动程序用重载的流插入运算符输出所建立的每一个 Date 对象。程序用重载的运算 符+=将对象 d2 增加 7 天,用函数 setDate 将对象 d3 设置为 1992 年 2 月 28 日,接着将一 个新对象 d4 设置为 1969 年 3 月 18 日并用重载的前置自增运算符将 d4 增加 1 天。为证实执 行过程的正确性,在执行前置自增操作的前后分别输出了日期。最后,用重载的后置自增 运算符将对象 d4 增加一天。为了证实执行的过程的正确性,在执行后置自增操作的前后 也分别输出了日期。 重载前置自增运算符是简明直接的,前置自增运算符调用 private 工具函数 helplncrement 来执行实际的自增运算。函数 helpIncrement 必须要处理日期的边界情况, 因为对某月的日期加 1 时,它可能已经达到了最大值,此时需要将月份加 1 并把日期置为 1,如果月份已经是 12,则必须将年份加 1 而月份置为 1。函数 helpIncrement 使用函数 leapYear 和 endofMonth 正确地递增日期。 重载的前置自增运算符返回对当前对象 Date(已自增)的引用。这是因为当前对象的 *this 作为 Date&而返回。 重载后置自增运算符需要一点儿技巧。为模拟后置操作,函数必须返回该 Date 对象 未自增的副本。在进入 operator++时,函数先把当前对象(*this)保存在 temp 中,然后调 用 helpIncrement 递增当前的 Date 对象,最后返回未递增的对象在 temp 中的副本。注意 这个函数不能返回对局部 Date 对象 temp 的引用,因为声明该对象的函数退出时删除了 局部变量。这样,声明这个函数的返回类型为 Date&将返回不复存在的对象的引用。返回 局部变量的引用是个常见的错误,一些编译器会发出警告。 小 结 ●运算符<<在 C++中有多种用途,既可以用作流插入运算符又可以用作左移位运算 符,这是运算符重载的一个范例。同样,运算符>>也是 C++中的一个重载运算符,它既可 以用作流读取运算符,也可以用作右移位运算符。 ●为了使运算符在不同的上下文中具有不同的含义,C++允许程序员重载大多数运算 符。编译器根据运算符的使用方式产生合适的代码。 ●运算符重载提高了 C++的可扩展性。 ●运算符重载是通过编写函数定义实现的。函数名是由关键字 operator 和其后要重 载的运算符符号组成的。 ●用于类的对象的运算符必须重载,但是有两种例外情形。对于相同类的两个对象使 用赋值运算符而不用重载,默认的方式是复制数据成员。地址运算符(&)也无需重载就可 以用于任何类的对象,它返回对象在内存中的地址。 ●C++为其内部类型提供了的丰富的运算符集,重载这些运算符的目的是为用户自定 义的类型提供同样简洁的表达式。 ●重载不能改变运算符的优先级和结合律。 ●重载不能改变运算符操作数的个数。重载的一元运算符仍然是一元运算符,重载的 79 二元运算符仍然是二元运算符。C++惟一的三元运算符(?:)不能被重载。 ●不能建立新的运算符符号,只有现有的运算符才能被重载。 ●运算符重载不能改变该运算符用于内部类型的对象时的含义。 ●在重载运算符()、[]、->,或者=时,运算符重载函数必须声明为类的一个成员。 ●运算符函数既可以是成员函数,也可以是非成员函数。 ●当运算符函数是一个成员函数时,最左边的操作数必须是运算符类的一个类对象 (或者对该类对象的引用)。 ●如果左边的操作数必须是一个不同的类的对象,该运算符函数必须作为一个非成 员函数来实现。 ●只有当二元运算符的最左边的操作数是该类的一个对象或者当一元运算符的操作 数是该类的一个对象时,才会调用运算符成员函数。 ●选择非成员函数重载运算符的另外一个原因是使运算符具有可交换性。例如,绐定 正确的重载运算符定义,运算符左边的参数可以是其他数据成员的对象。 ●类的一元运算符可重载为一个没有参数的非 static 成员函数或者带有一个参数的 非成员函数,参数必须是用户自定义类型的的对象或者对该对象的引用。 ●二元运算符可以重载为带有一个参数的非 static 成员函数或者带有两个参数的非 成员函数(参数之一必须是类的对象或者是对类的对象的引用)。 ●数组下标运算符不仅仅可用于数组,还可以用于从其他各种顺序容器类(如链表、 字符串、字典等等)中选择元素。此外,下标不仅仅可以是整数,还可以是字符或者字符串 等等。 ●复制构造函数根据同类中的其他对象初始化一个对象。不论何时需要复制对象时都 会调用复制构造函数,例如在按值调用时,或是从被调用函数返回值时。在复制构造函数 中,被复制的对象是通过引用传递的。 ●编译器不知道怎样实现用户自定义类型和内部类型之间的转换,程序员必须明确 地指明如何进行转换。这种转换可以用转换构造函数实现(即带有单个参数的构造函数), 这种函数仅仅把其他类型的对象转换为某个特定类的对象。 ●转换运算符(又称为强制类型转换运算符)可以把一种类的对象转换为其他类的对象 或内部类型的对象。这种运算符必须是一个非 static 成员函数,而不能是友元函数。 ●转换构造函数是带有一个参数的构造函数,用来把参数转换为构造函数所在类的 对象 c 编译器可隐式调用这种构造函数。 ●赋值运算符是最常用的重载运算符,通常用来把一个对象赋给相同类的另外一个 对象。通过使用转换构造函数,赋值运算符也能够使不同类的对象之间相互赋值。 ●在不提供重载的赋值运算符时,赋值运算符的默认行为是复制类的数据成员。在有 些情况下这是允许的,但是当对象中包含指向动态分配的内存区的指针时,成员复制会 导致两个不同的对象指向同一块动态分配的内存区。这样,调用其中一个对象的析构函数 将释放该动态分配的内存块,如果另一个对象引用该内存区,其结果是不确定的。 ●要重载既能允许前置又能允许后置的自增运算符,每个重载的运算符函数必须有 一个明确的特征,以使编译器能确定要使用的++版本。重载前置++的方法与重载其他前 置一元运算符一样。向后置自增运算符函数提供第二个参数(必须是 int 类型)达到了把前 置和后置自增运算符函数区分开来的目的。实际上,用户并没有给该特定的整数参数提供 值,它仅仅是让编译器区分前置和后置自增运算符函数。 80 第三章 继承 教学目标 ●能通过继承现有的类建立新类 ●了解继承是如何提高软件的可复用性 ●了解基类和派生类的概念 ●能够用多重继承从多个基类派生出新类 3.1 简介 本 章 和 下 一 章 要 讨 论面 向 对 象 的 程 序 设 计 的 两个极其重 要 的功能 — —继 承 (inheritance)和多态性(polymorphism)。继承是软件复用的一种形式,实现这种形式的方 法是从现有的类建立新类。新类继承了现有类的属性和行为,并且为了使新类具有自己所 需的功能,新类还要对这些属性和行为予以修饰。软件复用缩短了程序的开发时间,促使 开发人员复用已经测试和调试好的高质量的软件, 减少了系统投入使用后可能出现的问题。所有这些都是激动人心的。利用多态性可以 编写出对现有的各种类和将要实现的类予以加工的程序。继承和多态性是处理复杂软件的 一种很有效的技术。 在建立一个新类时,程序员可以让新类继承预定义基类(baseclass)的数据成员和成 员函数,而不必重新编写新的数据成员和成员函数。这种新类称为派生类(derivedclass)。 派生类本身也可能会成为未来派生类的基类。对于单一继承,派生类只有一个基类。对于 多重继承,派生类常常是从多个基类派生出来的,这些基类之间可能毫无关系。单一继承 比较简单,我们介绍几个例子,使读者能很快成为专家。多重继承更复杂,也更容易出错, 因此我们只显示简单的例子,建议读者在进一步深造之后再利用这种功能。 派生类通常添加了其自身的数据成员和成员函数,因而通常比基类大得多。派生类比 基类更具体,它代表了一组外延较小的对象。对于单一继承,派生类和基类有相同的起源。 继承的真正魅力在于能够添加基类所没有的特点以及取代和改进从基类继承来的特点。 C++提供三种继承:public、protected 和 private。本章以介绍 public 为主.附带介 绍另外两种。第三种形式是 protected 继承,是 C++中的新生事物,用得还不多。在 public 继承中,派生类的对象也是其基类的对象,但是反过来基类对象不是其派生类的对象。本 章要利用“派生类对象是基类的对象”这一关系完成一些有趣的操作。例如,把各种不同 但又通过继承而相关的对象连成基类对象的链表,它允许用通常的方法处理各种对象。在 本章和下一章中就会看到,这是面向对象程序设计的一种重要的功能。 本章介绍了一种新的成员访问控制形式,即 protected 访问。派生类及其友元允许访 问 protected 基类成员,而其他函数则不行。 开发软件系统的经验表明,软件系统中的大部分代码都是处理紧密相关的特殊情况。由 于设计者和程序员的精力都集中于这些特殊情况,因而很难在这种系统中看到“大手 笔”的程序作品。面向对象的程序设计提供了几种“见树木而知森林”的方法,有时把这 个过程称为抽象。 如果一个程序中有多种密切相关的特殊情况,通常的做法是用 switch 语句来区分各 81 种特殊情况,然后对每种情况提供处理逻辑。第 10 章将讲述如何通过继承和多态性用更 简单的逻辑取代 switch 逻辑。 我们要区别“是一个对象”和“有一个对象”的差别(以下简称“是”关系和“有” 关系)。“是”关系是一种继承,在“是”关系中,派生类的对象也以作为基类的对象处 理。而“有”关系是一种复合,在这种关系中,一个类的对象拥有作为其成员的其他类的 对象。 派生类不能访问其基类的 private 成员,否则会破坏基类的封装性。但是,派生类能 够访问基类的 public 成员和 protected 成员。基类中不应该让派生类通过继承而访问的成 员要在基类中声明为 privale。派生类只能通过基类 public 和 protected 接口提供的访问 函数访问基类的 private 成员。 继承存在的一个问题是派生类会继承它无需拥有或者不应该拥有的基类 public 成员 函数。基类中不适合于派生类的成员可以在派生类中重新加以定义。有些情况下,不适合 用 public 继承。 继承所最具有吸引力的特点是新类可以从现有的类库中继承。项目开发者可以开发出 自己的类库,也可以利用已广为使用的类库。基于这种观点,将来有一天,软件也可以像 当今的硬件一样用标准的可复用组件进行构造。未来需要功能更强的软件,软件的这种开 发方式正可以迎接这种挑战。 3.2 继承:基类和派生类 一个类的对象经常会是另一个类的对象。例如,矩形当然是四边形(正方形、平行四边 形和梯形也是这样),因此可以说矩形类 Rectangle 是从四边形类 Quadrilateral 继承而 来的。在本例中,类 Quadrilateral 叫做基类,类 Rectangle 称为派生类。矩形是四边形的 一种特殊类型,但是要说四边形是矩形则是不正确的。图 9.1 示例了几个简单的继承例 子。 基类 派生类 student GraduateStudent UndergraduateStudent Shape Circle Triangle Rectangle Loan CarLoan HomeIpprovementLoan MoregageLoan Employee FacultyMember StaffMember Account CheckingAccount SavingsAccount 图 3.1 几个简单的继承例子 其他的面向对象程序设计语言使用了不同的术语。例如,在继承方面,smslltalk 语 言把基类类称为超类,派生类叫做子类。 因为由继承而产生的派生类通常比基类大,所以超类和子类这样的术语似乎是不合适的, 82 本书没有使用这些术语。由于派生类对象可以看成基类的对象,因此基类有更多相关对象, 而派生类的相关对象更少,因此可以把基类理解为“超类”,派生类理解为“子类”。 继承形成了树状层次结构,基类和它的派生类构成了一种层次关系。一个类可以单独 存在,但是当利用继承机制使用该类时,该类就成为给其他类提供属性和行为的基类, 或者成为继承其他类的属性和行为的派生类。 下面是一个简单的继承层次结构。一个典型的大学社区有成千上万个人,他们是社区 的成员。这些人由雇员(employee)和学生(student)组成。雇员又分为学院成员(faculty)和职 员(staff),学院成员既可能是校长和系主任等等的管理者(administrator),也可能是教 员(teacher)。这种关系构成的继承层次结构如图 9.2 所示。注意有些行政人员也任了课, 因此我们用多重继承构成 AdministratorTeacher 类。由于学生常常在学校打工,职工也 常常去修课,因此还可以用多重继承构成 EmployeeStudent 类。 图 3.2 大学社区成员的继承层次结构 另外一个实际存在的继承层次结构是像图 3.3 那样的 shape 层次结构。初次学习面 向对象程序设计的学生都认为现实世界中存在着大量具有层次结构的实例,也正因为如 此,这些学生从来没有认真思考过现实世界中的这种层次结构是如何分门别类的,所以 应该在这方面好好思考一下。 图 3.3 类 shape 的部分层次结构 为了说明类 CommissionWorker 是从类 Employee 派生而来的,类 CommissionWorker 通常 要作如下形式的定义; class CommissionWorker:public Employee{ 上述继承方法称为 public 继承(public inheritance),这种类型的继承是最常用的。本 章还要讨论 private 继承(privateinheritane)和 protected 继承(protectedinheritance)。 对于 public 继承来说,基类的 public 成员和 protected 成员可以分别作为派生类的 public 成员和 protected 成员而被继承。 用类似的方法处理基类对象和派生类对象是 可能的。基类的属性和行为表述了基类对象及派生类对象的共性。从基类 public 派生出来 的所有对象都可以作为基类对象处理。我们将研究很多例子。在这些例子中,我们可以利 用这种关系很容易地设计程序,而非面向对象的语言(如 C 语言)就做不到这一点。 3.3 protected 成员 基类的 public 成员能够被程序中所有函数访问,private 成员只能被基类的成员函 数和友元访问。protected 访问是 public 访问和 private 访问之间的中间层次。基类的 protected 成员只能被基类的成员和友元以及派生类的成员和友元访问。派生类成员简单 83 地使用成员名就可以引用基类的 public 成员和 protected 成员。注意 protected 数据破坏 了封装,基类 protected 成员改变时,所有派生类都要修改。 软件工程视点 3. 1 一般来说,声明 private 类的数据成员和使用 Protected 方式只能是系统要满足特定 性能要求时的“最后一招”。 3.4 把基类指针强制转换为派生类指针 公有派生类的对象可作为其相应基类的对象处理,这使得一些有意义的操作成为可 能。例如,从某个特定基类派生出来的各种类,尽管这些类的对象彼此之间互不相同,但 是仍然能够建立这些对象的链表,只要把这些对象作为基类对象处理就可以了。然而反过 来是不行的,基类的对象不能自动成为派生类的对象。 常见编程错误 3.1 将基类对象作为派生类对象处理。 程序员可以用显式类型转换把基类指针强制转换为派生类指针。但是,如果要复引用 该指针,那么在转换前首先应该把它指向某个派生类的对象,这一点要小心。本节采用大 多数编译器中常用的方法。 常见编程错误 3.2 把指向基类对象的指针显式地强制转换为派生类指针,然后引用该对象中并不存在 的派生类的成更会导 致运行时的逻辑错误。 第一个例子见图 3.4。第 1 行到第 39 行是类 Point 的定义和其成员函数的定义,第 40 行到第 94 行是类 Circle 的定义和其成员函数的定义,第 95 行到第 132 行是类的驱动 程序,该程序演示了如何把派生类指针赋给基类指针和如何把基类指针强制转换为派生 类指针。余下的部分是程序输出。 首先看一下类 Point 的定义。Point 的 public 接口包含成员函数 setPoint、getX 和 getY。Point 的数据成员 x 和 y 指定为 protected,从而在防止了 Point 对象的用户直接访 问这些数据的同时,又能够让派生类直接访问继承来的数据成员。如果将数据成员指定为 private,那么就要用 Point 的 public 成员函数甚至派生类来访问这些数据。注意,由于 重载的流插入运算符函数是类 Point 的友元,所以 Point 重载流插入函数能够直接引用 变量 x 和 y。因为重载的流插入运算符函数不是类 Point 的成员函数,所以需要通过对象 来引用变量 x 和 y(即 p.x 和 p.y)。注意这个类提供内联的 publiic 成员函数 getX 和 getY,因此 operator<<不必成为友元就可以达到良好性能。但所需 public 成员函数不 一定在每个类的 public 接口中提供,因此最好还是建立友元。 1 // Fig. 3.4: point.h 2 // Definition of class Point 84 3 #ifndef POINT H 4 #define POINT H 5 6 class Point { 7 friend ostream &operator<<( ostream &, const Point & ); 8 public: 9 Point( int = 0, int = 0 ); // default constructor 10 void setPoint( int, int ); // set coordinates 11 int getX() const { return x; } // get x coordinate 12 int getY() const/{/ return y; } // get y coordinate 13 protected: accessible by derived classes 14 int x, y; // x and y coordinates of the Point 15 }; 16 17 #endif 18 // Fig. 3.4: point.cpp 19 // Member functions for class Point 20 #include 21 #include "point.h" 22 23 // Constructor for class Point 24 Point::Point( int a, int b ) { setPoint( a, b ); } 25 26 // Set x and y coordinates of Point 27 void Point::setPoint{ int a, int b ) 28 { 29 30 y = b; 31 } 32 33 // output Point (with overloaded stream insertion operator) 34 ostream &operator<<{ ostream &output, const Point &p ) 35 { 36 output << '[' << p.x << ", "<< p.y << ']'; 37 38 return output; // enables cascaded calls 39 } 40 // Fig. 3.4: circle.h 41 // Definition of class Circle 42 #ifndef CIRCLE_H 43 #define CIRCLE_H 44 45 #include 46 #include 85 47 #include "point.h" 48 49 class Circle : public Point { // Circle inherits from Point 50 friend ostream &operator<<( ostream &, const Circle & ); 51 public: 52 // default constructor 53 Circle( double r = 0.0, int x = O, int y = 0 ); 54 55 void setRadius( double ); // set radius 56 double getRadius() const; // return radius 57 double area() const; // calculate area 58 protected: 59 double radius; 60 }; 61 62 #endif 63 // Fig. 3.4:circle.cpp 64 // Member function definitions for class Circle 65 #include "circle.h" 66 67 // Constructor for Circle calls constructor for Point 68 // with a member initializer then initializes radius. 69 Circle::Circle( double r, int a, int b ) 70 : Point( a, b ) // call base-class constructor 71 { setRadius( r ); } 72 73 // Set radius of Circle 74 void Circle::setRadius( double r ) 75 { radius = ( r >= O ? r : 0 ); } 76 77 // Get radius of Circle 78 double Circle::getRadius() const { return radius; } 79 80 // Calculate area of Circle 81 double circle::area() const 82 { return 3.14159 * radius * radius; } 83 84 // Output a Circle in the form: 86 ostream &operator<<( ostream &output, const Circle &c ) 87 { 88 output << "Center =" << static cast< Point >( c ) 89 << "; Radius =" 90 << setiosflags( ios::fixed | ios::showpoint ) 91 << setprecision( 2 ) << c.radius; 86 92 93 return output; // enables cascaded calls 94 } 95 // Fig. 3.4:fig09 04.cpp 96 // Casting base-class pointers to derived-class pointers 97 #include 98 #include 99 #include "point.h" 100 #include "circle.h" 101 102 int main() 103 { 104 Point *pointPtr = 0, p( 30, 50 ); 105 Circle *circlePtr = 0, c( 2.7, 120, 89 ); 106 107 cout << "Point p: "<< p << ,\nCircle C: "<< c << '\n'; 108 109 // Treat a Circle as a Point (see only the base class part) 110 pointPtr = &C; // assign address of Circle to pointPtr 111 cout << "\nCircle C (via *pointPtr):" 112 << *pointPtr << '\n'; 113 114 // Treat a Circle as a Circle (with some Casting) 115 pointPtr = &C; // assign address of Circle to pointPtr 116 117 // cast base-class pointer to derived-class pointer 118 circlePtr = static cast< Circle * >( pointPtr ); 119 cout << "\nCircle C (via *circlePtr):\n" << *circlePtr 120 << "\nArea of C (via circlePtr):" 121 << circlePtr->area() << '\n'; 122 123 // DANGEROUS: Treat a Point as a Circle 124 pointPtr = &p; // assign address of Point to pointPtr 125 126 // cast base-class pointer to derived-class pointer 127 circlePtr = static_cast< Circle * >( pointPtr ); 128 cout << "\nPoint p (via *circlePtr):\n" << *circlePtr 129 << "\nArea of object circlePtr points to:" 130 << circlePtr->area() << endl; 131 return 0; 132 } 输出结果: Point p: [ 30, 50] 87 Circle c: Center = [ 120, 89]; Radius = 2.70 Circle c(via *circlePtr):[ 120,89 ] Circle c( via *circlePtr ): Center = [ 120,89] ;Radius = 2.70 Area of c (via circlePtr): 22.90 oint p( via *circlePtr ): Center = [ 30, 50]; Radius = 0.00 Area of object circlePtr points to: 0.00 图 3.4 把基类指针强制转换为派生类指针 类 Circle 继承了类 Point,类定义的第一行指定了这种继承是 public 继承: class Circle : public Point { // Circle inherits from Point Point 和 Circle 重载的流插入运算符输出了这两个对象的信息。然后,驱动程序将派生类 指针(对象 c 的地址)赋绐基类指针 pointPtr 并用 Point 的 operator<<输出 Circle 的对象 c,并复引用指针*pointPtr。 注意只显示 Circle 对象 c 的 Point 部分。对 public 继承,总是可以将派生类指针赋给基类, 因为派生类对象也是基类对象。基类指针只“看到”派生类对象的基类部分。编译器进行 派生类指针向基类指针的隐式转换。 随后,程序将派生类指针(对象 c 的地址)赋给基类指针 pointPtr,并将 pointPtr 强 制转换回 Circle*类型,强制转换后的结果赋给指针 circlePtr。使用 Circle 重载流插入运 算符输出 Circle 的对象 c 并复引用指针*circlePtr。然后通过指针 circlePtr 输出 Circle 对象 c 的面积。因为该指针一直指向 Circle 对象,所以输出了该对象的合法面积值。 因为把基类指针直接赋给派生类指针蕴含着危险性,所以编译器不允许这么做,也 不执行隐式转换。使用显式类型转换是告诉编译器程序员已经知道了这种危险性。正确地 使用指针是程序员的责任,因此编译器允许有危险的转换。 接着,程序演示了将基类指引 (对象 p 的地址)赋给基类指针 pointPtr,并将 pointPtr 强制转换为 Circle*类型,强制转换操作的结果赋给了 circlePtr。Point 对象 p 用 Circle 的 operator<<输出,并复引用指针*circlePtr。注意半径元素输出为 0(实际上不 存在,因为 circlePtr 实际上针对 Point 对象)。将 Point 作为 Circle 输出就会导致 radius 为未定义的值(这里刚好为 0),因为指针总是指向 Point 对象。Point 对象没有 radius 成员, 因此输出 circlePtr 所指 radius 数据成员内存地址中的值。circlePtr 所指对象的面积 (Point 对象 P)也是通过 circlePtr 输出。注意面积值为 0.00,这是根据 radius“未定义” 的值算出的。显然,访问不存在的数据成员是很危险的。调用不存在的成员函数可能使程 序崩溃。 本节介绍指针转换的机制。为下一章介绍多态与面向对象编程打下了基础。 88 3.5 使用成员函数 当从基类派生出一个派生类时,派生类的成员函数可能需要访问基类的某些成员函 数。 软件工程视点 3. 2 派生类不能直接访问其基类的 private 成员。 这是 C++中关键的软件工程视点。如果派生类能访问其基类的 private 成员,那么就 会破坏基类的封装性。隐藏 private 成员有助于测试、调试和正确地修改系统。如果派生类 能访问其基类的 private 成员,那么从派生类派生出的类也应该能访问这些成员,这样 就会传递对 private 数据的访问权,从而使封装所带来的益处在整个类层次上损失殆尽。 3.6 在派生类中重定义基类成员 派生类可以通过提供同样签名的新版本(如果签名不同,则是函数重载而不是函数重 定义)重新定义基类成员函数。派生类引用该函数时会自动选择派生类中的版本。作用域运 算符可用来从派生类中访问基类的该成员函数的版本。 常见编程错误 3.3 派生类中重新定义基类的成员函数时,为完成某些附加工作.派生类版本通常要调 用基类中的该函数版本。不使用作用域运算符会由于派生类成员函数实际上调用了自身而 引起无穷递归。这样会使系统用光内存,是致命的运行时错误。 考 察 一 个 简 单 的 类 Employee , 它 存 储 雇 员 的 姓 ( 成 员 firstName) 和 名 ( 成 员 lastName)。这种信息对于所有雇员(包括 Employee 的派生类的雇员)是很普遍的。现在假设 从雇员类 Employee 派生出了小时工类 HourlyWorker、计件工类 PieceWorker、老板类 Boss 和销售员类 CommissionWorker。小时工每周工作 40 小时,超过 40 小时部分的报酬是平时 的 1.5 倍;计件工是按生产的工作计算报酬的,每件的报酬是固定的,假设他只生成一 种类型的工件,因而类 PieceWorker 的 private 数据成员是生产的工件数量和每件的报酬; 老板每周有固定的薪水;销售员每周有小部分固定的基本工资加上其每周销售额的固定 百分比。为简单起见,此处只研究类 Empbyee 和派生类 HourlyWorker。 本章的第二个例子见图 3.5。第 1 行到 47 行分别是类 Employee 的定义和其成员函数 的定义,第 48 行到 94 行分别是类 HoudyWorker 的定义和其成员函数的定义,第 95 行到 结束是类继承层次 Employee/HourlyWorker 的驱动程序,该程序很简单,仅仅建立并初 始化了类 HourlyWorker 的对象,然后调用类 HourlyWorker 的成员函数 print 输出对象的 数据。 1 // Fig. 3.5: employ.h 2 // Definition of class Employee 3 #ifndef EMPLOY_H 4 #define EMPLOY_H 89 5 6 class Employee { 7 public: 8 Employee( const char *, const char * ); // constructor 9 void print() const; // output first an last name 10 ~Employee(); // destructor 11 private: 12 char *firstName; // dynamically allocated string 13 char * lastName; // dynamically allocated string 14 } ; 15 16 #endif 17 // Fig. 3.5: employ.cpp 18 // Member function definitions for class Employee 19 #include 21 #include 22 #include "employ.h" 23 24 // Constructor dynamically allocates space for the 25 // first and last name and uses strcpy to copy 26 // the first and last names into the object. 27 Employee::Employee( const char *first, const char *last ) 28 { 29 firstName = new char( strlen( first ) + 1); 30 assert( firstName != 0 ); // terminate if not allocated 31 strcpy( firstName, first ); 32 33 lastName = new char( strlen( last ) + 1 ); 34 assert( lastName != 0 ); // terminate if not allocated 35 strcpy( lastName, last ); 36 } 37 38 // Output employee name 39 void Employee::print() const 40 {cout << firstName << ' ' << lastName; } 41 42 // Destructor deallocates dynamically allocated memory 43 Employee::~Employee() 44 { 45 delete [] firstName; // reclaim dynamic memory 46 delete [] lastName; // reclaim dynamic memory 47 } 48 // Fig. 3.5: hourly.h 49 // Definition of class HourlyWorker 90 50 #ifndef HOURLY_H 51 #define HOURLY_H 52 53 #include "employ.h" 54 55 class HourlyWorker : public Employee { 56 public: 57 HourlyWorker( const char*, const char*, double, double ); 58 double getPayO const; // calculate and return salary 59 void print() const; // overridden base-class print 60 private: 61 double wage; // wage per hour 62 double hours; // hours worked for week 63 }; 64 65 #endif 66 // Fig. 3.5: hourly.cpp 67 // Member function definitions for class HourlyWorker 68 #include 69 #include 70 #include "hourly.h" 71 72 // Constructor for class HourlyWorker 73 HourlyWorker::HourlyWorker(constchar*first, 74 const char *last, 75 double initHours, double initwage ) 76 : Employee( first, last ) // call base-class constructor 77 { 70 hours = initHours; // should validate 79 wage = initWage; // should validate 80 } 81 82 // Get the HourlyWorker's pay 83 double HourlyWorker::getPay() const { return wage * hours; } 84 85 // Print the HourlyWorker's name and pay 86 void HourlyWorker::print() const 87 { 88 cout << "HourlyWorker::print() is executing\n\n"; 89 Employee::print(); // call base-class print function 90 91 cout <<" is an hourly worker with pay of $" 92 << setiosflags( ios::fixed | ios::showpoint ) 93 << setprecision( 2 ) << getPay() << endl; 91 94 } 95 // Fig. 3.5: fig.09_05.cpp 96 // Overriding a base-class member function in a 97 // derived class. 98 #include 99 #include "hourly.h" 100 101 int main() 102 { 103 HourlyWorker h( "Bob", "Smith", 40.0, 10.00 ); 104 h.print(); 105 return 0; 106 } 输出结果: HourlyWorker::print() is executing Bob Smith is an hourly worker with pay of $400.00 图 3.5 在派生类中重新定义基类的成员函数 类 Employee 的定义由两个 private char*类型的数据成员(fisttName 和 lastName)和 三个成员函数(构造函数、析构函数和 print 函数)组成。构造函数接收两个字符串,并动态 分配存储字符串的字符数组。宏 assert 用来确定是否为 firstName 和 lastName 分配了内 存。如果没有,程序终止并返回一条出错信息,该信息指出了被测试的条件以及条件所在 的行号和文件。由于 Employee 的数据是 private 类型,所以只能用成员函数 print 访问数 据,函数 print 非常简单,仅仅输出雇员的姓和名。析构函数将动态分配的内存交还给系 统(防止内存泄漏)。 类 HoudyWorker 对类 Employee 的继承是 public 继承。类定义的第一行指定了这种继 承方式: class HourlyWorker:public EmPloyee HourlyWorker 的 public 接口包括 Employee 的函数 print 和 HourlyWorker 的成员函数 getPay 和 print。注意,类 HourlyWorker 定义了其自身的 print 函数(使用同样的函数原 型 Employee:print()),所以类 HourlyWorker 有权访问两个 print 函数。类 HourlyWorker 还包含用来计算雇员的每周薪水的 private 数据成员 wage 和 hours。 HourlyWorker 的构造函数用成员初始化值语法将 字符串 first 和 last 传 递 给 Employee 的构造函数,从而初始化了基类的成员,然后再初始化成员 wage 和 hours。成员 函数 getPay 用来计算 HourlyWorker 的工资。 类 HourlyWorker 的成员函数 print 重新定义 Employee 的 print 成员函数。为提供更 多的功能而在派生类中重新定义基类的成员函数是常有的事。被重新定义的函数有时候为 执行一些新任务而要调用基类中的函数版本。在本例中,派生类函数 print 调用基类 Employee 的 print 函数输出了雇员的名字(基类 print 函数是惟一能访问该类 private 数 据的函数),派生类的 print 函数输出了雇员的工资。 调用基类 print 函数的方法如下: Employee::print(); 92 因为基类函数和派生类函数的名字相同,所以必须在基类函数前使用基类名和作用域运 算符,否则将调用派生类的函数版本(即类 HourlyWorker 的 print 函数调用其自身),从 而导致无穷递归。 3.7 public、protected 和 private 继承 从一个基类派生一个类时,继承基类的方式有三种:public、protected 和 private。protected 继承和 private 继承不常用,而且使用时必须相当小心。本书中的范例 都是使用 public 继承。图 3.6 总结了每种继承中派生类对基类成员的访问性。第一列包含 基类成员的访问说明符。 基类成员的 访问说明符 继承类型 public 继承 protected 继承 private 继承 public 在派生类中为 public 在派生类中为 protected 在派生类中 为 private 可以由任何非 static 可以直接由任何非 static 可以直接由 任何非 static 成员函数、友元函数和 成员函数、友元函数访问 成员函数、友元 函数访问 非成员函数访问 protecetd 在派生类中为 proteced 在派生类中为 protected 在派生类中 private 可以直接由任何非 static 可以直接由任何非 static 可以直接由 任何非 static 成员函数访问 成员函数、友元函数访问 成员函数、友 元函数访问 private 在派生类中隐藏 在派生类中隐藏 在派生类中 隐藏 可以通过基类的 public 可以通过基类的 public 可以通过基 类的 public 或 protected 成 员 函 数 或 protected 成 员 函 数 由 或 protected 成员函数 由非 static 成员函数和 非 static 成员函数和友 由非 static 成员函数和 友元函数访问 元函数访问 友元函数访 问 图 3.6 派生类对基类成员的访问性 从 public 基类派生某个类时,基类的 public 成员会成为派生类的 public 成员,基类 的 protected 成员成为派生类的 protected 成员。派生类永远也不能直接访问基类的 private 成员,但可通过基类 public 或 protected 成员间接访问。 93 从 protected 基类派生一个类时,基类的 public 成员和 protected 成员成为派生类 的 protected 成员。从 private 基类派生一个类时,基类的 public 成员和 protected 成员 成为派生类的 private 成员(例如,函数成为工具函数),provate 和 protected 继承不是 “是”的关系。 3.8 直接基类和间接基类 基类既可能是派生类的直接基类,也可能是派生类的间接基类。在声明派生类时,派 生类的首部要显式地列出直接基类。间接基类不是显式地列在派生类的首部,而是沿着类 的多个层次向上继承。 3.9 在派生类中使用构造函数和析构函数 由于派生类继承了其基类的成员,所以在建立派生类的实例对象时,必须调用基类 的构造函数来初始化派生类对象的基类成员。派生类的构造函数既可以隐式调用基类的构 造函数,也可以在派生类的构造函数中通过给基类提供初始化值(利用了前面所讲过的成 员初始化值语法)显式地调用基类的构造函数。 派生类不继承基类的构造函数和赋值运算符,但是派生类的构造函数和赋值运算符 能调用基类的构造函数和赋值运算符。 派生类的构造函数总是先调用其基类构造函数来初始化派生类中的基类成员。如果省 略了派生类的构造函数,那么就由派生类的默认构造函数调用基类的默认构造函数。析构 函数的调用顺序和调用构造函数的顺序相反,因此派生类的析构函数在基类析构函数之 前调用。 软件工程视点 3.3 假设生成派生类对象,基类和派生类都包含其他类的对象,则在建立派生类的对象 时,首先执行基类成员对象的构造函数,接着执行基类的构造函数,以后执行派生类的 成员对象的构造函数,最后才执行派生类的构造函数。析构函数的调用次序与调用构造函 教的次序相反。 软件工程视点 3. 4 建立成员对象的顺序是对象在类定义中的声明顺序。成员初始化值的顺序不影响建立 对象的顺序。 软件工程视点 3. 5 对继承关系而言,基类构造函数的调用顺序是派生类定义中指定的继承顺序,派生 类成员初始化值列表中指定的基类构造函数的顺序不影响对象的建立顺序。 图 3.7 中的程序演示了基类和派生类的构造函数及析构函数的调用顺序。程序的第 1 行到第 35 行是一个简单的 Point 类,包含一个构造函数、一个析构函数以及 protected 数 94 据成员 x 和 y。构造函数和析构函数打印了调用它们的 Point 对象的信息。 第 36 行到第 72 行是一个简单的 Circle 类,它是通过 public 继承从 Point 类派生出 来的。类 circle 包含一个构造函数、一个析构函数以及 Private 数据成员 radius。构造函数 和析构函数打印了调用它们的 Circle 对象的信息。为初始化基类的数据成员 x 和 y, Circle 的构造函数用成员初始化值语法和传递变量 a 和 b 的值调用 Point 类的构造函数。 第 73 行到最后是层次结构 Point/Circle 的驱动程序。程序首先在 main 函数内实例化 了一个 Point 对象。由于该对象在进入其范围后又立即退出其范围,所以调用了 Point 的构造函数和析 构函数。然后,程序实例化了类 Circle 的对象 circle1。这个过程调用了类 Point 的构造函 数,从而输出了类 Circle 的构造函数传递给它的值,随后再输出 Circle 构造函数所指定 的输出内容。接着,程序实例化了类 Circle 的对象 circle2。这个过程同样需要调用类 Point 和 Circle 的构造函数。注意,类 Point 的构造函数在执行 Circle 构造函数之前执行。 main 函数结束时,程序为对象 circle1 和 circle2 调用析构函数。因为调用析构函数的顺 序和调用构造函数的顺序相反,所以先为对象 circle2 调用析构函数,调用顺序是调用完 类 Circle 的析构函数后,再调用类 Point 的析构函数。为对象 circle2 调用完析构函数后, 再以相同的顺序为对象 cirele1 调用析构函数。 1 // Fig. 3.7: point2.h 2 // Definition of class Point 3 #ifndef POINT2_H 4 #define POINT2_H 5 6 class Point { 7 public: 8 Point( int = O,int = 0 ); // default constructor 9 ~Point(); // destructor l0 protected: // accessible by derived classes 11 int x, y; // x and y coordinates of Point 12 }; 13 14 #endif 15 // Fig. 3.7: point2.cpp 16 // Member function definitions for class Point 17 #include 18 #include "peint2.h" 19 20 // Constructor for class Point 21 Point::Point( int a, int b ) 22 { 23 x = a; 24 y = b; 25 26 cout << "Point constructor:" 95 27 << '[' << x << ", "<< y << ']' << endl; 28 } 29 30 // Destructor for class Point 31 Point::~Point() 32 { 33 cout << "Point destructor: " 34 << '[' << x << ", "<< y << ']' << endl; 35 } 36 // Fig. 3.7: circle2.h 37 // Definition of class Circle 38 #ifndef CIRCLE2_H 39 #define CIRCLE2_H 40 41 #include "point2.h" 42 43 class Circle : public Point { 44 public: 45 // default constructor 46 Circle( double r = 0.0, int x = O, int y = 0 ); 47 48 ~Circle(); 49 private: 50 double radius; 51 }; 52 53 #endif 54 // Fig. 3.7: circle2.cpp 55 // Member function definitions for class Circle 56 #include "circle2.h" 57 58 // Constructor for Circle calls constructor for Point 59 Circle::Circle( double r, int a, int b ) 60 : Point( a, b ) // call base-class Constructor 61 { 62 radius = r; // should validate 63 cout << "Circle constructor: radius is" 64 << radius << "[" << x << ", "<< y << ']' << endl; 65 } 66 67 // Destructor roi class Circle 68 Circle::~Circle() 69 { 70 cout << "Circle destructor: radius is " 96 71 << radius << " [ " << x << ", "<< y << ']' << endl; 72 } 73 // Fig. 3.7: fig09_07.cpp 74 // Demonstrate when base-class and derived-class 75 // constructors and destructors are called. 76 #include 77 #include "point2.h" 78 #include "circle2.h" 79 80 int main() 81 { 82 // Show constructor and destructor calls for Point 83 { 84 Point p( 11, 22 ); 85 } 86 87 cout << endl; 88 Circle circle1( 4.5, 72, 29 ); 89 cout << endl; 90 Circle circle2( 10, 5, 5 ); 91 cout << endl; 92 return 0; 93 } 输出结果: Point constructor: [ 11, 22 ] Point destructor: [ 11, 22 ] Point constructor: [ 72, 29 ] Circle constructor: radius is 4.5 [ 72, 29] Point constructor: [ 5, 5 ] Circle constructor: radius is 10 [ 5, 5 ] Circle destructor: radius is 10 [ 5, 5 ] Point destructor: [ 5, 5 ] Circle destructor: radius is 4.5 [ 72, 29 ] Point destructor: [ 72, 29 ] 图 3.7 基类和派生类的构造函数和析构函数的调用顺序 97 3.10 将派生类对象隐式转换为基类对象 尽管派生类对象也是基类对象,但是派生类类型和基类类型是不同的。在 public 继 承中,派生类对象能作为基类对象处理。由于派生类具有对应每个基类成员的成员(派生 类的成员通常比基类的成员多),所以把派生类的对象赋给基类对象是合理的。但是,反 过来赋值会使派生类中基类不具有的成员没有定义,所以这是不允许的。尽管如此,提供 正确的重载赋值运算符和(或)转换构造函数可以允许这种操作(见第 8 章)。 常见编程错误 3.4 把派生类对象赋给其基类对象,然后试图在新的基类对象中引用只在派生类中才有 的成员是十语法错误。 注意,在本节后面提到指针时,也适用于引用。 在 public 继承中,因为派生类对象也是基类对象,所以指向派生类对象的指针可以 隐式地转换为指向基类对象的指针。 基类指针和派生类指针与基类对象和派生类对象的混合和匹配有如下四种可能的方 式: 1.直接用基类指针引用基类的对象。 2.直接用派生类指针引用派生类的对象。 3.用基类指针引用一个派生类的对象。由于派生类的对象也是基类的对象,所以这 种引用方式是安全的,但是用这种方法只能引用基类成员。如果试图通过基类指针引用那 些只在派生类中才有的成员,编译器会报告语法错误。 4.用派生类指针引用基类的对象。这种引用方式会导致语法错误。派生类指针必须先 强制转换为基类指针。 常见编程错误 3.5 将基类指针强制转换为派生类指针,如果用该指针引用基类对象,而基类对象中没 有所要引用的派生类的成员,那么这时就会发生错误。 将派生类对象作为基类对象可能是很方便的,但使用基类指针操作这些对象容易出 问题。例如,在某个计算工资单的系统中,我们希望能够遍历关于雇员的清单并计算出每 人每周的工资。但是,使用基类指针使得程序只能调用基类的工资单计算例程(如果基类 中确实存在该例程)。我们需要一种方法为每一个对象(不管它是派生类对象还是基类对象) 调用正确的工资单计算例程,并且这种方法只需简单地使用基类指针。解决这个问题的答 案是使用第 10 章介绍的虚函数和多态性。 9.11 关于继承的软件工程 我们可以用继承来定制现有的软件。为了把现有类定制成满足我们的需要的类,首先 要继承现有类的属性和行为,然后添加和去除一些属性和行为。在 C++中,派生类不必访 问基类的源代码,但是需要能够连接到基类的目标代码。这种强大的功能对独立软件供应 商(ISV)很有吸引力。 98 ISV 开发出具有目标代码格式的类后,他们就拥有了这些类的所有权,因而可以销售和 发放使用许可证。 用户拥有这些类后,在不必访问源代码(所有权属于 ISV)的情况下,他们就能够从这些 类库中派生出新的类,所有的 ISV 需要为目标代码提供头文件。 软件工程视点 3.6 理论上,用户不需要看到所继承类的源代码。但实际上,根据发放许可证的经验,客 户通常会需要源代码。程序员似乎还是不大愿意放心地把别人编写的代码放进自己的程序 中。 性能提示 3.1 如果性能是主要考虑,则程序员可能要浏览所继承类的源代码,以便根据性能要求 调整代码。 学生们很难认识到大型软件项目的设计者和实现者所面临的问题。有过开发这种项目 经验的人都知道缩短软件开发过程的关键是鼓励软件复用。面向对象的程序设计普遍鼓励 软件复用,而 C++尤其提倡软件复用。 正是继承了实用的类库才发挥出了软件复用的最大优势。随着人们对 C++的兴趣不断 增长,对类库感兴趣的人也将增加。正如个人电脑的出现带动了 ISV 生产的套装软件日益 增长,C++也必将带动类库的建立和销售。因为应用程序设计者会用这些类库建立他们自 己的应用程序,所以类库设计者也将因此而获得丰厚的报偿。当前随 C++编译器分发的类 库倾向于一定的通用性并限制使用范围。在世界范围内开发应用于各种领域的类库的时代 正在来临。 软件工程视点 3. 7 建立一个派派生类不会影响其基类的源代码和目标代码,继承这一机制保护了基类 的完整性。 基类描述了共性。所有从基类派生出来的类都继承了基类的功能。在面向对象的设计 过程中,设计者先寻求井提取出构成所需基类的共性,然后再通过继承从基类派生出超 出基类功能的定制派生类。 软件工程视点 3.8 在面向对象的系统中,类常常是紧密相关的。提取出共同的属性和行为并把它们放在 一个基类中,然后再通过继承生成派生类。 正如非面向对象系统的设计者力图避免不必要的函数一样,面向对象系统的设计者 也应该避免不必要的类。多余的类不仅会带来类管理上的问题,而且会阻碍软件的复用。 理由很简单,因为用户难以在巨大的类集合中定位某个类权。权衡的结果还是建立较少的 类,每个类都实际增加一些功能。这样的类对于某些用户来说可能功能太丰富了一点,但 是他们可以屏蔽掉多余的功能,然后使之满足自己的需要。 性能提示 3.2 大于功能需求的派生类可能会浪费内存和处理资源。因此应继承最接近要求的类。 99 注意,因为派生类中没有列出继承来的成员,所以浏览一组派生类的声明会令人迷 惑,但是派生类中确实存在继承来的成员。 软件工程视点 3.9 派生类除了包含其基类的属性和行为外,还能够包含附加的属性和行为。继承机制能 够使基类独立于派生类编译。为了把基类与派生类中增加的属性和行为组合成派生类,编 译器只需要编译派生类中增加的属性和行为。 软件工程视点 3.10 只要基类的 public 接口不变,对基类的修改无需修改派生类,但是派生类需要重新 编译。 3.12 复合与继承的比较 我们讨论了 public 继承所支持的"是"关系,还讨论把对象作为成员的"有"关系,并举 了几个例子。"有"关系通过复合现有的类建立了新类。例如,假设有雇员类 Employee、生日 类 BirthDate 和电话号码类 TelephonehNunber,说雇员(Employee)是—个生日(BirthDate) 或电话号码(TelephoneNumber)是不对的,但是说雇员有生日和电话号码当然是合适的。 软件工程视点 3. 11 只要成员类的 public 接口不变,对成员类的修改无需修改复合类,但是复合类需要 重新编译。 3.13 对象的“使用”关系和“知道”关系 继承和复合都提倡建立与现有的类有许多共性的新类来实现软件复用。还有其他一些 方法可以利用类所提供的服务。尽管“人”不是一辆汽车,“人”也不能包含汽车,但 “人”当然可以“使用”汽车。一个函数可以简单地向对象发出函数调用来“使用”这个 对象。 一个对象可以“知道”另外一个对象,知识网中常常存在这种关系。一个对象可以包 含指向对象的指针或对该对象的引用,从而“知道”那个对象的存在。在这种情况下,可 以说一个对象和另一个对象具有“知道”关系。 3.14 实例研究:类 Point、Circle 和 Cylinder 3 下面考察本章的一个练习.即点、圆、圆柱体的层次结构。我们首先开发并使用类 Point(图 3.8).然后从类 Point 派生出类 Circle(图 3.9),最后从类 Circle 派生出类 100 Cylinder(图 3.10)。 图 3.8 列出了类 Point。图中的第 1 行到第 17 行是类 Point 的定义。可以看到,类 Point 的数据成员为 protected。因此.当从类 Point 派生出类 Circle 时,类 Circle 的成 员函数不必使用访问函数就能够直接引用坐标 x 和 y,这样可使性能更好。 第 18 行到第 39 行定义了类 Point 的成员函数。第 40 行到第 57 行是类 Point 的驱动程 序。程序中的 main 函数必须使用访问函数 getX 和 getY 读取 protected 数据成员 x 和 y 的 值。要记住,protected 数据成员只能被类和其派生类的成员和友元访问。 1 // Fig. 3.8: point2.h 2 // Definition of class Point 3 #ifndef POINT2_H 4 #define POINT2_H 5 6 class Point { 7 friend ostream &operator<<( ostream &, const Point & ); 8 public: 9 Point( int = 0, int = O ); // default constructor 10 void setPoint( int, int ); // set coordinates 11 int getX() const { return x; } // get x coordinate 12 int getY() const { return y; } // get y coordinate 13 protected: // accessible to derived classes 14 int x, y; // coordinates of the point 15 } ; 16 17 #endif 18 // Fig. 3.8: point2.cpp 19 // Member functions for class Point 20 #include 21 #include "point2.h" 22 23 // Constructor for class Point 24 Point::Point( int a, int b ) { setPoint( a, b ); } 25 26 // Set the x and y coordinates 27 void Point::setPoint( int a, int b ) 28 { 29 x = a; 30 y = b; 31 } 32 33 // Output the Point 34 ostream &operator<<( ostream &output, const Point &p ) 35 { 36 output << '[' << p.x << ", "<< p.y << '] '; 101 37 38 return output; // enables cascading 39 } 40 // Fig. 3.8:fig09 08.cpp 41 // Driver for class Point 42 #include 43 #include "point2.h" 44 45 int main() 47 Point p( 72, 115 ); // instantiate Point object p 48 49 // protected data of Point inaccessible to main 50 cout << "X coordinate is" << p.getx() 51 << "\nY coordinate is "<< p.getY(); 52 53 p.setPoint( 10, 10 ); 54 cout << "\n\nThe new location of p is "<< p << endl; 55 56 return 0; 57 } 输出结果: X coordinate is 72 Y coordinate is 115 The new location of p is [ 10,10 ] 图 3.8 演示 Point 类 第二个例子是图 3.9。该例子复用了图 3.8 中的类 Point 的定义和成员函数定义, 分别是类 Circle 的定义、类 Circle 的成员函数的定义和类的驱动程序。类 Circle 对类 Point 的继承是 public 继承,这意味着类 Circle 的 public 接口包括类 Point 的成员函数 以及 Circle 成员函数 setRadius、getRadius 和 area。 注意 Circle 重载 operator<<函数,Circle 类的友元可以通过将 Circle 引用 c 强制转 换为 Point 而输出 Circle 的 Point 部分。因此调用 Point 的 operator<<并用相应的 Point 格式输出 x 和 y 坐标。 驱动程序先实例化了类 Circle 的一个对象,然后用“get”函数读取该对象的信息 。 main 函数既不是类 Circle 的成员函数也不是其友元,因此它不能直接引用该类的 protected 数据。为此,程序中用"set"函数 setRadius 和 setPoint 重新设置圆的半径和圆 心坐标。最后,驱动程序先将 Point&(对 Point 对象的引用)类型的引用变量 pRef 初始化为 Circle 的对象 c,然后打印出 pRef,尽管它已经被初始化为一个 Circle 对象,但是它还 “认为”自己是一个 Point 对象,所以该 Circle 对象实际上作为 Point 对象打印的。 1 // Fig. 3.9: circle2.h 102 2 // Definition of class Circle 3 #ifndef CIRCLE2_H 4 #define CIRCLE2_H 5 6 #include "point2.h" 7 8 class Circle : public Point { 9 friend ostream &operator<<( ostream &, const Circle & ); 10 public: 11 // default constructor 12 Circle( double r = 0.0, int x = 0, int y = 0 ); 13 void setRadius( double ); // set radius 14 double getRadius() const; // return radius 15 double area() const; // calculate area 16 protected: // accessible to derived classes 17 double radius; // radius of the Circle 18 }; 19 20 #endif 21 // Fig. 3.9: circle2.cpp 22 // Member function definitions for class Circle 23 #include 24 #include 25 #include "circle2.h" 26 27 // Constructor for Circle calls constructor for Point 28 // with a member initializer and initializes radius 29 Circle::Circle( double r, int a, int b ) 30 : Point( a, b ) // call base-class constructor 31 { setRadius( r ); } 32 33 // Set radius 34 void Circle::setRadius( double r ) 35 { radius = ( r >= 0 ? r : 0 ); } 36 37 // Get radius 38 double Circle::getRadius() const { return radius; } 39 40 // Calculate area of Circle 41 double Circle::area() const 42 { return 3.14159 * radius * radius; } 43 44 // Output a circle in the form: 45 // Center [ x, Y ]; Radius = #.## 103 46 ostream &operator<<( ostream &output, const Circle &c ) 47 { 48 output << "Center = "<< static_cast< Point > ( c ) 49 << "; Radius =" 50 << setiosflags( ios::fixed | ios::showpoint ) 51 << setprecision( 2 ) << c.radius; 52 53 return output; // enables cascaded calls 54 } 55 // Fig. 3.9: fig09_09.cpp 56 // Driver for class Circle 57 #include 58 #include "point2.h" 59 #include "circle2.h" 6O 61 int main() 62 { 63 Circle c( 2.5, 37, 43 ); 64 65 cout << "X coordinate is "<< c.getX() 66 << "\nY coordinate is "<< c.getY() 67 << "\nRadius is "<< c.getRadius(); 68 69 c.setRadius( 4.25 ); 70 c.setPoint( 2, 2 ); 71 cout << "\n\nThe new location and radius of C are\n" 72 << c << "\nArea "<< c.area() << '\ n'; 73 74 Point &pRef = c; 75 cout << "\nCircle printed as a Point is: "<< pRef << endl; 76 77 return 0; 78 } 输出结果: X coordinate is 37 Y coordinate is 43 Radius is 2.5 The new location and radius of c are Center = [ 2,2 ]; Radius = 4.25 Area 56.74 Circle printed as a Point is: [ 2, 2 ] 104 图 3.9 演示 Circle 类型 最后一个例子是图 3.10。该例复用了类 Point 和类 Circle 的定义以及图 3.8 和图 3.9 中的成员函数的定义,分别是类 Cylinder 的定义、Cylinder 成员函数的定义以及类 的驱动程序。类 Cylinder 对类 Circle 的继承是 public 继承,这意味着类 Cylinder 的 public 接口包括类 Cylinder 的成员函数、类 Point 的成员函数以及成员函数 setHeight、getHeight、area(对 Circle 中的 area 重新定义)以及 volume。注意 Cylinder 构 造函数要调用直接基类 Circle 的构造函数,而不调用间接基类 Point 的构造函数。每个派 生类构造函数只负责调用直接基类的构造函数(多重继承中可能有多个直接基类)。另外, 注意 Cylinder 重载 operator<<函数,Cylinder 类的友元可以通过将 Cylinder 引用 c 强制 转换为 Circle 而输出 Cylinder 的 Circle 部分。因此调用 Circle 的 operator<<并用相应 Circle 格式输出 x 和 y 坐标。 驱动程序实例化了类 Cylinder 的一个对象,然后用“get”函数读取该对象的信息 。 main 函数既不是类 Cylinder 的成员函数也不是其友元,因此它不能直接引用类 Cylinder 的 protected 数据。为此,程序用"set"函数 setHeight 和 setRadius 以及 setPoint 重新设 置圆柱体的高度、半径和坐标值。最后,驱动程序先把 Point&(对 Point 对象的引用)类型 的引用变量 pRef 初始化为类 Cylinder 的对象 cyl,然后打印出 pRef,尽管它已经被初始 化为一个 Cylinder 对象,但是它还是“认为”自己是一个 Point 对象,所以该 Cylinder 对象实际上作为一个 Point 对象来打印;其次,将 Circle&(对 Circle 的引用)类型的引用 变量 cRof 初始化为 Cylinder 对象 cyl,然后驱动程序打印 cireleRef,尽管它已经被初 始化为一个 Cylinder 对象,它还“认为”自己是一个 Circle 对象,所以该 Cylinder 对象 实际上是作为一个 Circle 对象打印的,Circle 的面积也同时打印出来。 这个例子很好地演示了 public 继承以及对 protected 数据成员的定义和引用,读者 现在应该对继承的基本知识有了一定的了解。下一章要讨论如何用多态性编写具有继承层 次结构的程序。数据抽象、继承和多态性是面向对象程序设计的关键所在。 1 // Fig. 3.10: cylindr2.h 2 // Definition of class Cylinder 3 #ifndef CYLINDR2_H 4 #define CYLINDR2_H 5 6 #include "circle2.h" 7 8 class Cylinder : public Circle { 9 friend ostream &operator<<( ostream &, const Cylinder & ); 10 11 public: 12 // default constructor 13 Cylinder( double h = 0.0, double r = 0.0, 14 int x = 0, int y = 0 ); 15 16 void setHeight( double ); // set height 105 17 double getHeight() const; // return height 18 double area() const; // calculate and return area 19 double volume() const; // calculate and return volume 2O 21 protected: 22 double height; // height of the Cylinder 23 }; 24 25 #endif 26 // Fig. 3.10: cylindr2.cpp 27 // Member and friend function definitions 28 // for class Cylinder. 30 #include 31 #include "cylindr2.h" 32 33 // Cylinder constructor calls Circle constructor 34 Cylinder::Cylinder( double h, double r, int x, int y ) 35 : Circle( r, x, y ) // call base-class constructor 36 { setHeight( h ); } 37 38 // Set height of Cylinder 39 void Cylinder::setHeight( double h ) 40 { height = ( h >= 0 ? h : 0 ); } 41 42 // Get height of Cylinder 43 double Cylinder::getHeight() const { return height; } 44 45 // Calculate area of Cylinder (i.e., surface area) 46 double Cylinder::area() const 47 { 48 return 2 * Circle::area() + 49 2 * 3.14159 * radius * height; 5O } 51 52 // Calculate volume of Cylinder 53 double Cylinder::volume() const 54 { return Circle::area() * height; } 55 56 // Output Cylinder dimensions 57 ostream &operator<<( ostream &output, const Cylinder &c ) 58 { 59 output << static_cast< Circle >( c ) 60 << "; Height = "<< c.height; 61 106 62 return output; // enables cascaded calls 63 } 64 // Fig. 3.10: fig09_10.cpp 65 // Driver for class Cylinder 66 #include 67 #include 69 #include "circle2.h" 70 #include "cylindr2.h" 72 int main() 73 { 74 // create Cylinder object 75 Cylinder cy1( 5.7, 2.5, 12, 23 ); 76 77 // use get functions to display the Cylinder 78 cout << "X coordinate is " << cy1.getX() 79 << "\nY coordinate is "<< cy1.getY() 80 << "\nRadius is " << cy1.getRadius() 81 << "\nHeight is " << cy1.getHeight() << "\n\n"; 82 83 // use set functions to change the Cylinder's attributes 84 cy1.setHeight(10); 85 cy1.setRadius(4.25); 86 cy1.setPoint(2,2); 87 cout << "The new location, radius, and height of cy1 are:\n" 88 << cy1 << '\n'; 89 90 // display the Cylinder as a Point 91 Point &pRef = cy1; // pRef "thinks" it is a Point 92 cout << "\nCylinder printed as a Point is: " 93 << pRef << "\n\n"; 94 95 // display the Cylinder as a Circle 96 Circle &circleRef = cy1; // circleef thinks it is a Circle 97 cout << "Cylinder printed as a Circle is:\n" << circleRef 98 << "\nArea: "<< circleRef.area() << endl; 99 100 return 0; 101 } 输出结果: X coordinate is 12 Y coordinate is 23 Radius is 2.5 Height is 5.7 107 The new location, radius, and height of cy1 are: Center = [ 2, 2 ]; Radius = 4.25; Height = 10.00 Cylinder printed as a Point is: [ 2, 2 ] Cylinder printed as a Circle is: Center = [2, 2] ; Radius = 4.25 Area: 56.74 图 3.10 演示 Cylinder 类 3.15 多重继承 本章前面讨论了单一继承,即一个类是从一个基类派生来的。一个类也可以从多个基 类派生而来,这种派生称为“多重继承”(multiPle inheritance)。多重继承意味着一个 派生类可以继承多个基类的成员,这种强大的功能支持了软件的复用性,但可能会引起 大量的歧义性问题。 编程技巧 3.1 多重继承使用得好可具有强大的功能。当新类型与两个或多个现有类型之间存在” 是”关系时(即类型 A“是”类型 B 并且也“是”类型 c)应该使用多重继承。 图 3.11 中的程序是一个多重继承的例子。类 Base1 包含一个 protected 数据成员 int value,还包含设置 value 值的构造函数和返回 value 值的 public 成员函数 getData。 类 Base2 和类 Base1 相似,只不过它的 protected 数据成员是 char letter。类 Base2 也包含一个 Public 成员函数 getData,但是该函数返回的是 char letter 的值。 类 Derivcd 通过多重继承机制继承了类 Base1 和类 Base2,它有一个 private 数据成 员 float real 和一个读取 float real 的 public 成员函数 getReal。 多重继承是非常直接的,即在 class derived 后的冒号(:)之后跟上用逗号分开的公 有基类列表。还可以看到,构造函数 Derived 显式地调用了每个基类(即 Bae1 和 Base2)的 构造函数。同样,按指定的继承顺序调用基类构造函数,而不是按构造函数出现的顺序调 用。如果成员初始化值列表中不显式调用基类构造函数,则隐式调用基类的默认构造函数。 1 // Fig. 3.11: basel.h 2 // Definition of class Basel 3 #ifndef BASE1_H 4 #define BASE1_H 5 6 class Base1 { 7 public: 8 Base1( int x ) { value = x; } 108 9 int getData() const { return value; } 10 protected: // accessible to derived classes 11 int value; // inherited by derived class 12 }; 13 14 #endif 15 // Fig. 3.11: base2.h 16 // Definition of class Base2 17 #ifndef BASE2_H 18 #define BASE2_H 19 20 class Base2 { 21 public: 22 Base2( char c ) { letter = c; } 23 char getData() const { return letter; } 24 protected: // accessible to derived classes 25 char letter; // inherited by derived class 26 }; 27 28 #endif 29 // Fig. 3.11: derived.h 30 // Definition of class Derived which inherits 31 // multiple base classes (Basel and Base2). 32 #ifndef DERIVED_H 33 #define DERIVED_H 34 35 #include "base1.h" 36 #include "base2.h" 37 38 // multiple inheritance 39 class Derived : public Base1, public Base2 { 40 friend ostream &operator<<( ostream &, const Derived & ); 41 42 public: 43 Derived( int, char, double }; 44 double getReal() const; 45 46 private: 47 double real; // derived class's private data 48 }; 49 50 #endif 51 // Fig. 3.11: derived.cpp 52 // Member function definitions for class Derived 109 53 #include 54 #include "derived.h" 55 56 // Constructor for Derived calls constructors for 57 // class Basel and class Base2. 58 // Use member initializers to call base-class constructors 59 Derived::Derived( int i, char C, double f ) 60 : Base1( i ), Base2( c ), real ( f ) {} 61 62 // Return the value of real 63 double Derlved::getRealO const { return real; } 64 65 // Display all the data members of Derived 66 ostream &operator<<( ostream &output, const Derived &d ) 67 { 68 output <<" Integer: "<< d.value 69 << "\n Character: "<< d.letter 70 << "\nReal number: "<< d.real; 71 72 return output; // enables cascaded calls 73 } 74 / Fig. 3.11: fig09_ll.cpp 75 // Driver for multiple inheritance example 76 #include 77 #include "base1.h" 78 #include "base2.h" 79 #include "derived.h" 80 81 int main() 82 { 83 Base1 b1( 10 ), *base1Ptr = 0; // create Basel object 84 Base2 b2( 'Z' ), *base2Ptr = 0; // create Base2 object 85 Derived d( 7, 'A', 3.5 ); // create Derived object 86 87 // print data members of base class objects 88 cout << "Object b1 contains integer "<< b1.getData() 89 << "\nObject b2 contains character" << b2.getData() 90 << "\nObject d contains:\n" << d << "\n\n"; 91 92 // print data members of derived class object 93 // scope resolution operator resolves getData ambiguity 94 cout << "Data members of Derived can be" 95 << "accessed imdividually:" 96 << "In Integer: "<< d. Base1::getData() 110 97 << "\n Character: << d. Base2::getData() 98 << "\nReal number: "<< d.getReal() << "\n\n"; 99 100 cout << "Derived can be treated as an" 101 << "object of either base class:In"; 102 103 // treat Derived as a Basel object 104 base1Ptr = &d; 105 cout << "base1Ptr->getData() yields" 106 << base1Ptr->getData() << '\n'; 107 108 // treat Derived as a Base2 object 109 base2Ptr = &d; 110 cout << "base2Ptr->getData() yields" 111 << base2Ptr->getData() << endl; 112 113 return 0; 114 } 输出结果: object bl contains integer 10 object b2 contains character z object d contains: Integer: 7 Character: A Real number:3.5 Data members of Derived can be accessed individually: Integer: 7 Character: A Real number:3.5 Derived can be treated as an object of either base class: baselPtr->getDataO yields 7 base2Ptr->getData() yields A 图 3.11 多重继承的程序 在 Derived 中重载的流插入运算符通过派生类对象 d 用圆点表示法打印 value、letter 和 real 的值。因为该运算符函数是类 Derived 的友元,所以 operator<<可以直接访问类 Derived 的 private 数据成员 real,还能访问 Base1 和 Base2 的 protected 数据成员 value 和 letter。 下面探讨一下 main 函数中的驱动程序。程序中首先建立了类 Base1 的对象 b1 和类 Base2 的对象 b2,并将它们分别初始化为 int 类型的值 10 和 char 类型的值'z',然后建立 111 类 Derived 的对象 d 并将其初始化成包括 int 类型的值 7、char 类型的值'A'和 float 类型的 值 3.5。 通过调用每个对象的 getData 函数打印每个基类对象的内容。尽管有两种 getData 函 数,但是因为直接引用了对象 b1 和 b2 的 getData 函数版本,因此对它们的调用没有歧义 性问题。 接下来用静态关联打印出 Derived 的对象 d 的内容。因为该对象包含两个 getData 函数, 一个是从类 Base1 继承来的,另一个是从 Base2 继承来的,所以存在着歧义性问题。用二 元作用域运算符很容易解决这个问题。例如,d.Base1::getData()打印了 int 类型的 value 值, d.Base2::getData()打印了 char 类型的 letter 值。调用 d.getReal()打印 float 类型的 real 值不存在歧义性问题。然后演示了单一继承的“是”关系也适用于多重继承。程序中把派 生类对象 d 的地址赋给了基类指针 Base1Ptr,并用该指针调用 Base1 的成员函数 getData 打印出 int value 值。之后又把 d 的地址赋给基类指针 Base2Ptr,并用该指针调用 Base2 的成员函数 getData 打印出 charletter 的值。 这个例子展示了多重继承的机制并介绍了一种简单的歧义性问题。多重继承是一个很 复杂的话 题,许多高级 C++书籍对此有详细的论述。 软件工程视点 3.12 多重继承是个强大的功能,但可能增加系统的复杂性。使用多重继承的系统需要更加 认真设计,能用单一继承时应尽量使用单一继承。 小 结 ●面向对象的程序设计能力的关键之一是通过继承实现软件的复用。 ●程序员可以让新类继承已定义基类的数据成员和成员函数,而不必重新编写新的 数据成员和成员函数。这种新类称为派生类。 ●对于单一继承,派生类只有一个基类。对于多重继承,派生类常常是从多个基类派 生出来的,这些基类之间可能毫无关系。 ●派生类通常添加了其自身的数据成员和成员函数,因而通常比基类大得多。派生类 比基类更具体,它代表了一组外延较小的对象。 ●派生类不能访问其基类的 private 成员,否则会破坏基类的封装性。但是,派生类 能够访问基类的 public 成员和 protected 成员。 ●派生类的构造函数总是先调用其基类构造函数来初始化派生类中的基类成员。 ●调用析构函数的次序和调用构造函数的次序相反,因此派生类析构函数在基类析 构函数之前调用。 ●利用继承能够实现软件复用。软件复用缩短了程序的开发时间,促使开发人员复用 已经测试和调试好的高质量的软件。 ●可以用现有的类库实现继承。 ●将来有一天,软件也可以像如今的硬件一样用标准的可复用组件进行构造。 ●派生类不必访问基类的源代码,但是需要知道基类的接口和能够连接到基类的目 标代码。 ●派生类对象可作为其 public 基类的对象处理,但是反过来不行。 ●基类和派生类之间具有层次关系。 ●一个类可以单独存在,但是当利用继承机制使用该类时,该类就成为给其他类提 112 供属性和行为的基类,或者成为继承其他类的属性和行为的派生类。 ●继承层次的深度在特定系统的限制范围之内是任意的。 ●层次结构是管理和弄清复杂问题的有用工具。随着软件日益复杂化,C++提供了支 持层次结构的继承和多态性机制。 ●程序员可以用显式类型转换把基类指针转换为派生类指针。但是,如果要复引用该 指针,那么在转换前首先应该把它指向某个派生类的对象。 ●protected 访问是介于 public 访问和 private 访问之间的中间层次。基类的 protected 成员只能被基类的成员和友元以及派生类的成员和友元访问,没有其他函数能 访问基类的 protected 成员。 ●protected 成员能用来扩展对派生类的访问权限,而拒绝非类成员函数和非友元函 数的访问权。 ●通过在派生类名后放置冒号并紧跟逗号分隔的基类列表可以用来指明多重继承。在 派生类构造函数调用基类构造函数时使用成员初始化值的列表。 ●从基类派生一个类时,基类可被声明为 public、protected 或者 private。 ●从 public 基类派生一个类时,基类的 public 成员成为派生类的 public 成员,基类 的 protected 成员成为派生类的 protected 成员。 ●从 protected 基类派生一个类时,基类的 public 和 protected 成员都成为派生类的 Protected 成员。从 private 基类派生一个类时,基类的 public 和 protected 成员均成为 派生类的 private 成员。 ●基类既可能是派生类的直接基类,也可能是派生类的间接基类。在声明派生类时, 派生类的首部要显式地列出直接基类。间接基类不是显式地列在派生类的头部,而是沿着 类的多个层次向上继承。 ●不适合派生类的基类成员可以在派生类中重新定义。 ●区分“是”关系和“有”关系是很重要的。在“是”关系中,派生类的对象也以作 为基类的对象处理。而“有”关系中,一个类的对象拥有作为其成员的其他类的对象。 “是”关系是一种继承,“有”关系是一种复合。 ●派生类对象可以赋给基类对象。由于派生类具有每一个基类成员,这种赋值是有意 义的。 ●指向派生类对象的指针可以隐式地转换为指向基类对象的指针。 ●使用显式强制转换可以将基类指针转换为派生类指针,指向的目标应该是派生类的 对象。 ●基类描述了共性。所有从基类派生出来的类都继承了基类的功能。在面向对象的设计 过程中,设计者先寻求并提取出构成所需基类的共性,然后再通过继承从基类派生出超 出基类功能的定制派生类。 ●因为派生类中没有列出所有的成员,所以浏览一组派生类的声明会令人迷惑,当派 生类的声明中没有列出继承来的成员时更是如此,但是派生类中确实存在继承来的成员。 ●“有”关系通过复合现有的类建立了新类。 ●“知道”关系是对象包含其他对象的指针或引用,因而知道存在这些对象。 ●成员对象构造函数以声明对象的顺序调用。对继承关系而言.基类构造函数以指定 的继承顺序调用并且在调用派生类构造函数之前调用。 ●对于一个派生类对象,先调用基类的构造函数,然后调用派生类的构造函数(也许 调用成员对象的构造函数)。 ●当取消派生类对象时,析构函数的调用顺序与调用构造函数的顺序相反,即先调用 派生类的析构函数,然后调用基类的析构函数。 113 ●一个类可以从多个基类派生而来,这种派生称为多重继承。 ●在继承指示符即冒号(:)后跟上用逗号分开的基类列表表示了多重继承。 ●派生类构造函数用成员初始化值语法调用其每个基类的构造函数。基类构造函数按 其在继承中声明的基类顺序依次调用。 第四章 虚函数和多态 教学目标 ●了解多态性的概念 ●了解怎样声明和使用实现多态性的虚函数 ●了解抽象类和具体类的区别 ●学会怎样声明建立抽象类的纯虚函数 ●认识多态性是如何扩展和维护系统 ●了解 C++如何实现虚函数和动态关联 4.1 简介 虚函数(virtual function)和多态性(Plymorphism)使得设计和实现易于扩展的系统成 为可能。程序可以对层次中所有现有类的对象(基类对象)进行一般性处理。程序开发期间 不存在的类可以用一般化程序稍作修改或不经修改即加进去,只要这些类属于一般处理 的继承层次。程序中惟一要修改的部分是需要直接了解加进层次中的特定类的部分。 4.2 类型域和 switch 语句 处理多种不同类型对象的手段之一是使用 switch 语句。switch 语句能够根据每一种 对象的类型选择对该对象合适的操作。例如,在形状层次中,每个形状指定自己的类型数 据成员,switch 结构可以根据特定对象的类型确定调用哪个 print 函数。 但是,使用 switch 逻辑存在许多问题。例如,程序员可能会忘记应有的类型测试; 在一条 switch 语句中可能会忘记测试所有可能的情况;在修改基于 switch 语句的系统时 可能会忘记在现有的 switch 语句中插入新类;为了处理新的类型,每次修改 switch 语句 都要修改系统中的每一条 switch 语句,这很费时并且容易出错。 正如以后会看到的,利用了虚函数和多态性的程序设计无需使用 switch 逻辑。程序 员可以用虚函数机制自动完成等价的逻辑,因而避免与 switch 逻辑有关的各种各样的错 误。 软件工程视点 4.1 使用虚函数和多态性可简化源代码的长度。为支持更简单的顺序代码,虚函数和多态 性包含的分支逻辑更少。这种简化有助于程序的测试、调试和维护。 114 4.3 虚函数 假定一组形状类(如 Circle、Trriangle、Rectangle 和 Square 等等)都是从基类 Shape 派生出来的。在面向对象的程序设计中,我们可能要使每一个这样的类都能够绘制其自身 形状。尽管每个类都有它自己 draw 函数,但是绘制每种形状的 draw 函数却是大不相同的。 当需要绘制形状时,不管它是什么形状,把它作为基类 Shape 的对象处理是再好不过的。 然后,我们只需要简单地调用基类 Shape 的函数 draw,并让程序动态地确定(即在执行时 确定)使用哪个派生类的 draw 函数。 为了使这种行为可行,我们把基类中的函数 draw 声明为虚函数,然后在每个派生类 中重新定义 draw 使之能够绘制合适的形状。虚函数的声明方法是在基类的函数原型前加 上关键字 virtual。例如,基类 Shape 中可能出现: virtual void draw() const; 上述原型声明函数 draw 是不取参数也不返回数值的常量函数,而且是个虚函数。 软件工程视点 4.2 一旦一个函数被声明为虚函数,即使重新定义类时没有声明虚函数,那么它从该点 之后的继承层次结构中都是虚函数。 编程技巧 4.1 虽然函数在类层次结构的高层中声明为虚函数会使它在低层隐式地成为虚函数,但 有些程序员为了提高程序的清晰性更喜欢在每一层中显式地声明这些虚函数。 软件工程视点 4.3 没有定义虚函数的派生类简单地继承其直接基类的虚函数。 如果在基类中将函数 draw 声明为 virtual,然后用基类指针或引用指明派生类对象 并使用该指针调用 draw 函数(如 shapePtr->draw()),则程序会动态地(即在运行时)选择 该派生类的 draw 函数,这称为动态关联(见 4.6 和 10.9 节的实例研究)。 如 果 用 名 字 和 圆 点 成 员 选 择 运 算 符 引 用 一 个 特 定 的 对 象 来 调 用 虚 函 数 ( 如 squareObject.draw()),则被调用虚函数是在编译时确定的(称为静态关联),调用的虚函 数也就是为该特定对象的类定义(或继承该特定对象类)的函数。 4.4 抽象基类和具体类 当我们把类看作一种数据类型时,我们通常认定该类型的对象是要被实例化的。但是, 在许多情况下,定义不实例化为任何对象的类是很有用处的,这种类称为“抽象 类”(abstract class)。因为抽象类要作为基类被其他类继承,所以通常也把它称为“抽 象基类”(abstract base class)。抽象基类不能用来建立实例化的对象。 抽象类的惟一用途是为其他类提供合适的基类,其他类可从它这里继承和(或)实现 115 接口。能够建立实例化对象的类称为具体类(concrete class )。 例如,我们可以建立抽象基类 TwoDimensionalObject,然后从它派生出具体类 Square、Circle 和 Triangle 等等,也可以建立抽象基类 ThreeDimensionalObject,然后从 它派生出具体类 Cube、Sphere 和 Cylinder 等等。这些抽象基类表述的含义因为太广泛而定 义不出实在的对象。如果要建立实例对象,则需要含义更加明确的类,,这就是所谓的 “具体类”。具体类具有足以能够建立实例化对象的明确含义。 如果将带有虚函数的类中的一个或者多个虚函数声明为纯虚函数,则该类就成为抽 象类。纯虚函数是在声明时”初始化值”为 0 的函数,例如: virtual float earnings() const = O; // pure virtual 软件工程视点 4.4 如果某个类是从一个带有纯虚函数的类派生出来的,并且没有在该派生类中提供该 纯虚函数的定义,则该虚函数在派生类中仍然是纯虚函数,因而该派生类也是一个抽象 类。 常见编程错误 4.1 试图实例化一个抽象类对象(即包合一个或者多个纯虚函数的类)是一种语法错误。 一个类层次结构中可以不包含任何抽象类,但是正如以后会看到的,很多良好的面 向对象的系统,其类层次结构的顶部是一个抽象基类。在有些情况中,类层次结构顶部有 好几层都是抽象类。 形状类的层次结构就是一种典型的范例。我们可以在该层次结构的顶部建立抽象基类 shape , 在 往 下 的 一 层 中 还 可 以 再 建 立 两 个 抽 象 基 类 , 即 二 维 形 状 类 TwoDimensionalShape 和三维形状类 ThreeDimensionalShape,再往下我们就可以开始定 义二维形状的具体类如圆形类和正方形类以及三维形状的具体类如球类和立方体类等等。 4.5 多态性 C++支持多态性。所谓多态性是指:通过继承相关的不同的类,他们的对象能够对同 一个函数调用作出不同的响应。例如,如果类 Rectangle 是从类 Quadrilateral 派生出来 的,那么类 Rectangle 的对象比类 Quadrilateral 的对象的更具体,对类 Quadfilateral 的对象的操作(如计算周长和面积)也能用在类 Rextangle 的对象上。 多态性是通过虚函数实现的。当通过基类指针(或引用)请求使用虚函数时,C++会在 与对象关联的派生类中正确地选择重定义的函数。 有时候在基类中定义的非虚函数会在派生类中重新定义。如果用基类指针调用该成员 函数,则选择基类版本的成员函数;如果用派生类指针调用该成员函数,则选择派生类 版本的成员函数。这不是多态性行为。 下面的例子使用图 9.5 的基类 Employee 和派生类 HourlYWorker: Employee e, *ePtr = &e; HourlyWorker h, *hPtr = &h; ePtr->print(); // call base-class print function hPtr-> print(); // call derived-class print function 116 ePtr = &h; // allowable implicit conversion ePtr->print(); // still calls base-class print 基类 Employee 和派生类 HourlyWorker 都定义了自己的 print 函数。由于这个函数没 有声明为 virtual,而且签名相同,因此通过 Employee 指针调用 print 函数时调用 Employee::print() (不管 Employee 指针指向基类对象还是派生类 HourlyWorker 对象),而 通过 HourlyWorker 指针调用 print 函数则调用 Worker::print()。派生类也可以调用基类函 数,但派生类对象通过派生类对象的指针调用基类 print 时,函数要显式调用如下: hPtr-> Employee::print(); // call base—class print function 表示调用基类 print。 使用虚函数和多态性能够使成员函数的调用根据接收到该调用的对象的类型产生不 同的动作(但会需要少量执行时的开销)多态性赋予了程序员极大的灵活性。下面几节要举 例说明多态性和虚函数的功能。 软件工程视点 4. 5 利用虚函数和多态性,程序员可以处理普遍性而让执行环境处理特殊性。即使在不知 道一些对象的类型的情况下,程序员也可以命令各种各样的对象表现出适合这些对象妁 行为。 软件工程视点 4.6 多态性提高了可扩展性:处理多态性行为的软件可以用与接收消息的对象类型无关 的方式编写。因此,不必修改基本系统应可以把能够响应现有消息的新类型的对象添加到 系统中。除了实例化新对象的客户代码需要重新编译外,程序无需重新编译。 软件工程视点 4.7 抽象类为类层次结构中的各个成员定义接口。抽象类中包含了要在派生类中定义的纯 虚函数,该层次结构中的所有函数都可以通过多态性使用同样的接口。 尽管不能实例化抽象基类的对象,但是可以声明引用抽象基类的指针。当实例化了具 体类的对象后,可以用这种指针使派生类对象具有多态操作能力。 下面考虑一个应用多态性和虚函数的例子。一个屏幕管理程序需要显示各种各样的对 象,甚至包括在屏幕管理程序编写后又添加到系统中的新类型的对象。系统可能需要显示 各种各样的形状,例如正方形、圆形、三角形、矩形等等(每一个类都是基类 Shape 的派生 类)。屏幕管理程序使用基类指针(指向 Shape)来管理要显示的对象。为了能够绘制所有的 对象(不管该对象在继承层次结构中的哪一层),管理程序都是使用指向该对象的基类指 针并向该对象简单地发送一条 draw 消息。函数 draw 在基类 Shape 中被声明为纯虚函数, 并且在每一个派生类中被重新定义,每个对象都知道如 何绘制自身。屏幕管理程序不必关心这些细节内容,它只要简单地告诉每个对象进行绘制 即可。 多态性特别适合于实现分层的软件系统。例如,在操作系统中各种类型的物理设备彼 此之间的操作是不同的,然而从设备读取数据和把数据写入设备的命令在某种程度是统 一的。发送给设备驱动程序对象的“写”消息(write 函数调用)需要在该设备驱动程序的 上下文中具体地解释,并且还要解释设备驱动程序是如何操作该特定类型设备的。但是, write 调用本身和对任何其他对象的 write 调用实际上没有什么区别,都只是把内存中一 117 定数目的字节放在设备中。面向对象的操作系统可能会用抽象基类为所有设备驱动程序提 供合适的接口,然后通过继承抽象基类生成执行所有类似操作 的派生类。设备驱动程序所提供的功能(即 public 接口)在抽象基类中则是以纯虚函数形式 出现的,派生类中提供了这些虚函数的实现.已实现的函数能够响应特定类型的设备驱 动程序。 利用多态编程,程序可以从类层次的不同层中遍历对象的指针数组。这种数组中的指 针都是派生类对象的基类指针。例如,TwoDimensionalshape 类的对象数组可以包含指向 派生类 Square、Circle、Triangle、Rectangle 和 Line 等对象的 TwoDimensionalShape *指 针。使用多态编程时,发出一个绘制数组中每个对象的消息即可在屏幕上画出正确的图形。 4.6 实例研究:利用多态性的工资单系统 下面的范例程序用虚函数和多态性根据雇员的类型完成工资单的计算(见图 10.1)。所 用的基类是雇员类 Employee,其派生类包括:老板类 Boss,不管工作多长时间他总是有 固定的周薪;销售员类 CommissionWorker,他的收入是一小部分基本工资加上销售额的 一定的百分比;计件工类 PieceworkWorker,他的收入取决他生产的工件数量;小时工类 HourlyWorker,他的收入以小时计算,再加上加班费。 函数 earnings 的调用当然要普遍适用于所有的雇员。每人收入的计算方法取决于它 属于哪一类雇员。因为这些类都是由基类 Employee 派生出来的,所以函数 earnings 在基 类 Employee 中被声明为 virtual,并在每个派生类中都正确地实现 earnings。为计算任何 雇员的收入,程序简单地使用了一个指向该雇员对象的基类指针并调用函数 earnings。在 一个实际的工资单系统中,各种雇员对象可能保存在一个数组(链表)中,数组每个指针 都是 Employee *类型,然后程序遍历链表中的每一个节点,并在每一个节点处用 Employee *指针调用对象的 earnings 函数。 下面看一看类 Employee。该类的 public 成员函数包括:构造函数,该构造函数有两 个参数,第一个参数是雇员的姓,第二个参数是雇员的名;析构函数,用来释放动态分 配的内存;两个“get”函数,分别返回雇员的姓和名;纯虚函数 earnings 和虚函数 print。为什么要把 earnings 函数声明为纯虚函数呢?因为在类 Employee 中提供这个函数 的实现是没有意义的,将它声明为纯虚函数表示要在派生类中而不是在基类中提供具体 的实现。对于具有广泛含义的雇员,我们不能计算出他的收入,而必须首先知道该雇员的 类型。程序员不会试图在基类 Employee 中调用该纯虚函数,所有的派生类根据相应的实 现为这些类重定义 earnings。 类 Boss 是通过 public 继承从类 Employee 派生出来的,它的 public 成员函数包括: 构造函数,构造函数有三个参数,即雇员的姓和名以及周薪,为了初始化派生类对象中 基类部分的成员 firstName 和 lastName,雇员的姓和名传递给了类 Employee 的构造函数; “set”函数,用来把新值赋绐 private 数据成员 weeklySalary;虚函数 earnings,用来 定义如何计算 Boss 的工资;虚函数 print,它输出雇员类型,然后调用 Employee:print() 输出员工姓名。 类 CommissionWorker 是通过 public 继承从类 Employee 派生出的,它的 public 成员 函数包括:构造函数,构造函数有五个参数,即姓、名、基本工资、回扣及产品销售量,井 将姓和名传递给了类 Employee 的构造函数;"set"函数,用于将新值赋给 private 数据成 118 员 salary、commission 和 quantity; 虚函数 earnings,用来定义如何计算 CommissionWorker 的工资;虚函数 print,输出 雇员类型,然后调用 Employs:print()输出员工姓名。 类 PieceWorker 是通过 public 继承从类 Employee 派生出来的,public 成员函数包括: 构造函数,构造函数有四个参数,即计件工的姓、名、每件产品的工资以及生产的产品数 量,并将姓和名传递给了类 Employee 的构造函数;"set"函数,用来将新值赋给 private 数据成员 wagePerPiece 和 quantity; 虚函数 earnings,用来定义如何计算 PieceWorker 的工资;虚函数 print,它输出雇员 类型,然后调用 Employee:print()输出员工姓名。 类 HourlyWorker 是通过 public 继承从类 Employee 派生出来的,public 成员函数包 括: 构造函数,构造函数有四个参数,即姓、名、每小时工资及工作的时间数,并将姓、名 传递给了类 Employee 的构造函数;“set”函数,将新值赋给 private 数据成员 wage 和 hours;虚函数 earnings,用来定义如何计算 HourlyWorker 的工资;虚函数 print,输出 雇员类型,然后调用 Employee:print()输出员工姓名。 1 // Fig. 4.1: employ2.h 2 // Abstract base class Employee 3 #ifndef EMPLOY2_H 4 #define EMPLOY2_H 5 6 #include 7 8 class Employee { 9 public: 10 Employee( const char *, const char * ); 11 ~Employee(); // destructor reclaims memory 12 const char *getFirstName() const; 13 const char *getLastName() const; 14 15 // Pure virtual function makes Employee abstract base class 16 virtual double earnings() const = 0; // pure virtual 17 virtual void print() const; // virtual 18 private: 19 char *firstName; 20 char *lastName; 21 }; 22 23 #endif 24 // Fig. 4.1: employ2.cpp 25 // Member function definitions for 26 // abstract base class Employee. 27 // Note: No definitions given for pure virtual functions. 28 #include 29 #include 119 30 #include "employ2.h" 31 32 // Constructor dynamically allocates space for the 33 // first and last name and uses strcpy to copy 34 // the first and last names into the object. 35 Employee::Employee( const char *first, const char *last ) 36 { 37 firstName = new char strlen( first ) + 1 ]; 38 assert( firstName != 0 ); // test that new worked 39 strcpy( firstName, first ); 40 41 lastName = new char strlen( last ) + 1 ] ; 42 assert( lastName != 0 ); // test that new worked 43 strcpy( lastName, last ); 44 } 45 46 // Destructor deallocates dynamically allocated memory 47 Employee::~Employee() 48 { 49 delete [] firstName; 50 delete [] lastName; 51 } 52 53 // Return a pointer to the first name 54 // Const return type prevents caller from modifying private 56 // deletes dynamic storage to prevent undefined pointer. 57 const char *Employee::getFirstName() const 58 { 59 return firstName; // caller must delete memory 60 } 61 62 // Return a pointer to the last name 63 // Const return type prevents caller from modifying private 64 // data. Caller should copy returned string before destructor 65 // deletes dynamic storage to prevent undefined pointer 66 const char *Employee::getLastName() const 67 { 68 return lastName; // caller must delete memory 69 } 7O 71 // Print the name of the Employee 72 void Employee::print() const 73 { cout << firstName << ' ' << lastName; } 74 // Fig. 4.1: boss1.h 120 76 #ifndef BOSS1_H 78 #include "employ2.h" 79 80 class Boss : public Employee { 81 public: 82 Boss( const char *, const char *, double = 0.0 ); 83 void setWeeklySalary( double ); 84 virtual double earnings() const; 85 virtual void print() const; 86 private: 87 double weeklySalary; 88 }; 89 90 #endif 91 // Fig. 10.1: boss1.cpp 92 // Member function definitions for class Boss 93 #include "boss1.h" 94 95 // Constructor function for class Boss 96 BOSS::BOSS( const char *first, const char *last, double s ) 97 : Employee( first, last ) // call base-class constructor 98 { setWeeklySalary( s ); } 99 100 // Set the Boss's salary 101 void Boss::setWeeklySalary( double s ) 102 { weeklySalary = s > 0 ? s : 0; } 103 104 // Get the BOSS'S pay 105 double Boss::earnings() const { return weeklySalary; } 106 107 // Print the BOSS'S name 108 void Boss::print() const 109 { 110 cout << "\n Boss: "; 111 Employee::print(); 112 } 113 / Fig. 10.1: commisl.h 115 #ifndef COMMIS1_H 116 #define COMMIS1_H 117 #include "employ2.h" 118 119 class commissionWorker : public Employee { 120 public: 121 CommissionWorker( const char *, const char *, 121 122 double = 0.0, double = 0.0, 123 int= 0 ); 124 void setSalary( double ); 125 void setCommission( double ); 126 void setQuantity( int ); 127 virtual double earnings() const; 128 virtual void print() const; 129 private: 130 double salary; // base salary per week 131 double commission; // amount per item sold 132 int quantity; // total items sold for week 133 }; 134 135 #endif 136 // Fig. 10.1; commis1.cpp 137 // Member function definitions for class CommissionWorker 138 #include 139 #include "commis1.h" 140 141 // Constructor for class CommissionWorker 142 CommissionWorker::CommissionWorker( const char * first, 143 const char *last, double s, double c, int q ) 144 : Employee( first, last ) // call base-class constructor 145 { 146 setSalary( s ); 147 setCommission( c ); 148 setQuantity( q ); 149 } 150 151 // Set CommissionWorker's weekly base salary 152 void CommissionWorker::setSalary( double s ) 153 { salary = s > 0 ? s : 0; } 154 155 // Set CommissionWorker's conunission 156 void CommissionWorker::setCommission( double c ) 157 { commission = c > 0 ? c : 0; } 158 159 // Set commissionWorker's quantity sold 160 void CommissionWorker::setQuantity( int q ) 161 { quantity = q > 0 ? q : 0; } i62 163 // Determine CommissionWorker's earnings 164 double CommissionWorker::earnings() const 165 { return salary + commission * quantity; } 122 166 167 // Print the CommissionWorker's name 168 void CommissionWorker::print() const 169 { 170 cout << "\nCommission worker: "; 171 Employee::print(); 172 } 173 // Fig. 4.1: piecel.h 174 // pieceWorker class derived from Employee 175 #ifndef PIECE1_H 176 #define PIECE1_H 177 #include "employ2.h" 178 179 class PieceWorker : public Employee { 180 public: 181 PieceWorker( const char *, const char *, 182 double = 0.0, int = 0); 183 void setWage( double ); 184 void setQuantity( int ); 185 virtual double earnings() const; 186 virtual void print() const; 187 private: 188 double wagePerPiece; // wage for each piece output 189 int quantity; // output for week 190 }; 191 192 #endif 193 // Fig. 4.1: piecel.cpp 194 // Member function definitions for class pieceWorker 195 #include 196 #include "piecel.h" 197 198 // Constructor for class PieceWorker 199 pieceWorker::pieceWorker( const char *first, const char *last, 200 double w, int q ) 201 : Employee( first, last ) // call base-class constructor 202 { 203 setWage( w ); 204 setQuantity( q ); 205 } 206 207 // Set the wage 208 void PieceWorker::setwage( double w ) 209 { wagePerPiece = w > 0 ? w : 0; } 123 210 211 // Set the number of items output 212 void PieceWorker::setQuantity( int q ) 213 { quantity = q > 0 ? q : 0; } 214 215 // Determine the PieceWorker's earnings 216 double PieceWorker::earnings() const 217 { return quantity * wagePerPiece; } 218 219 // Print the PieceWorker's name 220 void PieceWorker::print() const 221 { 222 cout << "\n Piece worker: "; 223 Employee::print(); 224 } 225 // Fig. 10.1: hourlyl.h 226 // Definition of class HourlyWorker 227 #ifndef HOURLY1_H 228 #define HOURLY1_H 229 #include "employ2.h" 230 231 class HourlyWorker : public Employee { 232 public: 233 HourlyWorker( const char *, const char *, 234 double = 0.0, double = 0.0); 235 void setWage( double ); 236 void setHours( double ); 237 virtual double earnings() const; 238 virtual void print () const; 239 private: 240 double wage; // wage per hour 241 double hours; // hours worked for week 242 } ; 243 244 #endif 245 // Fig. 4.1: hourly1.cpp 246 // Member function definitions for class HourlyWorker 247 #include 248 #include "hourly1.h" 249 250 // Constructor for class HourlyWorker 251 HourlyWorker::HourlyWorker( const char *first, 252 const char *last, 253 double w, double h } 124 254 : Employee( first, last ) // call base-class constructor 255 { 256 setwage( w ); 257 setHours( h ); 258 } 259 260 // Set the wage 261 void HourlyWorker::setwage( double w ) 262 { wage = w > 0 ? w : 0; } 263 264 // Set the hours worked 265 void HourlyWorker::setHours( double h ) 266 { hours = h >= 0 && h < 168 ? h : 0; } 267 268 // Get the HourlyWorker's pay 269 double HourlyWorker::earnings() const 270 { 271 if ( hours <= 40 ) // no overtime 272 return wage * hours; 273 else // overtime is paid at wage * 1.5 274 return 40 * wage + ( hours - 4O ) * wage * 1.5; 275 } 276 277 // Print the HourlyWorker's name 279 { 280 cout << "\n Hourly worker: "; 281 Employee::print(); 282 } 283 // Fig. 4.1: figl0_01.cpp 284 // Driver for Employee hierarchy 285 #include 286 #include 287 #include "employ2.h 288 #include "boss1.h" 289 #include "commis1.h" 290 #include "piece1.h" 291 #include "hourly1.h" 292 293 void virtualViaPointer( const Employee * ); 294 void virtualViaReference( const Employee & ); 295 296 int main() 297 { 298 // set output formatting 125 299 cout << setiosflags( ios::fixed | ios::showpoint ) 300 << setprecision( 2 ); 301 302 Boss b( "John", "Smith", 800.00 ); 303 b.print(); // static binding 304 cout << "earned $" << b.earnings(); // static binding 305 virtualViaPointer( &b ); // uses dynamic binding 306 virtualViaReferenee( b ); // uses dynamic binding 307 308 CommissionWorker c( "Sue", "Jones", 200.0, 3.0, 150 ); 309 c.print(); // static binding 310 cout << "earned $" << c.earnings(); // static binding 311 virtualViaPointer( &c )/ // uses dynamic binding 312 virtualViaReference( c ); // uses dynamic binding 313 314 PieceWorker p( "Bob", "Lewis", 2.5, 200 ); 315 p.print(); // static binding 316 cout << "earned $" << p.earnings(); // static binding 317 virtualViaPointer( &p ); // uses dynamic binding 318 virtualViaReference( p ); // uses dynamic binding 319 320 HourlyWorker h( "Karen", "Price", 18.75, 40 ); 321 h.print(); // static binding 322 cout << "earned $" << h.earnings(); // static binding 323 virtualViaPointer( &h ); // uses dynamic binding 324 virtualViaReference( h ); // uses dynamic binding 325 cout << endl; 326 return 0; 327 } 328 329 // Make virtual function calls off a base-class pointer 330 // using dynamic binding. 331 void virtualViaPointer( const Employee *baseClassPtr ) 332 { 333 baseClassPtr->print(); 334 cout << "earned $" << baseClassPtr->earnings(); 335 } 336 337 // Make virtual function calls off a base-class reference 338 // using dynamic binding. 339 void virtualViaReference( const Employee &baseClassRef ) 340 { 341 baseClassRef.print(); 342 cout << " earned $ " << baseclassRef.earnings(); 126 343 } 输出结果: BOSS: John Smith earned $800,00 Boss: John Smith earned $800.00 Boss: John Smith earned $800.00 Commission Worker: Sue Jones earned $650.00 Commission worker: Sue Jones earned $650.00 Commission worker: Sue Jones earned $650,00 Piece worker: Bob Lewis earned $500.00 Piece worker: Bob Lewis earned $500.00 Piece worker: Bob Lewis earned $500.00 Hourly worker: Karen Price earned $550.00 Hourly worker: Karen Price earned $050.00 Hourly worker: Karen Price earned $550.00 图 4. 1 Employee 类层次的多态性 驱动程序 main 函数中的四小段代码是类似的,因此我们只讨论处理 Boss 对象的第 一段代码。 第 302 行: Boss b("John","Smith",800.OO); 实例化了类 Boss 的派生类对象 b,并为构造函数提供了参数(即姓和名以及固定的周薪)。 第 303 行: b.print(); // static binding 用圆点成员选择运算符显式地调用类 Boss 中的成员函数 print。在编译时就可以知道被调 用函数的对象类型,所以它是静态关联。使用该调用是为了和用动态关联调用函数 print 做一比较。 第 304 行: cout << " earned $ " << b.earnings(); // static binding 用圆点成员选择运算符显式地调用类 Boss 中的成员函数 earnings,这也是一例静态关联。 使用该调用是为了和用动态关联调用函数 earnings 做一比较。 第 305 行: virtualViaPointer(&b); // uses dynamic binding 用派生类对象 b 的地址调用函数 virtualViaPointer(第 331 行)。函数在参数 baseClassPtr 中接收这个地址,该参数声明为 constEmployee *,这正是实现多态性所必须要做的。 第 333 行: baseClassPtr->print() 调用 baseClassPtr 所指向对象的成员函数 print。由于 print 在基类中被声明为虚函数, 因此系统调用了派生类对象的 print 函数(仍然是多态性行为)。该函数调用是一例动态关 联,即用基类指针调用虚函数,以便在执行时才确定调用哪一个函数。 第 334 行: cout<<"earned $ "<earnings(); 调用 baseClassPtr 所指向对象的成员函数 earnings。由于 earnings 在基类中被声明为虚 127 函数,因此系统调用了派生类对象的 earnings 函数,这也是动态关联的一个范例。 第 306 行: virtualViaReference(b); // uses dynamic binding 调用函数 vitualViaRefrence(第 339 行)演示多态性也可以用基类引用调用虚函数来完成。 该函数在参数 baseClassRef 中接收对象 b,该参数声明为 constEmployee&。这就是通过引 用来影响多态行为。 第 341 行: baseClassRef.print(); 调用 baseClassRef 所引用对象的成员函数 print。由于 print 在基类中被声明为虚函数, 因此系统调用了派生类对象的 print 函数。该函数调用是一例动态关联,即用基类引用调 用函数,以便在执行时才确定调用哪一个函数。 第 342 行: cout<< "earned $ "< 6 7 class Shape { 8 public: 9 virtual double area() const { return 0.0; } 10 virtual double volume() const { return 0.0; } 11 12 // pure virtual functions overridden in derived classes 13 virtual void printShapeName() const = 0; 14 virtual void print() const = 0; 15 }; 129 16 17 #endif 18 // Fig. 4.2: point1.h 19 // Definition of class Point 20 #ifndef POINT1_H 22 #include "shape.h" 24 class Point : public Shape { 25 public: 26 Point( int = 0, int = 0 ); // default constructor 27 void setPoint( int, int ); 28 int getX() const { return x; } 29 int getY() const { return y; } 30 virtual void printShapeName() const { cout << "Point: "; } 31 virtual void print() const; 32 private: 33 int x, y; // x and y coordinates of Point 34 }; 35 36 #endif 37 // Fig. 4.2:point1.cpp 38 // Member function definitions for class Point 39 #include "point1.h" 40 41 Point::Point( int a, int b ) { setPoint( a, b ); } 42 43 void Point::sefPoint( int a, int b } 44 { 45 x = a; 46 y = b; 47 } 48 49 void Point::print() const 5O { cout << '[' << x << ", "<< y << '] '; } 51 // Fig. 10.2: circle1.h 52 // Definition of class Circle 53 #ifndef CIRCLE1_H 54 #define CIRCLE1_H 55 #include "point1.h" 56 57 class Circle : public Point { 58 public: 59 // default constructor 60 Circle( double r = 0.0, int x = 0, int y = 0 ); 61 130 62 void setRadius( double ); 63 double getRadius() const; 64 virtual double area() const; 65 virtual void printShapeName() const { cout << "Circle: "; } 66 virtual void print() const; 67 private: 68 double radius; // radius of Circle 69 }; 7O 71 #endif 72 // Fig. 4.2: circlel.cpp 73 // Member function definitions for class Circle 74 #include "circie1.h" 75 76 Circle::Circle( double r, int a, int b ) 77 : Point( a, b ) // call base-class constructor 78 { setRadius( r ); } 79 80 void Circle::setRadius( double r ) { radius = r > 0 ? r : 0; } 81 82 double Circle::getRadius() const { return radius; } 83 84 double Circle::area() const 85 { return 3.14159 * radius * radius; } 86 87 void Circle::print() const 88 { 89 Point::print(); 90 cout << "; Radius =" << radius; 91 } 92 // Fig. 4.2: cylindrl.h 93 // Definition of class Cylinder 94 #ifndef CYLINDR1_H 95 #define CYLINDR1_H 96 #include "circle1.h" 97 98 class Cylinder : public Circle { 99 public: 100 // default constructor 101 Cylinder( double h = 0.0, double r = 0.0, 102 int x = 0, int y = 0 ); 103 104 void setHeight( double ); 105 double getHeight() const; 131 106 virtual double area() const; 107 virtual double volume() const; 108 virtual void printShapeName() const {cout << "Cylinder: ";} 109 virtual void print() const; 110 private: 111 double height; // height of Cylinder 112 }; 113 114 #endif 115 // Fig. 4.2: cylindr1.cpp 116 // Member and friend function definitions for class Cylinder 117 #include "cylindr1.h" 118 119 Cylinder::Cylinder( double h, double r, int x, int y ) 120 : Circle( r, x, y ) // call base-class constructor 121 { setHeight( h ); } 122 123 void Cylinder::setHeight( double h ) 124 { height = h > 0 ? h : 0; } 125 126 double Cylinder::getHeight() const { return height; } 127 128 double Cylinder::area() const 129 { 130 // surface area of Cylinder 131 return 2 * Circle::area() + 132 2 * 3.14159 * getRadiusO * height; 133 } 134 135 double Cylinder::volume() const 136 { return Circle::area() * height; } 137 138 void Cylinder::print() const 139 { 140 Circle::print(); 141 cout << "; Height =" << height; 142 } 143 // Fig. 10.2: figl0_02.cpp 144 // Driver for shape, point, circle, cylinder hierarchy 145 #include 146 #include 147 #include "shape.h" 148 #include "point1.h" 149 #include "circle1.h" 132 150 #include "cylindr1.h" 151 152 void virtualViaPointer( const Shape * ); 153 void virtualViaReference( const Shape & ); 154 155 int main() 156 { 157 cout << setiosflags( ios::fixed | ios::showpoint ) 158 << setprecision( 2 ); 159 160 Point point( 7, 11 ); // create a Point 161 Circle circle( 3.5, 22, 8 ); // create a Circle 162 Cylinder cylinder( 10, 3.3, 10, 10 ); // create a Cylinder 163 164 point.printShapeName(); // static binding 165 point.print(); // static binding 166 cout << '\n'; 167 168 circle.printShapeName(); // static binding 169 circle.print(); // static binding 170 cout << '\n'; 171 172 cylinder.printShapeName(); // static binding 173 cylinder.print(); // static binding 174 cout << "\n\n"; 175 176 Shape *arrayOfShapes[ 3 ]; // array of base-class pointers 177 178 // aim arrayOfShapes[ 0 ] at derived-class Point object 179 arrayOfShapes[ 0 ] = &point; 180 181 // aim arrayOfShapes[ 1 ] at derived-class Circle object 182 arrayOfShapes[ 1 ] = &circle; 183 184 // aim arrayOfShapes[ 2 ] at derived-class Cylinder object 185 arrayOfShapes[ 2 ] = &cylinder; 186 187 // Loop through arrayOfShapes and call virtualViaPointer 188 // to print the shape name, attributes, area, and volume 189 // of each object using dynamic binding. 190 cout << "Virtual function calls made off" 191 << "base-class pointers\n"; 192 193 for ( int i = 0; i < 3; i++ ) 133 194 virtualViaPointer( arrayOfShape[ i ] ); 195 196 // Loop through arrayOfShapes and call virtualViaReference 197 // to print the shape name, attributes, area, and volume 198 // of each object using dynamic binding. 199 cout << "Virtual function calls made off" 200 << "base-class references\n"; 201 202 for (int j = 0; j < 3; j++ ) 203 virtualViaReference( *arrayOfShapes[ j ] ); 204 205 return 0; 206 } 207 208 // Make virtual function calls off a base-class pointer 209 // using dynamic binding. 210 void virtualViaPointer( const Shape *baseClassPtr ) 211 { 212 baseClassPtr->printShapeName(); 213 baseClassPtr->print(); 214 cout << "\nArea = "<< baseClassPtr->area() 215 << "\nVolume =" << baseClassPtr->volume() << "\n\n"; 216 } 217 218 // Make virtual function calls off a base-class reference 219 // using dynamic binding. 220 void virtualViaReference( const Shape &baseClassRef ) 221 { 222 baseClassRef.printShapeName(); 223 baseClassRef.print(); 224 cout << "\nArea = "<< baseClassRef.area() 225 << "\nVolume "<< baseClassRef.volume() << "\n\n"; 226 } 输出结果: Point: [ 7, 11 ] Circle: [ 22, 8 ]; Radius 3.50 Cylinder: [ 10, 10 ] ; Radius = 3.30; Height = 10.00 Virtual function calls made off base-class pointers Point: [7, 11] Area = 0.00 Volume = 0.00 134 Circle: [ 22, 8]; Radius = 3.50 Area = 38.48 Volume = 0.00 Cylider: [ 10,10 ]; Radius = 3.30; Height = 10.00 Area = 275.77 Volume = 342.12 Virtual function calls made off base-class pointers Point: [ 7, 11] Area = 0.00 Volume = 0.00 Circle: [ 22, 8] ; Radius = 3.50 Area = 38.48 Volume = 0.00 Cylinder:[10, 10]; Radius = 3.30; Height = 10.00 Area = 275.77 Volume = 342.12 图 4.2 定义抽象基类 Shape 基 类 Shape 由 三 个 public 虚 函 数 组 成 , 不 包 含 任 何 数 据 。 函 数 print 和 printShapeName 是纯虚函数,因此它们要在每个派生类中重新定义。函数 area 和 volume 都返回 0.0,当派生类需要对面积(area)和(或)体积(volume)有不同的计算方法时,这些函 数就需要在派生类中重新定义。注意 Shape 是个抽象类,包含一些“不纯”的虚函数 (area 和 volume)。抽象类可以包含非虚函数和通过派生类继承的数据。 类 Point 是通过 public 继承从类 Shape 派生来的。因为 Point 没有面积和体积(均为 0.0),所以类中没有重新定义基类成员函数 area 和 volume,而是从类 Shape 中继承这 两个函数。函数 printShapeName 和 print 是虚函数(在基类被定义为纯虚函数)的实现,如 果不在类 Point 中重新定义这些函数,那么 Point 仍然为抽象类则不能实例化 Point 对象。 其他成员函数包括:将新的 x 和 y 坐标值赋绐 Point 对象(即点)的一个”set”函数和返 回 Point 对象的 x 和 y 坐标值的“get”函数。 类 Circle 是通过 public 继承从类 Point 派生来的。因为它没有体积,所以类中没有 重新定义基类成员函数 volume,而是从类 Shape 中继承。Circle 是有面积的,因此要重新 定义函数 area。函数 printShapeName 和 print 是虚函数(在基类中被定义为纯虚函数)的实 现。如果此处不重新定义该函数,则会继承类 Point 中该函数的版本。其他成员函数包括 为 Circle 对象设置新的 radius(半径值)的“set”函数和返回 Circle 对象的 radius 的 “get”函数。 类 Cylinder 是通过 public 继承从类 Circle 派生来的。因为 Cylinder 对象的面积和体 积同 Circle 的不同,所以需要在类中重新定义函数 area 和 volume。函数 printShapeName 和 print 是虚函数(在基类中被定义为纯虚函数)的实现。如果此处不重新定义该函数,则 135 会继承类 Circle 中该函数的版本。 类中还包括一个设置 Cylinder 对象 height(高度)的“set”函数和一个读取 Cylinder 对 象(圆柱体)的 height 的”get”函数。 驱动程序一开始就分别实例化了类 Point 的对象 point、类 Circle 的对象 circle 和类 Cylinder 的对象 cylinder。程序随后调用了每个对象的 printShapeName 和 print 函数, 并输出每一个对象的信息以验证对象初始化的正确性。每次调用 printShapeName 和 print(第 164 行到第 173 行)都使用静态关联,编译器在编译时知道调用 printShapeName 和 print 的每种对象类型。 接着把指针数组 arrayOfShapes 的每个元素声明为 Shape*类型,诙数组用来指向每 个派生类对象。首先把对象 point 的地址赋给了 arrayOfShapes[O](第 179 行)、把对象 circle 的 地 址 赋 给 了 arrayOfShapes[1]( 第 182 行 ) 、 把 对 象 cylinder 的 地 址 赋 给 了 arrayOfShapes[2](第 185 行)。 然后用 for 结构(第 193 行)遍历 arrayOfShapes 数组,并对每个数组元素调用函数 virtualViaPointer (第 194 行): virtualViaPointer(arrayOfShapes[ i ]); 函数 virtualViaPointer 用 baseClassPtr(类型为 constShape*)参数接收 arrayOfShapes 数组中存放的地址。每次执行 virtualViaPointer 时,调用下列 4 个虚函数: baseClassPtr->printShapeName() baseClassPtr->print() baseClassPtr->area() baseClassPtr->Volume() 这些调用方法对执行时 baseClassPtr 所指的对象调用一个虚函数,对象类型无法在编 译时确定。输出中显示了对每个类调用的相应函数。首先,辅出字符串“Point:”和相应的 point 对象,面积和体积的计算结果都是 0.00。然后,输出字符串“Circle:”和 circle 对 象的圆心及半径,程序计算出了对象 circle 的面积,返回体积值为 0.00。最后,输出字符 串“Cylinder:”以及相应的 cylinder 对象的底面圆心、半径和高,程序计算出了对象 cylinder 的面积和体积。所有调用函数 printShapeName、print、area 以及 volume 的虚函数 都是在运行时用动态关联解决的。 最后用 for 结构(第 202 行遍历 arrayOfShapes 数组,并对每个数组元素调用函数 virtualViaReference (第 203 行): virtualViaReference(*arrayofShapes[ j ]); 函 数 virtualViaReference 用 baseClassRef( 类 型 为 constShape&) 参 数 接 收 对 arrayOfShapes 数组中存放的地址的引用(通过复引用)。每次执行 virtualViaReference 时, 调用下列 4 个虚函数: baseClassRef.printShapeName() baseClassRef.print() baseClassRef.area() baseClassRef.volume() 这些调用方法对执行时 baseClassRef 所指的对象调用上述函数。输出中使用基类引用 与使用基类指针时产生的结果是相同的。 136 4.10 多态、虚函数和动态关联 C++中的多态比较容易编程。虽然还可以像 C 语言等非面向对象语言中一样进行多态 编程,但这种做法既复杂,又危险,需要进行指针操作。本节介绍 C++如何在内部实现多 态、虚函数和动态关联,以便了解这些功能是如何实现的,更重要的是帮助读者了解多态 的开销(除了内存占用和处理器时间)。这样就可以更清楚地确定何时使用多态,何时不用 多态。第 20 章“标准模板库(STL)”中将会介绍 STL 组件不用多态和虚函数,从而避免运 行开销,达到符合 STL 特定要求的最优性能。 首先,我们要介绍 C++编译器在编译时建立怎样的数据结构来支持运行时的多态。然 后我们介绍执行程序如何利用这些数据结构执行虚函数和实现与多态相关的动态关联。 C++ 编 译 有 一 个 或 几 个 虚 函 数 的 类 时 , 对 该 类 建 立 虚 函 数 表 (virtualfunctiontable,vtableL vtable 让执行程序选择每次执行类的虚函数时正确的 实现方法。图 4.3 演示了 Shape、Point、Circle 和 Cylinder 类的虚函数表。 Shape 类的 vtable 中,第一个指针指向该类 area 函数的实现方法,即返回面积 0.0 的函 数 。 第 二 个 指 针 指 向 该 类 volume 函数的实现方法,即返回体 积 0.0 的 函 数 。 printShapeName 和 print 函数都是纯虚函数,没有实现方法,因此函数指针都设置为 0。 类中的 vtable 有一个或几个 0 指针时,称为抽象类。类中的 vtable 没有 0 指针时,称为 具体类(如 Point、Circle 和 Cylinder)。 Point 类继承 Shape 类的 area 和 volume 函数,因此编译器只是把 Point 类 vtable 表 中 的 这 两 个 指 针 设 为 Shape 类 中 area 和 volume 指 针 的 副 本 。 Point 类 将 函 数 printShapeName 重定义为打印”Point:”,使函数指针指向 Point 类的 printShapeName 函数。Point 类还重定义 print,使相应函数指针指向 Point 类打印[x,y]的函数。 Circle 类 vtable 表 中 Circle 的 area 函 数 指 针 指 向 Circle 的 area 函 数 ( 返 回 πr2)。volume 函数指针只是从 Point 类复制,是原先从 Shape 向 Point 复制的指针 。 printShapeName 函数指针指向 Circle 版本打印”Circle:”的函数。print 函数指针指向 Circle 类的打印[x,y]r 的函数。 Cylinder 类 vtable 表中 Cylinder 的 area 函数指针指向 Cylinder 的 area 函数,该函 数计算 Cylinder 的表面积 2πr2+2πrh。Cylinder 的 volume 函数指针指向 volume 函数,返 回 πr2h。Cylinder 的 priintShapeName 函数指针指向打印"Cylinder"的函数。Cyelinder 的 print 函数指针指向 Cylinder 类打印[x,y]rh 的函数。 多态是通过复杂的数据结构实现的,涉及三层指针。前面只介绍了其中一层,即 vtable 中的函数指针。这些指针在调用虚函数时指向实际执行的函数。 下面要考虑第二层指针。实例化带虚函数的类对象时,编译器在对象前面连接该类的 vtable 指 针(注意:这个指针通常放在对象前面,但也不一定非要这样实现)。 第三层指针是接受虚函数调用的对象句柄(这个句柄也可以是个引用)。 下面看看典型的虚函数调用如何执行。考虑函数 virtualViaPointer 中的下列调用: baseClassPtr->printShapeName() 假设 baseClassPtr 包含 arrayOfShapes[1]的指针,即对象 circle 的地址。则编译器编译这 条语句时,它确定调用实际上是对基类指针进行,并且 printShapeName 是个虚函数。 然后编译器确定 printShapeName 是每个 vtable 表中的第三个项目。要找到这个项目, 编译器发现需要跳过前两个项目。为此,编译器编译 8 个字节的偏移量或位移量(在目前 流行的 32 位机器中,每个指针为 4 个字节),将其编译到机器语言目标码中,用于执行虚 137 函数调用。 然后编译器产生完成下列工作的代码(说明:下列编号对应图 4.3 中圆圈内的数字): 1.从 arrayOfShapes 中选择第 i 个项目(这里是对象 circle 的地址)并将其传递给 virtualViaPointer,从而将 baseClassPtr 设置为指向 circle。 2.复引用指针,取得 circle 对象,它以指向 Circlevtable 的指针开始。 3. 复引用 circle 的 vtable 指针,取得 Circlevtable。 4.跳过 8 个字节位移,选择 printShapeName 函数指针。 5.复引用 printShapeName 函数指针,构成要执行的实际函数名,并用函数调用运 算符()执行相应的 printShapeName 函数和打印字符串”Circle:”。 图 4.3 的数据结构看起来有点复杂,但这些调节大部分由编译器负责,程序员不必 担心,C++中的多态编程并不复杂。 每个虚函数调用中发生的指针复引用操作和内存访问需要增加一些执行时间。vtable 和 vtable 指针要占用一些内存。 现在,已经有了关于虚函数调用如何工作的足够细节,可以确定其是否适合具体的 应用程序。 性能提示 4.1 多态性(它是用虚函数和动态关联实现的)是高效的,程序更使用这种功能对系统性 能的影响极小。 性能提示 4.2 虚函数和动态关联使得多态性编程和 switch 逻辑编程形成了对照。C++优化编译器通 常能生成至少和手写的基于 switch 逻辑的代码具有同样效率的代码。对大多数应用程序 而言,多态的开销是可以接受的。但有时则不能接受多态的开销,例如性能要求很高的实 时应用程序。 小 结 ●虚函数和多态性使得设计和实现易于扩展的系统成为可能。在程序开发过程中,不 论类是否已经建立,程序员都可以利用虚函数和多态性编写处理这些类对象的程序。 ●虚函数和多态性的程序设计无需使用 switch 逻辑。程序员可以用虚函数机制自动 完成等价的逻辑,因而避免与 swilch 逻辑有关的各种各样的错误。如果要让客户代码确 定对象类型和表达,则是低质的类设计。 ●派生类在需要的时候可以自己提供基类的虚函数实现,否则就使用基类的实现。 ●如果通过用名字和圆点成员选择运算符引用一个特定的对象来调用虚函数,则引 用是在编译时确定的(称为静态关联).被调用的虚函数是为该特定对象的类定义的函数 或继承该类的函数。 ●在许多情况下,定义不实例化为任何对象的类很有用处,这种类称为“抽象类”。 因为抽象类要作为基类被其他类继承,所以通常也把它称为“抽象基类”。抽象基类不能 用来建立实例化的对象。 138 ●可以建立实例化对象的类称为具体类。 ●将带有虚函数的类中的一个或者多个虚函数声明为纯虚函数,则该类就成为抽象 类。纯虚函数是在声明时“初始化值”为。的函数。 ●如果某个类是从一个带有纯虚函数的类派生出来,并且没有在该派生类中提供该 纯虚函数的定义,则该虚函数在派生类中仍然是纯虚函数,因而该派生类也是一个抽象 类(不能有任何对象)。 ●C++支持多态性。所谓多态性是指:通过继承而相关的不同的类,他们的对象能够 对同一个函数调用做出不同的响应。 ●多态性是通过虚函数实现的。 ●当通过基类指针或引用请求使用虚函数时,c++会在与对象关联的派生类中正确的 选择重定义的函数。 ●使用虚函数和多态性能够使成员函数的调用根据接收到该调用的对象的类型产生 不同的动作。 ●尽管不能实例化抽象基类的对象,但是可以声明抽象基类的指针。当实例化了具体 类的对象后,可以用这种指针使派生类对象具有多态操作能力。 ●使用动态关联(也叫滞后关联)可以向系统中添加新类。对于要被编译的虚函数调用, 编译时可以不必知道对象的类型。在运行时,虚函数调用和被调用对象的成员函数相匹配。 ●动态关联可以使独立软件供应商(ISV)在不透露其秘密的情况下发行软件。发行的软 件可以只包括头文件和对象文件,不必透露源代码。软件开发者可以利用继承机制从 ISV 提供的类中派生出新类。和 ISV 提供的类一起运行的软件也能够和派生类一起运行,并且 能够使用 (通过动态关联)这些派生类中重定义的虚函数。 ●动态关联要求在运行时把对虚函数的调用转换为对应类的虚函数版本。虚函数表 (称为 vtable)实现为包含函数指针的数组,每一个包含虚函数的类都有一个 vtable。对于 类中的每一个虚函数,viable 都有一个包含一个函数指针的项目,该指针指向类的对象 所要使用的虚函数版本。特定类所要使用的虚函数可能是该类中重新定义的函数,也可能 是从较高层的基类直接或间接继承来的函数。 ●当基类提供了一个成员函数并将它声明为 virtual 时,泥生类可以但不是必须重 定义该虚函数,因此派生类可以使用基类的虚函数版本,这会在 vtable 中指明。 ●带有虚函数的类的每一个对象都包含一个指向该类 viable 的指针。系统在运行时 会获取并复引用正确的函数指针来完成函数调用,查找 vtable 和复引用指针只需要极少 的运行时间的开销,一般少于最优的客户代码。 ●如果基类中包含虚函数,把其析构函数声明为虚析构函数。这样做将会使所有派生 类的析构函数自动成为虚析构函数(即使它们与基类析构函数的函数名不同)。这时,如果 delete 运算符用于指向派生类对象的基类指针,而程序中又显式地用该运算符删除每一 个对象,那么系统会调用相应类的析构函数。 ●任何类在 vtable 中有一个或几个 0 指针时就成为抽象类。而没有 0 指针时就成为具 体类(如 Point、Circle 和 Cylinder)。 139 第五章 模板 教学目标 ●用函数模板生成相关(重载)函数组 ●区分函数模板与模板函数 ●用类模板生成相关类型组 ●区分类楼板与模板类 ●了解如何重载模板函数 ●了解模板、友元、继承与静态成员之间的关系 5.1 简介 本章介绍 C++最强大的特性之一 —— 模板。模板使我们可以用一个代码段指定一组 相关(重载)函数(称为模板函数)或一组相关类(称为模板类)。 我们可以对数组排序函数编写一个函数模板,然后 Cc++自动生成模板函数,可以对 int 数组、float 数组和字符串数组等等进行排序。 第 3 章介绍了函数模板。如果读者没有阅读该章,则这里再提供一些介绍和例子。 我们可以对堆栈类编写一个类模板,然后让 C++自动生成如 int、float 和 string 堆 栈类的类模板。 注意区分函数摸板与模板函数:函数模板和类模板像是具有各种形状的模板,而模 板函数和模板类则相当于按照模板描绘,其形状都是相同的.只是画上不同的颜色。 软件工程视点 5. 1 模板是 C++的软件复用的功能之一。 本章介绍一些函数模板和类模板的例子,并介绍模扳与其他 C++特性(如重载、继承、 友元和 static 成员)之间的关系。 这里介绍的模扳机制的设计和细节基于 Bjarne Stroustrup 的论文《Parameterized Types for C++》,发表于 1988 年 10 月在科罗拉多州丹佛举办的 USENIX C++会议上 (Proceedings of the USENIX C++ Conference)。 本章只是关于模板问题的简介,第 20 章“标准模板库(STL)”将深入介绍模板容器类、 迭代器和 STL 算法。第 20 章有几十个基于摸板的“有生命力的代码”,演示了更复杂的 模板编程技术。 5.2 函数模板 重载函数通常是基于不同的数据类型完成类似的操作。如果对每种数据类型的操作是 相同的,那么用函数模扳完成这项工作更为简洁和方便。程序员对函数模板的定义只编写 一次。基于调用函数时提供的参数类型,C++自动产生单独的目标代码函数来正确地处理 140 每种类型的调用。在 C 浯言中,这个任务是用预处理指令#define 建立的宏完成的(见第 17 章)。但是,宏可能会产生副作用,并且使编译器不能进行类型检查。函数模板和宏一样的 简洁,并且还能让编译器进行全面的类型检查。 测试与调试提示 5.1 函数模板和宏一样允许软件复用。但与宏不同的是,函数模板还可以消除许多类型错 误,因为 C++提供了安全的全面类型检查。 所有的函数模板定义都是用关键字 template 开始的,该关键字之后是用尖括号<>括 起来的形式参数表。每一个形式参数之前都有关健字 class,例如: template 或 template 或 template 内部类型和自定义类型可用来指定传递给函数的参数类型、函数返回类型和声明函数中变 量,函数模板中的形式参数的用法与之类似。该函数定义的方式与定义其他函数类似。注 意关键字 class 指定函数模板类型参数,实际上表示“任何内部类型或用户自定义类 型”。 常见编程错误 5.1 函数模板的每个形式类型参数之前不放置关键字 class(或新的关键字 typename)。 下面看看图 5.1 的 printArray 函数模板,这个函数的用法见图 5.2 的完整程序。 1 template< class T > 2 void printArray( const T *array, const int count ) 3 { 4 for ( int i = 0; i < count; i++ ) 5 cout << array [i ] << " "; 6 7 cout << endl; 8 } 图 5. 1 函数模板 该函数模板把惟一的形式参数 T(T 一般作为类型参数)声明为函数 printArray 打印的 数组类型。当编译器检测到程序源代码中调用函数 printArray 时,用 printArray 的第一 个参数的类型替换掉整个模板定义中的 T,并建立用来打印指定类型数组的一个完整的 模板函数,然后再编译这个新建的函数。图 5.2 的程序演示了三个 printArray 函数,这 三个函数分别需要一个 int 类型的数组、一个 double 类型的数组和一个 char 类型的数组 。 int 类型数组的实例函数如下所示: void printArray( const int *array, const int count ) { for (int i = O; i < count; i++ ) 141 cout << array [ i ] <<" "; count << endl; } 模板函数中的每一个形式参数要在函数参数表中至少出现一次。形式参数的名字可以只在 模板函数的形式参数表中出现一次。同一个形式参数名可用于多个模板函数。 图 5.2 的程序反映了模板函数 printArray 的用法。程序首先实例化 int 数组 a、double 数组 b 和 char 数组 c,长度分别为 5、7、6。然后调用 pfintArray 打印每个数组, 一次用 a 的第一个参数,类型为 int*;一次用 b 的第一个参数,类型为 double*;一次用 c 的第一个参数,类型为 char*。 例如,下列语句: printArray(a,aCount); 使编译器实例化 printArray 模板函数,类型参数 T 为 int。下列语句: printArray(b,bCount); 使编译器实例化第二个 pfintArry 模板函数,类型参数 T 为 double。下列语句: printArray(c,cCount); 使编译器实例化第三个 printArray 模板函数,类型参数 T 为 char。 本例中,模板机制使程序员不必用下列原型编写三个重载函数: void printArray( const int*, const int ); void printArray( const double*, const int ); void printArray( const char*, const int ); 1 // Fig 5.2: fig12_02.cpp 2 // Using template functions 3 #include 4 5 template< class T > 6 void printArray( const T *array, const int count ) 7 { 8 for (int i = 0; i < count; i++ ) 9 cout << array[ i ] <<" "; 10 11 cout << endl; 12} 13 14 int main() 15 { 16 const int aCount = 5, bCount = 7, cCount = 6; 17 int a[ aCount ] = { 1, 2, 3, 4, 5 }; 18 double b[ bCount ] = { 1.1, 2.2, 3.3, 4.4, 6.5, 6.6, 7.7 }; 19 char c[ cCount ] = "HELLO"; // 6th position for null 20 21 cout << "Array a contains:" << endl; 22 printArray( a, aCount ); // integer template function 23 24 cout << "Array b contains:" << endl; 142 25 printArray( b, bCount ); // double template function 26 27 cout << "Array c contains:" << endl; 28 printArray( c, cCount ); // character template function 29 30 return 0; 31 } 输出结果: Array a contains: 1 2 3 4 5 Array b contains: 1.1 2.2 3.3 4.4 5.5 6.6 7.7 Array c contains: H E L L O 图 5.2 使用模板函数 性能提示 5. 1 模板提供了软件复用的好处。请记住,尽管模板只编写一次,但程序中仍然实例化多 个模板类的副本。这些副本会占用大量内存。 12. 3 重载模板函数 模板函数与重载是密切相关的。从函数模板产生的相关函数都是同名的,因此编译器用重 载的解决方法调用相应函数。 函数模板本身可以用多种方式重载。我们可以提供其他函数模板,指定不同参数的相 同函数名。例如,图 5.2 的 printArray 函数模板可以用另一 printArray 函数模板重载, 用参数 lowSubscriPt 和 highSubscript 指定要打印的数组部分(见练习 5.4)。 函数模板也可以用其他非模板函数(同名而参数不同)重载。例如,图 5.1 的 printArray 函数模板可以用一个非模板函数重载,指定以整齐的表格式分栏打印字符串 数组(见练习 5.5)。 常见编程错误 5.2 如果使用用户自定义类的类型调用模板,而模板时该类型对象使用==、+、<=等运算 符,那么这些运算符需要重载。如果不重载这些运算符,则会发生错误,固为编译器在这 些函数不存在的情况下仍然调用这些重载的运算符函数。 编译器通过匹配过程确定调用哪个函数。首先,编译器寻找和使用最符合函数名和参 数类型的函数调用。如果找不到,则编译器检查是否可以用函数模板产生符合函数名和参 数类型的模板函数。 过去,这种与模板的匹配过程要求所有参数类型都完全匹配,而不能进行自动转换。 143 现在已经没有这么严格,可以采用通常的重载规则。 常见编程错误 5.3 编译器通过匹配过程确定调用哪个函数,如果找不到匹配或产生多个匹配,就全产 生编译错误。 5.4 类模板 堆栈独立于栈中数据项的类型,这一点不难理解。但是,用程序实现堆栈的时候又必 须提供数据类型,这为实现软件的复用性提供了一次很好的机会。所用的方法是描述一个 通常意义上的堆栈,然后建立这个类的实例类。所建的实例类虽然是通用类的副本,但是 它具有指定的类型。C++的模板类提供了这种功能。 软件工程视点 5.2 类模板通过实例化通用类的特定版本提高了软件的复用性。 为了说明如何定制通用类的模板以形成指定的模板类,模板类需要一种或多种类型参 数,所以模板类也常常称为参数化类型。 需要生成多种模板类的程序员只需简单地编写—个通用类模板的定义。在需要用模板 建立一个新类的时候,程序员只需要用一种简洁的表示方法,编译器就会写出模板类的 源代码。例如,堆栈类的模板可以作为编写各种类型堆栈的基础(如 float 类型、int 类型 或 char 类型的堆栈等等)。 图 5.3 中的程序定义了 Stack(堆栈)的类模板。模板类与通常的类定义没有什么不同, 只是以如下所示的首部开头(第 8 行): template 上述首部指出了这是一个类模板的定义,它有一类型参数 T(表示所要建立的 Stack 类的 类型)。程序员不需要专门使用标识符 T,任何标识符都可以使用。Stack 中存储的元素类 型在 Stack 类首部和成员函数定义中一般表示为 T。稍后将介绍如何将 T 与特定类型(如 double 或 id)相关联。 I // Fig. 5.3: tstackl.h 2 // Class template Stack 3 #ifndef TSTACK1_H 4 #define TSTACK1 H 5 6 #include 7 8 template< class T > 9 class Stack { 10 public: 1l Stack( int = 10 ); // default constructor (stack size 10) 12 ~Stack() { delete [] stackPtr; } // destructor 13 bool push( const T& ); // push an element onto the stack 14 bool pop( T& ); // pop an element off the stack 144 15 private: 16 int size; // # of elements in the stack 17 int top; // location of the top element 18 T *stackPtr; // pointer to the stack 19 20 bool isEmpty() const { return top == -1; } // utility 21 bool isFull() const { return top == size - 1; } // functions 22 }; 23 24 // Constructor with default size 10 25 template< class T > 26 Stack< T >::Stack( int S ) 27 { 28 size = S > 0 ? S : 10; 29 top = -1; // Stack is initially empty 30 stackPtr = new T[ size ]; // allocate space for elements 31 } 32 33 // Push an element onto the stack 34 // return true if successful, false otherwise 35 template 36 bool Stack< T >::push( const T &pushValue ) 37 { 38 if (!isFull() ) { 39 stackPtr[ ++top ] = pushValue; // place item in Stack 40 return true; // push successful 41 } 42 return false; // push unsuccessful 43 } 44 45 // Pop an element off the stack 46 template 47 bool Stack< T >::pop( T &popValue ) 48 { 49 if (!isEmpty() ) { 50 popValue = stackPtr[ top-- ]; // remove item from Stack 51 return true; // pop successful 52 } 53 return ffalse; // pop unsuccessfu 54 } 55 56 #endif 57 // Fig. 5.3: fig12_03.cpp 58 // Test drive for stack template 145 59 #include 60 #include "tstackl.h" 61 62 int main() 63 { 64 Stack< double > doubleStack( 5 ); 65 double f = 1.1; 66 cout << "Pushing elements onto doubleStack\n"; 67 68 while ( doubleStack.push( f ) ) { // success true returned 69 cout << f << ' '; 70 f += 1.1; 71 } 72 73 coout << "\nStack is full. cannot push "<< f 74 << "\n\nPopping elements from doubleStack\n"; 75 76 while ( doubleStack.pop( f ) ) // success true returned 77 cout << f << ' '; 78 79 cout << "\nStack is empty. Cannot pop\n"; 80 81 Stack< int > intStack; 82 int i = 1; 83 cout << "\nPushing elements onto intStack\n"; 84 85 while ( intStack.push( i ) ) { // success true returned 86 cout << i << ' '; 87 ++i; 88 } 89 90 cout << "\nStack is full. Cannot push " << i 91 << "\n\nPopping elements from intStack\n"; 92 93 while ( intStack.pop( i ) ) // success true returne 94 cout << i << ' '; 95 96 cout << "\nStack is empty. Cannot pop\n"; 97 return O; 98 } 输出结果: Pushing elements onto doubleStack 1.1 2.2 3.3 4.4 5.5 146 Stack is full. Caunot push 6.6 Pepping elements from doubleStack 5.5 4.4 3.3 2.2 1.1 Stack is empty. Cannot pop Pushing elements onto intStack 1 2 3 4 5 6 7 8 9 10 Stack is full. Cannot push 11 Popping elements form intStack 10 9 8 7 6 5 4 3 2 1 Stack is empty. Cannot pop 图 5.3 演示类模板 Stack 下面建立一个测试堆栈类模板(见图 5.5 的输出)的驱动程序(函数 main)。程序在开始 的时候实例化了一个大小为 5 的对象 doublestack。该对象声明为类 Stack<称为 double 类型的 Stack 类)的对象。为了产生出 double 类型的 Stack 类的源代码,编译器会 自动把模板中的参数类型 T 替换成 double。尽管程序看不到这个源代码,但仍将其放进源 代码中编译。 然 后 程 序 成 功 地 把 1.1 、 2.2 、 3.3 、 4.4 和 5.5 这 几 个 double 值 压 入 (push) 堆 栈 doubleStack。当试图将第六个值压人堆栈中的时候,push 循环中止(栈已经满了,因为它 只能容纳 5 个元素)。 然后程序再将这 5 个元素弹出(pop)堆栈(以 LIFO 顺序)。在试图弹出第六个元素的时, 出栈循环中止,因为这时堆栈已经空了。 接下来,程序用下面的声明语句实例化了一个 int 类型的堆栈 intStaek: StackintStack 因为没有指定堆栈的大小,所以使用默认构造函数(第 11 行)中的默认值 10 作为堆栈的大 小。重复上述操作,用循环结构不断向 intStaek 中压入整数值,直到栈满为止,然后再 循环从堆栈中弹出数值,直到栈空为止。 在类模板首部以外的成员函数定义都要以下面的形式开头: template 然后,成员函数的定义与普通成员函数的定义相似,只是 Stack 元素的类型要用类型参 数 T 表示。二元作用域运算符和 Stack类模板将成员函数的定义与正确的类模板范围 联系起来。本例中,类名是 Stack。当建立类型为 Stack的对象 doubleStack 的时候,Stack 的构造函数使用 new 建立了一个表示堆栈的 double 类型数组。因此,对于 语句: stackPtr = new T [size]; 编译器将在模板类 Stack中生成下面的代码: stackPtr new double[size]; 注意图 5.3 函数 main 中的代码即 main 上半部分的 doubleStack 操作和 main 下半部 分的 intStaek 操作基本相同。这里又可以使用函数模板。图 5.4 的程序用函数模板 testStack 进行与图 5.3 相同的工作,将一系列值压入 Stack中并从 Stack中弹出 147 数值。函数模板 testStack 用参数 T 表示 Stack中保存的数据类型。该函数模板取 4 个 参数:Stack类型对象的引用、类型为 T 的值用作压入 Stack的第一个值、类型为 T 的值用作压入 Stack的增量值以及 const char*类型的字符串表示输出的 Stack对 象名。函数 main 只是实例化 Stack类型对象 doubleStack 和实例化 Stack类型对象 intStaek,如下所示(第 37 行到第 38 行): testStack(doubleStack,1.1,1.1,"doubleStack"); teststack(intStack,1,1,"intstack"); 注意图 5.4 的输出与图 5.3 的输出一致。 1 // Fig. 5.4: fig12_04.cpp 2 // Test driver for Stack template. 3 // Function main uses a function template to manipulate 4 // objects of type Stack< T >. 5 #include 6 #include "tstack1.h" 7 8 // Function template to manipulate Stack< T > 9 template< class T > 10 void testStack( 11 Stack< T > &theStack, // reference to the Stack< T > 12 T value, // initial value to be pushed 13 T increment, // increment for subsequent values 14 const char *stackName ) // name of the Stack < T > object 15 { 16 cout << "\nPushing elements onto "<< stackName << '\n'; 17 18 while ( theStack.push( value ) ) { // success true returned 19 cout << value << ' '; 20 value += increment; 21 } 22 23 cout << "\nStack is full. Cannot push" << value 24 << "\n\nPopping elements from" << stackName << '\n'; 25 26 while ( theStack.pop( value ) ) // success true returned 27 cout << value << ' '; 28 29 cout << "\nStack is empty. Cannot pop\n"; 3O } 31 32 int main() 33 { 34 Stack< double > doubleStack( 5 ); 35 Stack< int > intStack; 36 148 37 testStack( doubleStack, 1.1, 1.1, "doubleStack" ); 30 testStack( intStack, 1, 1, "intStack" ); 39 40 return O; 41 } 输出结果: Pushing elements onto doubleStack 1.2 2.2 3.3 4.4 5.5 Stack is full. Cannot push 1.6 Popping elements from doubleStack 5.5 4.4 3.3 2.2 1.1 Stack is empty. Cannot pop Pushing elements onto intStack 1 2 3 4 5 6 7 8 9 10 Stack is full. Cannot push 11 Popping elements form intStack 10 9 8 7 6 5 4 3 2 1 Stack is empty. Cannot pop 图 5.4 向函数模板传递 Stack 模板对象 5.5 类模板与无类型参数 上节的 Sstack 类模扳只用模板首部的类型参数,也可以使用无类型参数(non-type parameter),无类型参数可以有默认参数,一般将无类型参数当作 Const 处理。例如,模 板首都可以取 int elements 参数,如下所示: template // note non-type parameter 然后下列声明: Stack。类的首部可以包含 private 数据成员,数组声明 如下: T stackHolder[elements]; // array tO hold stack contents 性能提示 5.2 如果可能,在编译时指定容器类(如数组类和堆栈类)的长度(可能通过非类型模板长 度参数)可以消除用 new 动态生成空间的执行时开销。 软件工程视点 5.3 149 如果可能,在编译时指定容器类的长度(可能通过非典型模板长度参数)以避免 new 无 法取得所要内存时造成致命的造行时错误。 练习中要用无类型参数生成开发的 Array 类的模板。这个模板可以用编译时指定类型 的指定元素个数实例化 Array 对象,而不必在运行时动态生成 Array 对象的空间。 不符合常用类模板的特定类型的类可以重定义该类型的类模板。例如,可以用一个 Array 类模板实例化任何类型的数组。程序员可以控制某个类型 Array 类的实例化,如 Martian,只要建立类名为 Array的新类即可。 5.6 模板与继承 模板与继承关系如下所示: ●类模板可以从模板类派生。 ●类模板可以从非模板类派生。 ●模板类可以从类模板派生。 ●非模板类可以从类模板中派生。 5.7 模板与友元 函数和整个类都可以声明为非模板类友元。使用类模板,可以声明各种各样的友元关系。 友元可以在类模板与全局函数间、另一个类(可能是模板类)的成员函数间或整个类中(可能 是模板类)建立。建立这种友元关系的符号可能很繁琐。 在下列 X 类的类模板中声明为: templateclass X 下列友元声明: friend void f1(); 使函数 f1 成为从上述类模板实例化的每个模板类的友元。 在下列 X 类的类模板中声明为: templateclass X 下列友元声明: friend void f2(X< T > 6); 对特定类型 T(如 float)使函数 f2(X&)成为 X的友元。 在类模板中,可以声明另一个类的成员函数是类模板产生的任何模板类的友元。只要 用类名和二元作用域运算符指定其它类的成员函数名。例如,在下列 X 类的类模板中声明 为: templateclass X 下列友元声明: friend void A::f4(); 使 A 类的成员函数 f4 成为上述类模板实例化的任何模板类的友元。 在下列 X 类的类模板中声明为: 150 templateclass X 下列友元声明: friend void C< T >::f5( X< T > & ); 对特定类型 T(如 float)使成员函数: C::f5( X< float> & ); 成为 X模板类的友元函数。 在下列 X 类的类模板中声明为: templateClass X 可以声明第二个类 Y,如下所示: friend class Y; 使 Y 类的每个成员函数成为 X 的类模板产生的每个模板类的友元。 在下列 X 类的类模板中声明为: templateclass X 可以声明第二个类 Z,如下所示: friend class Z< T >; 使模板类用特定类型 T(如 float)实例化时,class Z的所有成员成为模板类 X的友元。 5.8 模板与 static 成员 在非模板类中,类的所有对象共享一个 static 数据成员,Static 数据成员应在文件 范围内初始化。 从类模板实例化的每个模板类有自己的类模板 static 数据成员,该模板类的所有对 象共享一个 Static 数据成员。和非模板类的 static 数据成员一样,模板类的 static 数据 成员也应在文件范围内初始化。每个模板类有自己的类摸板的 static 数据成员副本。 小 结 ●模板使我们可以用一个代码段指定一组相关函数(称为模板函数)或一组相关类(称 为模板类)。 ●程序员对函数模板的定义只编写一次。基于调用函数时提供的参数类型,C++自动 产生单独的函数来正确地处理每种类型的调用。这些都是利用程序源代码的剩余空间进行 编译。 ●所有函数模板定义都足用关键字 template 开始的,该关键字之后是用尖括号<>括 起来的形式参数表。函数模板的每个形式类型参数之前应有关键字 class(或新的关键字 typename)。关键字 class 指定函数模板的类型参数,实际上表示“任何内部类型或用户自 定义类型”。 ●模板定义的形式参数可用来指定传递给函数的参数类型、函数返回类型和声明函数 中变量。 ●形式参数的名字可以只在模板的形式参数表中出现一次。同一个形式参数名可用于 多个模板函数。 ●函数模板本身可以用多种方式重载。我们可以提供其他函数模板,指定不同参数的 151 相同函数名。函数模板也可以用其他非模板函数(同名而不同参数)重载。 ●类模板提供了描述一个类和实例化类(即该通用类指定类型的版本)的方法。 ●为了说明如何定制通用类模板以形成指定的模板类,类模板需要类型参数,所以 类模板也常常称为参数化类型。 ●要使用模板类的程序员只需简单地编写一个类模板。在需要用模板建立一个新的指 定类型的类时,程序员只需要用一种简洁的表示方法,编译器就会写出该模板类的源代 码。 ●类模板的定义似乎与普通的类定义没什么不同,除了使用 template指明 这是一个带类型参数 T(指明创建的类的类型)的类模板定义。在类首部和成员函数的定义 中,类型 ●作为一个通用的类型名。 ●在类模板首部以外的成员函数定义都要以 template开头。接着,成员函 数的定义与普通成员函数的定义相似,只是类中的数据通常用类型参数 T 表示。二元作用 域运算符总是把成员函数的定义与正确的类范围联系起来。 ●类模板首部也可以使用无类型参数。 ●特定类型的类可以重定义该类型的类模板。 ●类模板可以从模板类派生。类模板可以从非模板类派生。模板类可以从类模板派生。 非模板类可以从类模板中派生。 ●函数和整个类都可以声明为非模板类的友元。使用类模板,可以声明各种各样的友 元关系。 友元可以在类模板与全局函数间、另一个类(可能是模板类)的成员函数间或整个类中 (可能是模板类)建立。 ●从类模板实例化的每个模板类有自己的类模板的 Static 数据成员.该模板类的所 有对象共享一个 static 数据成员。和非模板类的 static 数据成员一样,模板类的 Static 数据成员也应在文件范围内初始化。 ●每个模板类有该类模板的 static 数据成员副本。 152 153
还剩152页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

zeorro

贡献于2012-08-25

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf