读书笔记之:C++程序设计——清华大学

第2章 数据类型与表达式

1. C++中的数据类型如下:

2. C++中常量

变量的值应该是可以变化的,怎么值是固定的量也称变量呢?其实,从计算机实现的角度看,变量的特征是存在一个以变量名命名的存储单元,在一般情况下,存储单元中的内容是可以变化的。对常变量来说,无非在此变量的基础上加上一个限定: 存储单元中的值不允许变化。因此常变量又称为只读变量(read-only-variable)。

请区别用#define命令定义的符号常量和用const定义的常变量。符号常量只是用一个符号代替一个字符串,在预编译时把所有符号常量替换为所指定的字符串,它没有类型,在内存中并不存在以符号常量命名的存储单元。而常变量具有变量的特征,它具有类型,在内存中存在着以它命名的存储单元,可以用sizeof运算符测出其长度。与一般变量惟一的不同是指定变量的值不能改变。用#define命令定义符号常量是C语言所采用的方法,C++把它保留下来是为了和C兼容。C++的程序员一般喜欢用const定义常变量。虽然二者实现的方法不同,但从使用的角度看,都可以认为用了一个标识符代表了一个常量。有些书上把用const定义的常变量也称为定义常量,但读者应该了解它和符号常量的区别。

第3章 程序设计初步

1. 在输入流与输出流中使用控制符

需要注意的是: 如果使用了控制符,在程序单位的开头除了要加iostream头文件外,还要加iomanip头文件。

举例: 输出双精度数。

double a=123.456789012345;对a赋初值

(1) cout<<a;输出: 123.456

(2) cout<<setprecision(9)<<a;输出: 123.456789

(3) cout<<setprecision(6);恢复默认格式(精度为6)

(4) cout<< setiosflags(iosfixed);输出: 123.456789

(5) cout<<setiosflags(iosfixed)<<setprecision(8)<<a;输出: 123.45678901

(6) cout<<setiosflags(iosscientific)<<a;输出: 1.234568e+02

(7) cout<<setiosflags(iosscientific)<<setprecision(4)<<a; 输出: 1.2346e02

下面是整数输出的例子:

int b=123456;对b赋初值

(1) cout<<b;输出: 123456

(2) cout<<hex<<b; 输出: 1e240

(3) cout<<setiosflags(iosuppercase)<<b;输出: 1E240

(4) cout<<setw(10)<<b<<′,′<<b; 输出: 123456,123456

(5) cout<<setfill(′*′)<<setw(10)<<b;输出: **** 123456

(6) cout<<setiosflags(iosshowpos)<<b;输出: +123456

如果在多个cout语句中使用相同的setw(n),并使用setiosflags(iosright),可以实现各行数据右对齐,如果指定相同的精度,可以实现上下小数点对齐。

例3.1 各行小数点对齐。

#include <iostream>

#include <iomanip>

using namespace std;

int main( )

{

double a=123.456,b=3.14159,c=-3214.67;

cout<<setiosflags(iosfixed)<<setiosflags(iosright)<<setprecision(2);

cout<<setw(10)<<a<<endl;

cout<<setw(10)<<b<<endl;

cout<<setw(10)<<c<<endl;

return 0;

}

第4章 函数与预处理

1.变量属性小结

一个变量除了数据类型以外,还有3种属性:

(1) 存储类别 C++允许使用auto,static,register和extern 4种存储类别。

(2) 作用域 指程序中可以引用该变量的区域。

(3) 存储期 指变量在内存的存储期限。

以上3种属性是有联系的,程序设计者只能声明变量的存储类别,通过存储类别可以确定变量的作用域和存储期。

要注意存储类别的用法。auto, static和register 3种存储类别只能用于变量的定义语句中,如

auto char c; //字符型自动变量,在函数内定义

static int a; //静态局部整型变量或静态外部整型变量

register int d; //整型寄存器变量,在函数内定义

extern int b; //声明一个已定义的外部整型变量

说明: extern只能用来声明已定义的外部变量,而不能用于变量的定义。只要看到extern,就可以判定这是变量声明,而不是定义变量的语句。

下面从不同角度分析它们之间的联系。

(1) 从作用域角度分,有局部变量和全局变量。它们采用的存储类别如下:

● 局部变量

自动变量,即动态局部变量(离开函数,值就消失)

静态局部变量(离开函数,值仍保留)

寄存器变量(离开函数,值就消失)

形式参数(可以定义为自动变量或寄存器变量)

● 全局变量

静态外部变量(只限本文件引用)

外部变量(即非静态的外部变量,允许其他文件引用)

(2) 从变量存储期(存在的时间)来区分,有动态存储和静态存储两种类型。静态存储是程序整个运行时间都存在,而动态存储则是在调用函数时临时分配单元。

●动态存储

自动变量(本函数内有效)

寄存器变量(本函数内有效)

形式参数

● 静态存储

静态局部变量(函数内有效)

静态外部变量(本文件内有效)

外部变量(其他文件可引用)

(3) 从变量值存放的位置来区分,可分为

● 内存中静态存储区

静态局部变量

静态外部变量(函数外部静态变量)

外部变量(可为其他文件引用)

● 内存中动态存储区: 自动变量和形式参数

● CPU 中的寄存器: 寄存器变量

(4) 关于作用域和存储期的概念。从前面叙述可以知道,对一个变量的性质可以从两个方面分析,一是从变量的作用域,一是从变量值存在时间的长短,即存储期。前者是从空间的角度,后者是从时间的角度。二者有联系但不是同一回事。图4.16是作用域的示意图,图4.17是存储期的示意图。

第7章 结构体与共用体

1. 用typedef声明类型

除了用以上方法声明结构体、共用体、枚举等类型外,还可以用typedef声明一个新的类型名来代替已有的类型名。如

typedef int INTEGER; //指定用标识符INTEGER代表int类型

typedef float REAL; //指定用REAL代表float类型

这样,以下两行等价:

① int i,j; float a,b;

② INTEGER i,j; REAL a,b;

这样可以使熟悉FORTRAN的人能用INTEGER和REAL定义变量,以适应他们的习惯。

如果在一个程序中,整型变量是专门用来计数的,可以用COUNT来作为整型类型名:

typedef int COUNT; //指定用COUNT代表int型

COUNT i,j; //将变量i,j定义为COUNT类型,即int类型

在程序中将变量i,j定义为COUNT类型,可以使人更一目了然地知道它们是用于计数的。

也可以声明结构体类型:

typedef struct //注意在struct之前用了关键字typedef,表示是声明新名

{ int month;

int day;

int year;

}DATE; //注意DATE是新类型名,而不是结构体变量名

所声明的新类型名DATE代表上面指定的一个结构体类型。这样就可以用DATE定义变量:

DATE birthday;

DATE *p; //p为指向此结构体类型数据的指针

还可以进一步:

① typedef int NUM[100]; //声明NUM为整型数组类型,包含100个元素

NUM n; //定义n为包含100个整型元素的数组

② typedef char *STRING; //声明STRING为字符指针类型

STRING p,s[10]; //p为字符指针变量,s为指针数组(有10个元素)

③ typedef int (*POINTER)( ) //声明POINTER为指向函数的指针类型,函数返回整型值

POINTER p1,p2; // p1,p2为POINTER类型的指针变量

归纳起来,声明一个新的类型名的方法是:

① 先按定义变量的方法写出定义语句(如int i;)。

② 将变量名换成新类型名(如将i换成COUNT)。

③ 在最前面加typedef(如typedef int COUNT)。

④ 然后可以用新类型名去定义变量。

再以声明上述的数组类型为例来说明:

① 先按定义数组形式书写: int n[100];

② 将变量名n换成自己指定的类型名: int NUM[100];

③ 在前面加上typedef,得到typedef int NUM[100];

④ 用来定义变量: NUM n;(n是包含100个整型元素的数组)。

习惯上常把用typedef声明的类型名用大写字母表示,以便与系统提供的标准类型标识符相区别。

说明:

(1) typedef可以声明各种类型名,但不能用来定义变量。用typedef可以声明数组类型、字符串类型,使用比较方便。

(2) 用typedef只是对已经存在的类型增加一个类型名,而没有创造新的类型。

(3) 当在不同源文件中用到同一类型数据(尤其是像数组、指针、结构体、共用体等类型数据)时,常用typedef声明一些数据类型,把它们单独放在一个头文件中,然后在需要用到它们的文件中用#include命令把它们包含进来,以提高编程效率。

(4) 使用typedef有利于程序的通用与移植。有时程序会依赖于硬件特性,用typedef便于移植。

第8章 类与对象

1. 面向对象的软件开发

随着软件规模的迅速增大,软件人员面临的问题十分复杂。需要规范整个软件开发过程,明确软件开发过程中每个阶段的任务,在保证前一个阶段工作的正确性的情况下,再进行下一阶段的工作。这就是软件工程学需要研究和解决的问题。

面向对象的软件工程包括以下几个部分:

(1). 面向对象分析(object oriented analysis,OOA)

软件工程中的系统分析阶段,系统分析员要和用户结合在一起,对用户的需求作出精确的分析和明确的描述,从宏观的角度概括出系统应该做什么(而不是怎么做)。面向对象的分析,要按照面向对象的概念和方法,在对任务的分析中,从客观存在的事物和事物之间的关系,归纳出有关的对象(包括对象的属性和行为)以及对象之间的联系,并将具有相同属性和行为的对象用一个类(class)来表示。建立一个能反映真实工作情况的需求模型。

(2). 面向对象设计(object oriented design,OOD)

根据面向对象分析阶段形成的需求模型,对每一部分分别进行具体的设计,首先是进行类的设计,类的设计可能包含多个层次(利用继承与派生)。然后以这些类为基础提出程序设计的思路和方法,包括对算法的设计。在设计阶段,并不牵涉某一种具体的计算机语言,而是用一种更通用的描述工具(如伪代码或流程图)来描述。

(3). 面向对象编程(object oriented programming,OOP)

根据面向对象设计的结果,用一种计算机语言把它写成程序,显然应当选用面向对象的计算机语言(例如C++),否则无法实现面向对象设计的要求。

(4). 面向对象测试(object oriented test,OOT)

在写好程序后交给用户使用前,必须对程序进行严格的测试。测试的目的是发现程序中的错误并改正它。面向对象测试是用面向对象的方法进行测试,以类作为测试的基本单元。

(5). 面向对象维护(object oriented soft maintenance,OOSM)

因为对象的封装性,修改一个对象对其他对象影响很小。利用面向对象的方法维护程序,大大提高了软件维护的效率。

2. 成员函数的存储方式

用类去定义对象时,系统会为每一个对象分配存储空间。如果一个类包括了数据和函数,要分别为数据和函数的代码分配存储空间。按理说,如果用同一个类定义了10个对象,那么就需要分别为10个对象的数据和函数代码分配存储单元,如图8.4所示

能否只用一段空间来存放这个共同的函数代码段,在调用各对象的函数时,都去调用这个公用的函数代码。如图8.5所示

显然,这样做会大大节约存储空间。C++编译系统正是这样做的,因此每个对象所占用的存储空间只是该对象的数据部分所占用的存储空间,而不包括函数代码所占用的存储空间。如果声明了一个类:

class Time

{public:

int hour;

int minute;

int sec;

void set( )

{cin>>a>>b>>c;}

};

可以用下面的语句来输出该类对象所占用的字节数:

cout<<sizeof(Time)<<endl;

输出的值是12。这就证明了一个对象所占的空间大小只取决于该对象中数据成员所占的空间,而与成员函数无关。函数代码是存储在对象空间之外的。如果对同一个类定义了10个对象,这些对象的成员函数对应的是同一个函数代码段,而不是10个不同的函数代码段。

需要注意的是: 虽然调用不同对象的成员函数时都是执行同一段函数代码,但是执行结果一般是不相同的。不同的对象使用的是同一个函数代码段,它怎么能够分别对不同对象中的数据进行操作呢?原来C++为此专门设立了一个名为this的指针,用来指向不同的对象。

需要说明:

(1) 不论成员函数在类内定义还是在类外定义,成员函数的代码段都用同一种方式存储。

(2) 不要将成员函数的这种存储方式和inline(内置)函数的概念混淆。

(3) 应当说明: 常说的"某某对象的成员函数",是从逻辑的角度而言的,而成员函数的存储方式,是从物理的角度而言的,二者是不矛盾的。

3. 构造函数的使用

有关构造函数的使用,有以下说明:

(1) 在类对象进入其作用域时调用构造函数。

(2) 构造函数没有返回值,因此也不需要在定义构造函数时声明类型,这是它和一般函数的一个重要的不同之点。

(3) 构造函数不需用户调用,也不能被用户调用。

(4) 在构造函数的函数体中不仅可以对数据成员赋初值,而且可以包含其他语句。但是一般不提倡在构造函数中加入与初始化无关的内容,以保持程序的清晰。

(5) 如果用户自己没有定义构造函数,则C++系统会自动生成一个构造函数,只是这个构造函数的函数体是空的,也没有参数,不执行初始化操作。

4. 析构函数

析构函数(destructor)也是一个特殊的成员函数,它的作用与构造函数相反,它的名字是类名的前面加一个"~"符号。在C++中"~"是位取反运算符,从这点也可以想到: 析构函数是与构造函数作用相反的函数。

当对象的生命期结束时,会自动执行析构函数。具体地说如果出现以下几种情况,程序就会执行析构函数: ①如果在一个函数中定义了一个对象(它是自动局部对象),当这个函数被调用结束时,对象应该释放,在对象释放前自动执行析构函数。static局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数。如果定义了一个全局对象,则在程序的流程离开其作用域时(如main函数结束或调用exit函数) 时,调用该全局对象的析构函数。如果用new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数。

析构函数的作用并不是删除对象,而是在撤销对象占用的内存之前完成一些清理工作,使这部分内存可以被程序分配给新对象使用。程序设计者事先设计好析构函数,以完成所需的功能,只要对象的生命期结束,程序就自动执行析构函数来完成这些工作。

析构函数不返回任何值,没有函数类型,也没有函数参数。因此它不能被重载。一个类可以有多个构造函数,但只能有一个析构函数。

实际上,析构函数的作用并不仅限于释放资源方面,它还可以被用来执行"用户希望在最后一次使用对象之后所执行的任何操作",例如输出有关的信息。这里说的用户是指类的设计者,因为,析构函数是在声明类的时候定义的。也就是说,析构函数可以完成类的设计者所指定的任何操作。

一般情况下,类的设计者应当在声明类的同时定义析构函数,以指定如何完成"清理"的工作。如果用户没有定义析构函数,C++编译系统会自动生成一个析构函数,但它只是徒有析构函数的名称和形式,实际上什么操作都不进行。想让析构函数完成任何工作,都必须在定义的析构函数中指定。

5. 调用构造函数和析构函数的顺序

在使用构造函数和析构函数时,需要特别注意对它们的调用时间和调用顺序。

在一般情况下,调用析构函数的次序正好与调用构造函数的次序相反: 最先被调用的构造函数,其对应的(同一对象中的)析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。如图9.1示意。

但是,并不是在任何情况下都是按这一原则处理的。在第4章第4.11和4.12节中曾介绍过作用域和存储类别的概念,这些概念对于对象也是适用的。对象可以在不同的作用域中定义,可以有不同的存储类别。这些会影响调用构造函数和析构函数的时机。

下面归纳一下什么时候调用构造函数和析构函数:

(1) 在全局范围中定义的对象(即在所有函数之外定义的对象),它的构造函数在文件中的所有函数(包括main函数)执行之前调用。但如果一个程序中有多个文件,而不同的文件中都定义了全局对象,则这些对象的构造函数的执行顺序是不确定的。当main函数执行完毕或调用exit函数时(此时程序终止),调用析构函数。

(2) 如果定义的是局部自动对象(例如在函数中定义对象),则在建立对象时调用其构造函数。如果函数被多次调用,则在每次建立对象时都要调用构造函数。在函数调用结束、对象释放时先调用析构函数。

(3) 如果在函数中定义静态(static)局部对象,则只在程序第一次调用此函数建立对象时调用构造函数一次,在调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用析构函数。

第9章

1. 共用数据的保护

C++虽然采取了不少有效的措施(如设private保护)以增加数据的安全性,但是有些数据却往往是共享的,人们可以在不同的场合通过不同的途径访问同一个数据对象。有时在无意之中的误操作会改变有关数据的状况,而这是人们所不希望出现的。

既要使数据能在一定范围内共享,又要保证它不被任意修改,这时可以使用const,即把有关的数据定义为常量。

(1). const数据成员

其作用和用法与一般常变量相似,用关键字const来声明常数据成员。常数据成员的值是不能改变的。有一点要注意: 只能通过构造函数的参数初始化表对常数据成员进行初始化。如在类体中定义了常数据成员hour:

const int hour; //声明hour为常数据成员

不能采用在构造函数中对常数据成员赋初值的方法。

在类外定义构造函数,应写成以下形式:

Time∷Time(int h):hour(h){} //通过参数初始化表对常数据成员hour初始化

常对象的数据成员都是常数据成员,因此常对象的构造函数只能用参数初始化表对常数据成员进行初始化。

(2). 常成员函数

前面已提到: 一般的成员函数可以引用本类中的非const数据成员,也可以修改它们。如果将成员函数声明为常成员函数,则只能引用本类中的数据成员,而不能修改它们,例如只用于输出数据等。如

void get_time( ) const; //注意const的位置在函数名和括号之后

const是函数类型的一部分,在声明函数和定义函数时都要有const关键字,在调用时不必加const。常成员函数可以引用const数据成员,也可以引用非const的数据成员。const数据成员可以被const成员函数引用,也可以被非const的成员函数引用。具体情况可以用书中表9.1表示。

怎样利用常成员函数呢?

(1) 如果在一个类中,有些数据成员的值允许改变,另一些数据成员的值不允许改变,则可以将一部分数据成员声明为const,以保证其值不被改变,可以用非const的成员函数引用这些数据成员的值,并修改非const数据成员的值。

(2) 如果要求所有的数据成员的值都不允许改变,则可以将所有的数据成员声明为const,或将对象声明为const(常对象),然后用const成员函数引用数据成员,这样起到"双保险"的作用,切实保证了数据成员不被修改。

(3) 如果已定义了一个常对象,只能调用其中的const成员函数,而不能调用非const成员函数(不论这些函数是否会修改对象中的数据)。这是为了保证数据的安全。如果需要访问对象中的数据成员,可将常对象中所有成员函数都声明为const成员函数,但应确保在函数中不修改对象中的数据成员。不要误认为常对象中的成员函数都是常成员函数。常对象只保证其数据成员是常数据成员,其值不被修改。如果在常对象中的成员函数未加const声明,编译系统把它作为非const成员函数处理。

还有一点要指出: 常成员函数不能调用另一个非const成员函数。

2. 对象的复制

有时需要用到多个完全相同的对象。此外,有时需要将对象在某一瞬时的状态保留下来。这就是对象的复制机制。用一个已有的对象快速地复制出多个完全相同的对象。如

Box box2(box1);

其作用是用已有的对象box1去克隆出一个新对象box2。

其一般形式为

类名 对象2(对象1);

用对象1复制出对象2。

可以看到: 它与前面介绍过的定义对象方式类似,但是括号中给出的参数不是一般的变量,而是对象。在建立对象时调用一个特殊的构造函数——复制构造函数(copy constructor)。这个函数的形式是这样的:

//The copy constructor definition.

BoxBox(const Box& b)

{height=b.height;

width=b.width;

length=b.length;}

复制构造函数也是构造函数,但它只有一个参数,这个参数是本类的对象(不能是其他类的对象),而且采用对象的引用的形式(一般约定加const声明,使参数值不能改变,以免在调用此函数时因不慎而使对象值被修改)。

此复制构造函数的作用就是将实参对象的各成员值一一赋给新的对象中对应的成员。

回顾复制对象的语句

Box box2(box1);

这实际上也是建立对象的语句,建立一个新对象box2。由于在括号内给定的实参是对象,因此编译系统就调用复制构造函数(它的形参也是对象),而不会去调用其他构造函数。实参box1的地址传递给形参b(b是box1的引用),因此执行复制构造函数的函数体时,将box1对象中各数据成员的值赋给box2中各数据成员。

如果用户自己未定义复制构造函数,则编译系统会自动提供一个默认的复制构造函数,其作用只是简单地复制类中每个数据成员。

C++还提供另一种方便用户的复制形式,用赋值号代替括号,如

Box box2=box1; //用box1初始化box2

其一般形式为

类名 对象名1 = 对象名2;

可以在一个语句中进行多个对象的复制。如

Box box2=box1,box3=box2;

按box1来复制box2和box3。可以看出: 这种形式与变量初始化语句类似,请与下面定义变量的语句作比较:

int a=4,b=a;

这种形式看起来很直观,用起来很方便。但是其作用都是调用复制构造函数。

3. 复制构造函数

请注意对象的复制和9.8.1节介绍的对象的赋值在概念上和语法上的不同。对象的赋值是对一个已经存在的对象赋值,因此必须先定义被赋值的对象,才能进行赋值。而对象的复制则是从无到有地建立一个新对象,并使它与一个已有的对象完全相同(包括对象的结构和成员的值)。

可以对例9.7程序中的主函数作一些修改:

int main( )

{Box box1(15,30,25); //定义box1

cout<<″The volume of box1 is ″<<box1.volume( )<<endl;

Box box2=box1,box3=box2; //按box1来复制box2,box3

cout<<″The volume of box2 is ″<<box2.volume( )<<endl;

cout<<″The volume of box3 is ″<<box3.volume( )<<endl;

}

执行完第3行后,3个对象的状态完全相同。

请注意普通构造函数和复制构造函数的区别。

(1) 在形式上

类名(形参表列); //普通构造函数的声明,如Box(int h,int w,int len);

类名(类名& 对象名); //复制构造函数的声明,如Box(Box &b);

(2) 在建立对象时,实参类型不同。系统会根据实参的类型决定调用普通构造函数或复制构造函数。如

Box box1(12,15,16); //实参为整数,调用普通构造函数

Box box2(box1); //实参是对象名,调用复制构造函数

(3) 在什么情况下被调用

普通构造函数在程序中建立对象时被调用。

复制构造函数在用已有对象复制一个新对象时被调用,在以下3种情况下需要克隆对象:

① 程序中需要新建立一个对象,并用另一个同类的对象对它初始化,如前面介绍的那样。

当函数的参数为类的对象时。在调用函数时需要将实参对象完整地传递给形参,也就是需要建立一个实参的拷贝,这就是按实参复制一个形参,系统是通过调用复制构造函数来实现的,这样能保证形参具有和实参完全相同的值。

void fun(Box b) //形参是类的对象

{ }

int main( )

{Box box1(12,15,18);

fun(box1); //实参是类的对象,调用函数时将复制一个新对象b

return 0;

}

③ 函数的返回值是类的对象。在函数调用完毕将返回值带回函数调用处时。此时需要将函数中的对象复制一个临时对象并传给该函数的调用处。如

Box f( ) //函数f的类型为Box类类型

{Box box1(12,15,18);

return box1; //返回值是Box类的对象

}

int main( )

{Box box2; //定义Box类的对象box2

box2=f( ); //调用f函数,返回Box类的临时对象,并将它赋值给box2

}

以上几种调用复制构造函数都是由编译系统自动实现的,不必由用户自己去调用,读者只要知道在这些情况下需要调用复制构造函数就可以了。

第10章 运算符重载

1.成员函数或友元函数

C++规定,有的运算符(如赋值运算符、下标运算符、函数调用运算符)必须定义为类的成员函数,有的运算符则不能定义为类的成员函数(如流插入"<<"和流提取运算符">>"、类型转换运算符)。

2. 重载流插入运算符和流提取运算符

C++的流插入运算符"<<"和流提取运算符">>"是C++在类库中提供的,所有C++编译系统都在类库中提供输入流类istream和输出流类ostream。cin和cout分别是istream类和ostream类的对象。在类库提供的头文件中已经对"<<"和">>"进行了重载,使之作为流插入运算符和流提取运算符,能用来输出和输入C++标准类型的数据。因此,在本书前面几章中,凡是用"cout<<"和"cin>>"对标准类型数据进行输入输出的,都要用#include <iostream>把头文件包含到本程序文件中。

用户自己定义的类型的数据,是不能直接用"<<"和">>"来输出和输入的。如果想用它们输出和输入自己声明的类型的数据,必须对它们重载。

对"<<"和">>"重载的函数形式如下:

istream & operator >> (istream &,自定义类 &);

ostream & operator << (ostream &,自定义类 &);

即重载运算符">>"的函数的第一个参数和函数的类型都必须是istream&类型,第二个参数是要进行输入操作的类。重载"<<"的函数的第一个参数和函数的类型都必须是ostream&类型,第二个参数是要进行输出操作的类。因此,只能将重载">>"和"<<"的函数作为友元函数或普通的函数,而不能将它们定义为成员函数。

3. 转换构造函数

转换构造函数(conversion constructor function) 的作用是将一个其他类型的数据转换成一个类的对象。

先回顾一下以前学习过的几种构造函数:

  • 默认构造函数。以Complex类为例,函数原型的形式为

Complex( ); //没有参数

  • 用于初始化的构造函数。函数原型的形式为

Complex(double r,double i); //形参表列中一般有两个以上参数

  • 用于复制对象的复制构造函数。函数原型的形式为

Complex (Complex &c); //形参是本类对象的引用

  • 现在又要介绍一种新的构造函数——转换构造函数。

转换构造函数只有一个形参,如

Complex(double r) {real=r;imag=0;}

其作用是将double型的参数r转换成Complex类的对象,将r作为复数的实部,虚部为0。用户可以根据需要定义转换构造函数,在函数体中告诉编译系统怎样去进行转换。

在类体中,可以有转换构造函数,也可以没有转换构造函数,视需要而定。以上几种构造函数可以同时出现在同一个类中,它们是构造函数的重载。编译系统会根据建立对象时给出的实参的个数与类型选择形参与之匹配的构造函数。

4. 类型转换函数

用转换构造函数可以将一个指定类型的数据转换为类的对象。但是不能反过来将一个类的对象转换为一个其他类型的数据(例如将一个Complex类对象转换成double类型数据)。

C++提供类型转换函数(type conversion function)来解决这个问题。类型转换函数的作用是将一个类的对象转换成另一类型的数据。如果已声明了一个Complex类,可以在Complex类中这样定义类型转换函数:

operator double( )

{return real;}

类型转换函数的一般形式为

operator 类型名( )

{实现转换的语句}

在函数名前面不能指定函数类型,函数没有参数。其返回值的类型是由函数名中指定的类型名来确定的。类型转换函数只能作为成员函数,因为转换的主体是本类的对象。不能作为友元函数或普通函数。

从函数形式可以看到,它与运算符重载函数相似,都是用关键字operator开头,只是被重载的是类型名。double类型经过重载后,除了原有的含义外,还获得新的含义(将一个Complex类对象转换为double类型数据,并指定了转换方法)。这样,编译系统不仅能识别原有的double型数据,而且还会把Complex类对象作为double型数据处理。

例10.9 使用类型转换函数的简单例子。

#include <iostream>

using namespace std;

class Complex

{public:

Complex( ){real=0;imag=0;}

Complex(double r,double i){real=r;imag=i;}

operator double( ) {return real;} //类型转换函数

private:

double real;

double imag;

};

int main( )

{Complex c1(3,4),c2(5,-10),c3;

double d;

d=2.5+c1; //要求将一个double数据与Complex类数据相加

cout<<d<<endl;

return 0;

}

第11章 继承与派生

1.菱形继承

在一个类中保留间接共同基类的多份同名成员,这种现象是人们不希望出现的。

C++提供虚基类(virtual base class)的方法,使得在继承间接共同基类时只保留一份成员。

现在,将类A声明为虚基类,方法如下:

class A//声明基类A

{…};

class B :virtual public A //声明类B是类A的公用派生类,A是B的虚基类

{…};

class C :virtual public A //声明类C是类A的公用派生类,A是C的虚基类

{…};

注意: 虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。声明虚基类的一般形式为

class 派生类名: virtual 继承方式 基类名

经过这样的声明后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次。

在派生类B和C中作了上面的虚基类声明后,派生类D中的成员如图11.23所示。

需要注意: 为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承。如果像图11.24所示的那样,在派生类B和C中将类A声明为虚基类,而在派生类D中没有将类A声明为虚基类,则在派生类E中,虽然从类B和C路径派生的部分只保留一份基类成员,但从类D路径派生的部分还保留一份基类成员。

2. 虚基类的初始化

如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类(包括直接派生或间接派生的派生类)中,通过构造函数的初始化表对虚基类进行初始化。例如

class A//定义基类A

{A(int i){ } //基类构造函数,有一个参数

…};

class B :virtual public A //A作为B的虚基类

{B(int n):A(n){ } //B类构造函数,在初始化表中对虚基类初始化

…};

class C :virtual public A //A作为C的虚基类

{C(int n):A(n){ } //C类构造函数,在初始化表中对虚基类初始化

…};

class D :public B,public C //类D的构造函数,在初始化表中对所有基类初始化

{D(int n):A(n),B(n),C(n){ }

…};

注意: 在定义类D的构造函数时,与以往使用的方法有所不同。规定: 在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。

C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类C) 对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。

3. 基类与派生类的转换

只有公用派生类才是基类真正的子类型,它完整地继承了基类的功能。

基类与派生类对象之间有赋值兼容关系,由于派生类中包含从基类继承的成员,因此可以将派生类的值赋给基类对象,在用到基类对象的时候可以用其子类对象代替。具体表现在以下几个方面:

(1) 派生类对象可以向基类对象赋值。

可以用子类(即公用派生类)对象对其基类对象赋值。

A a1; //定义基类A对象a1

B b1; //定义类A的公用派生类B的对象b1

a1=b1; //用派生类B对象b1对基类对象a1赋值

在赋值时舍弃派生类自己的成员。如图11.26示意。实际上,所谓赋值只是对数据成员赋值,对成员函数不存在赋值问题。

请注意: 赋值后不能企图通过对象a1去访问派生类对象b1的成员,因为b1的成员与a1的成员是不同的。假设age是派生类B中增加的公用数据成员,分析下面的用法:

(2) 派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化。

如已定义了基类A对象a1,可以定义a1的引用变量:

A a1; //定义基类A对象a1

B b1; //定义公用派生类B对象b1

A& r=a1; //定义基类A对象的引用变量r,并用a1对其初始化

这时,引用变量r是a1的别名,r和a1共享同一段存储单元。也可以用子类对象初始化引用变量r,将上面最后一行改为

A& r=b1;//定义基类A对象的引用变量r,并用派生类B对象b1对其初始化

或者保留上面第3行"A& r=a1;",而对r重新赋值:

r=b1;//用派生类B对象b1对a1的引用变量r赋值

注意: 此时r并不是b1的别名,也不与b1共享同一段存储单元。它只是b1中基类部分的别名,r与b1中基类部分共享同一段存储单元,r与b1具有相同的起始地址。见图11.27。

(3) 如果函数的参数是基类对象或基类对象的引用,相应的实参可以用子类对象。

如有一函数fun:

void fun(A& r)//形参是类A的对象的引用变量

{cout<<r.num<<endl;} //输出该引用变量的数据成员num

函数的形参是类A的对象的引用变量,本来实参应该为A类的对象。由于子类对象与派生类对象赋值兼容,派生类对象能自动转换类型,在调用fun函数时可以用派生类B的对象b1作实参:

fun(b1);

输出类B的对象b1的基类数据成员num的值。

与前相同,在fun函数中只能输出派生类中基类成员的值。

(4) 派生类对象的地址可以赋给指向基类对象的指针变量,也就是说,指向基类对象的指针变量也可以指向派生类对象。

第12章 多态与虚函数

1.在什么情况下应当声明虚函数

使用虚函数时,有两点要注意:

(1)只能用virtual声明类的成员函数,使它成为虚函数,而不能将类外的普通函数声明为虚函数。因为虚函数的作用是允许在派生类中对基类的虚函数重新定义。显然,它只能用于类的继承层次结构中

(2) 一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名函数。

2.根据什么考虑是否把一个成员函数声明为虚函数呢?主要考虑以下几点:

(1) 首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。

(2) 如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。不要仅仅考虑到要作为基类而把类中的所有成员函数都声明为虚函数。

(3) 应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。

(4) 有时,在定义虚函数时,并不定义其函数体,即函数体是空的。它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。在12.4节中将详细讨论此问题。

需要说明的是: 使用虚函数,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtual function table,简称vtable),它是一个指针数组,存放每个虚函数的入口地址。系统在进行动态关联时的时间开销是很少的,因此,多态性是高效的

3.虚析构函数

如果将基类的析构函数声明为虚函数时,由该基类所派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不相同。

最好把基类的析构函数声明为虚函数。这将使所有派生类的析构函数自动成为虚函数。这样,如果程序中显式地用了delete运算符准备删除一个对象,而delete运算符的操作对象用了指向派生类对象的基类指针,则系统会调用相应类的析构函数。

虚析构函数的概念和用法很简单,但它在面向对象程序设计中却是很重要的技巧。专业人员一般都习惯声明虚析构函数,即使基类并不需要析构函数,也显式地定义一个函数体为空的虚析构函数,以保证在撤销动态分配空间时能得到正确的处理。

构造函数不能声明为虚函数。这是因为在执行构造函数时类对象还未完成建立过程,当然谈不上函数与类对象的绑定。

 

第13章 输入输出流

第14章 C++工具

 

posted @ 2012-07-27 13:49  Mr.Rico  阅读(...)  评论(...编辑  收藏