程序设计

525
程程程程 程程程程 程程程 Email: [email protected] [email protected]

Upload: sylvester-york

Post on 03-Jan-2016

36 views

Category:

Documents


6 download

DESCRIPTION

程序设计. 翁惠玉 Email: [email protected] [email protected]. 教材参考教材. C++ 程序设计思想与方法 人民邮电出版社 翁惠玉 C++ Primer (第 4 版) 人民邮电出版社 C++ 大学教程(第 5 版) 电子工业出版社. 第 10 章 创建功能更强的类型. 从面向过程到面向对象 类的定义 对象的使用 对象的构造与析构 常量对象与 const 成员函数 常量数据成员 静态数据成员与静态成员函数 友元. 从面向过程到面向对象. 抽象的过程 - PowerPoint PPT Presentation

TRANSCRIPT

Page 1: 程序设计

程序设计程序设计

翁惠玉Email: [email protected]

[email protected]

Page 2: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 2

教材参考教材教材参考教材

C++ 程序设计思想与方法

人民邮电出版社 翁惠玉 C++ Primer (第 4 版)

人民邮电出版社 C++ 大学教程(第 5 版) 电子工业出版

Page 3: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 3

第第 1010 章 创建功能更强的类型章 创建功能更强的类型 从面向过程到面向对象 类的定义 对象的使用 对象的构造与析构 常量对象与 const 成员函数 常量数据成员 静态数据成员与静态成员函数 友元

Page 4: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 4

从面向过程到面向对象从面向过程到面向对象

抽象的过程 面向对象的程序设计的特点 库和类

Page 5: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 5

抽象的过程抽象的过程 计算机的工作是建立在抽象的基础上。

机器语言和汇编语言是对机器硬件的抽象 高级语言是对汇编语言和机器语言的抽象

现有抽象的问题: 要求程序员按计算机的结构去思考,而不是按要解决的

问题的结构去思考。 当程序员要解决一个问题时,必须要在机器模型和实际

要解决的问题模型之间建立联系。 而计算机的结构本质上还是为了支持计算,当要解决一

些非计算问题时,这个联系的建立是很困难的

Page 6: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 6

面向对象的程序设计 面向对象的程序设计 为程序员提供了创建工具的功能 解决一个问题时

程序员首先考虑的是需要哪些工具创建这些工具用这些工具解决问题

工具就是所谓的对象 现有的高级语言提供的工具都是数值计

算的工具

Page 7: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 7

过程化过程化 vsvs 面向对象面向对象

过程化的设计方法:从功能和过程着手 输入圆的半径或直径 利用 S=πr2和 C=2πr 计算面积和周长 输出计算结果

面向对象的程序设计方法: 需要什么工具。如果计算机能提供给我们一个称为圆的

工具,它可以以某种方式保存一个圆,告诉我们有关这个圆的一些特性,如它的半径、直径、面积和周长。

定义一个圆类型的变量,以他提供的方式将一个圆保存在该变量中,然后让这个变量告诉我们这个圆的面积和周长是多少

以计算圆的面积和周长的问题为例

Page 8: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 8

从面向过程到面向对象从面向过程到面向对象

抽象的过程 面向对象的程序设计的特点 库和类

Page 9: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 9

面向对象的程序设计的特点 面向对象的程序设计的特点 代码重用:圆类型也可以被那些也需要处

理圆的其他程序员使用 实现隐藏:

类的创建者创造新的工具类的使用者则收集已有的工具快速解决所需解决的问题

这些工具是如何实现的,类的使用者不需要知道

Page 10: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 10

面向对象的程序设计的特点面向对象的程序设计的特点 继承:在已有工具的基础上加以扩展,形成一个功能

更强的工具。如在学校管理系统中,可以形成如下的继承关系

教师 学生 教辅

高级 中级 初级 本科 硕士 博士 实验室 行政

Page 11: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 11

面向对象的程序设计的特点面向对象的程序设计的特点 多态性:

当处理层次结构的类型时,程序员往往想把各个层次的对象都看成是基类成员。

如需要对教师进行考核,不必管他是什么职称,只要向所有教师发一个考核指令。每位教师自会按照自己的类型作出相应的处理。如高级职称的教师会按高级职称的标准进行考核,初级职称的教师会按初级职称的标准进行考核。

好处:程序代码就可以不受新增类型的影响。如增加一个院士的类型,它也是教师类的一个子类,整个程序不用修改,但功能得到了扩展。

Page 12: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 12

从面向过程到面向对象从面向过程到面向对象

抽象的过程 面向对象的程序设计的特点 库和类

Page 13: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 13

库和类 库和类

类是更合理的库 例:设计一个库,提供动态实型数组服务,该数组满足两个要求:可以任意指定下标范围;下标范围可在运行时确定;使用下标变量时会检查下标的越界。

Page 14: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 14

库的设计库的设计 数组的保存

数组需要一块保存数组元素的空间。这块空间需要在执行时动态分配。

数组的下标可以由用户指定范围。因此,对每个数组还需要保存下标的上下界。

保存这个数组的三个部分是一个有机的整体,因此可以用一个结构体把它们封装在一起。

数组操作 给数组分配空间 给数组元素赋值 取某一个数组元素的值 由于这个数组的存储空间是动态分配的,因此,

还必须有一个函数去释放空间

Page 15: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 15

ArrayArray 库的头文件库的头文件#ifndef _array_h

#define _array_h

// 可指定下标范围的数组的存储struct DoubleArray

{ int low;

int high;

double *storage;

};

Page 16: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 16

// 根据 low 和 high 为数组分配空间

bool initialize(DoubleArray &arr, int low, int high);

// 设置数组元素的值

bool insert(const DoubleArray &arr, int index, double value);

// 取数组元素的值

bool fatch(const DoubleArray &arr, int index, double &value);

// 回收数组空间

void cleanup(const DoubleArray &arr);

#endif

Page 17: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 17

ArrayArray 库的实现文件库的实现文件#include "array.h"

#include <iostream>

using namespace std;

bool initialize(DoubleArray &arr, int low, int hig

h)

{ arr.low = low;

arr.high = high;

arr.storage = new double [high - low + 1];

if (arr.storage == NULL) return false;

else return true;

}

Page 18: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 18

bool insert(const DoubleArray &arr, int index, double value){ if (index < arr.low || index > arr.high) return false; arr.storage[index - arr.low] = value; return true;}

bool fatch(const DoubleArray &arr, int index, double &value){ if (index < arr.low || index > arr.high) return false; value = arr.storage[index - arr.low] ; return true;}

void cleanup(const DoubleArray &arr){ delete [] arr.storage; }

Page 19: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 19

ArrayArray 库的应用库的应用#include <iostream>using namespace std;#include "array.h" int main() { DoubleArray array; double value; int low, high, i;

cout << " 请输入数组的下标范围: "; cin >> low >> high;

if (!initialize(array, low, high)) { cout << " 空间分配失败 " ; return 1; }

Page 20: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 20

for (i = low; i <= high; ++i) { cout << " 请输入第 " << i << " 个元素: "; cin >> value; insert(array, i, value); } while (true) { cout << " 请输入要查找的元素序号( 0 表示结束): "; cin >> i; if (i == 0) break; if (fatch(array, i, value)) cout << value << endl; else cout << " 下标越界 \n"; } cleanup(array); return 0;}

Page 21: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 21

ArrayArray 库的问题库的问题 这个数组的使用相当笨拙。每次调用和数组有关的函数

时,都要传递一个结构体给它。 我们可能在一个程序中用到很多库,每个库都可能需要做初始化和清除工作。库的设计者都可能觉得 initialize

和 cleanup 是比较合适的名字,因而都写了这两个函数。 系统内置的数组在数组定义时就指定了元素个数,系统

自动会根据元素个数为数组准备存储空间。而我们这个数组的下标范围要用 initialize 函数来指定,比内置数组多了一个操作。

当数组使用结束后,还需要库的用户显式地归还空间。 对数组元素的操作不能直接用下标变量的形式表示。

Page 22: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 22

ArrayArray 库的改进库的改进 将函数放入结构体 好处:

函数原型中的第一个参数不再需要。编译器自然知道函数体中涉及到的 low, high 和storage 是同一结构体变量中的成员

函数名冲突的问题也得到了解决。现在函数名是从属于某一结构体,从属于不同结构体的同名函数是不会冲突的。

Page 23: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 23

改进后的改进后的 ArrayArray 库的头文件库的头文件#ifndef _array_h

#define _array_h

struct DoubleArray{

int low;

int high;

double *storage;

bool initialize(int lh, int rh);

bool insert(int index, double value);

bool fatch(int index, double &value);

void cleanup();

};

#endif

函数都瘦身了!

Page 24: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 24

改进后的改进后的 ArrayArray 库的实现文件库的实现文件 与原来的实现有一个变化:函数名前要加限定

bool IntArray::initialize(int lh, int rh)

{low = lh;

high = rh;

storage = new double [high - low + 1];

if (storage == NULL) return false; else return true;

}

Page 25: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 25

改进后的改进后的 ArrayArray 库的应用库的应用

函数的调用方法不同。就如引用结构体的成员一样,要用点运算符引用这些函数

Page 26: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 26

#include <iostream>using namespace std;#include "array.h“

int main(){ DoubleArray array; double value; int low, high, i;

cout << " 请输入数组的下标范围: "; cin >> low >> high; if (!array.initialize(low, high)) { cout << " 空间分配失败 " ; return 1;}

Page 27: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 27

for (i = low; i <= high; ++i) { cout << "请输入第 " << i << " 个元素 :"; cin >> value; array.insert(i, value); } while (true) { // 数组元素的查找 cout << "请输入要查找的元素序号 (0表示结束) :"; cin >> i; if (i == 0) break; if (array.fatch(i, value)) cout << value << endl; else cout << " 下标越界 \n"; } array.cleanup(); return 0;}

Page 28: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 28

将函数放入结构体的意义将函数放入结构体的意义 将函数放入结构体是从 C 到 C++ 的根本改变 在 C 中,结构体只是将一组相关的数据捆绑了起来,它除了使程序逻辑更加清晰之外,对解决问题没有任何帮助。

将处理这组数据的函数也加入到结构体中,结构体就有了全新的功能。它既能描述属性,也能描述对属性的操作。事实上,它就成为了和内置类型一样的一种全新的数据类型。

为了表示这是一种全新的概念, C++ 用了一个新的名称 — 类来表示。

Page 29: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 29

第第 1010 章 创建功能更强的类型章 创建功能更强的类型 从面向过程到面向对象 类的定义 对象的使用 对象的构造与析构 常量对象与 const 成员函数 常量数据成员 静态数据成员与静态成员函数 友元

Page 30: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 30

类定义的一般格式类定义的一般格式 class 类名 { [private:]

私有数据成员和成员函数 public:

公有数据成员和成员函数 };

Page 31: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 31

公有和私有成员公有和私有成员

私有成员 (private) :只能由类的成员函数调用

公有成员 (public) :类的用户可以调用的信息,是类对外的接口

私有成员被封装在一个类中,类的用户是看不见的

Page 32: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 32

DoubleArrayDoubleArray 类的定义类的定义class DoubleArray {private: int low; int high; double *storage;public: bool initialize(int lh, int rh); bool insert(int index, double value); bool fatch(int index, double &value); void cleanup();};

Page 33: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 33

类定义说明类定义说明 private 和 public 的出现次序可以是任意

的。也可以反复出现多次。 成员还可以被说明为 protected

数据成员一般说明为 private ,需要被用户调用的函数说明为 public

Page 34: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 34

接口和实现分开接口和实现分开

与库设计一样,类的定义写在接口文件中,成员函数的实现写在实现文件中。

某些简单的成员函数的定义可以直接写在类定义中。

在类定义中定义的成员函数被认为是内联函数。

Page 35: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 35

类定义实例类定义实例

试定义一个有理数类,该类能提供有理数的加和乘运算。要求保存的有理数是最简形式。如 2/6 应记录为 1/3 。

Page 36: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 36

设计考虑设计考虑 保存有理数:保存一个有理数就是保存两个整数。 有理数类的操作:

加函数 乘函数 创建有理数的函数,用以设置有理数的分子和分母 输出有理数函数 化简函数

访问权限设计: 数据成员是私有的 化简函数是内部调用的函数,与用户无关,因此也是私有的 其他函数都是公有的

Page 37: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 37

#ifndef _rational_h#define _rational_h#include <iostream>using namespace std;class Rational {private:

int num;int den;void ReductFraction(); // 将有理数化简成最简形式

public:void create(int n, int d) { num = n; den = d;}

void add(const Rational &r1, const Rational &r2); void multi(const Rational &r1, const Rational &r2);

void display() { cout << num << '/' << den;}};#endif

有理数类的定义

Page 38: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 38

有理数类的实现有理数类的实现#include "Rational.h“

//add 函数将 r1 和 r2 相加,结果存于当前对象void Rational::add(const Rational &r1, const Rational &r2)

{

num = r1.num * r2.den + r2.num * r1.den;

den = r1.den * r2.den;

ReductFraction();

}

Page 39: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 39

void Rational::multi(const Rational &r1, const Rational &r2){ num = r1.num * r2.num; den = r1.den * r2.den; ReductFraction();}// ReductFraction 实现有理数的化简void Rational::ReductFraction(){int tmp = (num > den) ? den : num; for (; tmp > 1; --tmp)

if (num % tmp == 0 && den % tmp ==0) {num /= tmp; den /= tmp; break;}}

Page 40: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 40

第第 1010 章 创建功能更强的类型章 创建功能更强的类型 从面向过程到面向对象 类的定义 对象的使用 对象的构造与析构 常量对象与 const 成员函数 常量数据成员 静态数据成员与静态成员函数 友元

Page 41: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 41

对象的定义对象的定义 类与对象的关系:类型与变量的关系 对象定义方法:

直接在程序中定义某个类的对象存储类别 类名 对象列表;如定义两个 IntArray类的对象 arr1和 arr2,可写成: IntArray arr1, arr2;

用动态内存申请的方法申请一个动态对象。 Rational *rp;

Rp = new Rational;

rp = new Rational[20];

delete Rp ;或  delete [] rp ;

Page 42: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 42

对象的引用对象的引用 对象名 . 数据成员名 或 对象指针 -> 数据成员名 arr1.storage 或 rp->num

对象名 . 成员函数名(实际参数表) 或对象指针 -> 成员函数名(实际参数表)

arr1.insert() 或 rp->add()

外部函数不能引用对象的私有成员

Page 43: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 43

有理数类的使用有理数类的使用

#include <iostream>

using namespace std;

#include "Rational.h" // 使用有理数类int main()

{ int n, d;

Rational r1, r2, r3; // 定义三个有理数类的对象

计算两个有理数的和与积

Page 44: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 44

cout << " 请输入第一个有理数(分子和分母): ";

cin >> n >> d;

r1.create(n,d);

cout << " 请输入第二个有理数(分子和分母): ";

cin >> n >> d;

r2.create(n,d);

r3.add(r1, r2); // 执行 r3=r1+r2

r1.display(); cout << " + "; r2.display();

cout << " = "; r3.display(); cout << endl;

r3.multi(r1, r2); // 执行 r3=r1*r2

r1.display(); cout << " * "; r2.display();

cout << " = "; r3.display(); cout << endl;

return 0;

}

Page 45: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 45

执行实例执行实例

请输入第一个有理数(分子和分母): 1 6

请输入第二个有理数(分子和分母): 1 6

1 / 6 + 1 / 6 = 1 / 3

1 / 6 * 1 / 6 = 1 / 36

Page 46: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 46

对象赋值语句对象赋值语句 同类型的对象之间可以互相赋值,如两

个有理数对象 r1 和 r2 ,可以执行 r1 = r2; 当一个对象赋值给另一个对象时,所有

的数据成员都会逐位拷贝。上述赋值相当于执行

r1.num = r2.num

r1.den = r2.den

Page 47: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 47

thisthis 指针 指针 当定义一个对象时,系统会为对象分配空间,用于

存储对象的数据成员。而类中的成员函数对该类的所有对象只有一份拷贝。那么对于类的成员函数,它如何知道要对哪个对象进行操作呢?

每个成员函数都有一个隐藏的指向本类型的指针形参 this ,它指向当前调用成员函数的对象

如对函数 void create(int n, int d) { num = n; den = d;}

经过编译后,实际函数为 void create(int n, int d)

{ this->num = n; this->den = d;}

Page 48: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 48

thisthis 指针指针 通常,在写成员函数时可以省略 this ,编译

时会自动加上它们。

如果在成员函数中要把对象作为整体来访问

时,必须显式地使用 this 指针。这种情况常

出现在函数中返回一个对调用函数的对象的

引用,

Page 49: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 49

第第 1010 章 创建功能更强的类型章 创建功能更强的类型 从面向过程到面向对象 类的定义 对象的使用 对象的构造与析构 常量对象与 const 成员函数 常量数据成员 静态数据成员与静态成员函数 友元

Page 50: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 50

对象的构造与析构对象的构造与析构 某些类的对象,必须在对它进行了初始化以后才能使用。

对于某些类的对象在消亡前,往往也需要执行一些操作,做一些善后的处理。

初始化和扫尾的工作给类的用户带来了额外的负担,使他们觉得类和内置类型还是不一样。

用户希望使用类的对象就和使用内置类型的变量一样,一旦定义了,就能直接使用。用完了,由系统自动回收。

Page 51: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 51

构造函数与析构函数构造函数与析构函数

构造函数和析构函数是特殊的成员函数

构造函数:对数据成员进行初始化。

析构函数:执行与构造函数相反的操作,通常执行一些清理工作,如释放分配给对象的动态空间等。

Page 52: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 52

构造函数的特点构造函数的特点 定义对象时,系统会自动调用构造函数。 构造函数的名字必须与类名相同 构造函数可以有任意类型的参数,也可以不带参数,但

不能具有返回类型。因此在定义构造函数时,不能说明它的类型,甚至说明为 void 类型也不行。

如果没有给类定义构造函数,编译系统会自动生成一个缺省的构造函数。它只为对象开辟存储空间,空间中的内容为随机数。

构造函数可以重载

Page 53: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 53

构造函数实例构造函数实例 如 DoubleArray 类需要有一个构造函数,该

函数可定义为 DoubleArray(int lh, int rh)

{ low = lh; high = rh;

storage = new double [high – low + 1];}

有了构造函数,就不需要 initialize 函数了。以在定义时有 C++ 自动完成初始化工作。

定义对象时,须指定构造函数的实际参数 DoubleArray array(20, 30);

Page 54: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 54

构造函数实例构造函数实例 Rational 类不一定要有构造函数,但习惯上应改

为每个类定义一个构造函数,以便在需要时对对象进行初始化

Rational 类构造函数可定义为 Rational(int n1, int n2)

{ num = n1; den = n2; ReductFraction();}

定义对象时,须指定构造函数的实际参数。例 Rational r(2, 7);

Page 55: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 55

构造函数的定义和调用构造函数的定义和调用#include <iostream>using namespace std;class complex {private: double real, imag; public: complex(double r, double i) {real=r; imag=i;} double abscomplex() {double t; t=real*real +imag*imag; return(t);} };

Page 56: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 56

构造函数的使用构造函数的使用 有了构造函数后,对象定义的一般形式为: 类名 对象名(实际参数表); 其中,实际参数表必须和该类的某一个构

造函数的形式参数表相对应。 除非这个类有一个构造函数是没有参数的,那

么可以用: 类名 对象名; 不带参数的构造函数称为默认的构造函数。 一般每个类应该有一个缺省的构造函数

Page 57: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 57

int main()

{ complex a(1.2, 2.2);

// 在定义对象时必须给构造函数传递参数 cout << a.abscomplex() << endl;

retuen 0;

}

Page 58: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 58

带缺省参数的构造函数带缺省参数的构造函数#include <iostream>Using namespace std;class complex {private: double real, imag; public: complex(double r = 0.0, double i = 0.0) {real = r; imag = i;} …};int main(){ complex a; //全部用缺省值 complex b(1.1); // 只传递一个参数,第二个用缺省值 complex c(1.1, 2.2); //传递二个参数,不用缺省值 。。。 }

Page 59: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 59

带缺省值的带缺省值的 RationalRational 类的构造函数类的构造函数

Rational(int n1 = 0, int n2 = 1)

(num = n1; den = n2; ReductFraction();}

表示缺省情况下,构造的是一个值为 0

的有理数

Page 60: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 60

动态对象的初始化 动态对象的初始化 动态变量的初始化是在类型后面用一个圆括号指出它的实际参数表

如果要为一个动态的 DoubleArray 数组指定下标范围为 20 到 30 ,可用下列语句:

p = new DoubleArray(20, 30);

括号中的实际参数要和构造函数的形式参数表相对应。

Page 61: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 61

初始化列表方法初始化列表方法 构造函数还有一个与普通函数不同的地方,就是可以包含一个构造函数初始化列表。

构造函数初始化列表位于函数头和函数体之间。它以一个冒号开头,接着是一个以逗号分隔的数据成员构造列表

如 DoubleArray 的构造函数可写为 DoubleArray :: DoubleArray(int lh, int rh)

: low(lh), high(rh)

{ storage = new double [high - low + 1]; }

Page 62: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 62

为什么要使用初始化列表 为什么要使用初始化列表

对象的构造过程: 执行每一个数据成员的构造函数。如果成员没有出现

在初始化列表中,执行默认的构造函数,否则按初始化列表中列出的实际参数执行对应的构造函数

执行类的构造函数

利用初始化列表可以提高构造函数的效率。在初始化数据成员的同时完成了赋初始的工作。

我们完全可以在函数体内对数据成员赋初值 !!!

Page 63: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 63

必须用初始化的情况必须用初始化的情况

数据成员不是普通的内置类型,而是某一个类的对象,可能无法直接用赋值语句在构造函数体中为它赋初值

类包含了一个常量的数据成员,常量只能在定义时对它初始化,而不能对它赋值。因此也必须放在初始化列表中。

Page 64: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 64

重载构造函数重载构造函数 构造函数可以重载,导致对象可以

有多种方式构造 试设计一个时间转换器,用户可输

入秒、分秒或时分秒输出相应的秒数。

Page 65: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 65

时间转换器的实现时间转换器的实现#include <iostream>Using namespace std;class timer{ int second; public: timer(int t) {second=t;} timer(int min, int sec) {second=60*min+sec;} timer(int h, int min, int sec) {second=sec+60*min+3600*h;} int gettime() {return second;}}main(){timer a(20),b(1,20),c(1,1,10); cout<<a.gettime()<<endl; cout<<b.gettime()<<endl; cout<<c.gettime()<<endl;}

Page 66: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 66

析构函数析构函数 析构函数在撤销对象时,完成一些善后工作,由编译系统自动调用

析构函数与构造函数名字相同,但它前面必须加一个波浪号( ~ )

析构函数没有参数,没有返回值,也不能重载。 若定义类时没有定义析构函数,编译系统会自动生成一个缺省的空析构函数

Page 67: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 67

析构函数析构函数

并不是每个类都必须要有析构函数,如 R

ational 类就不需要析构函数。 一般在构造函数中有动态申请内存的,必

须有析构函数。如 DoubleArray 类,必须有析构函数释放 storage 指向的空间。有了析构函数,就不需要 cleanup 函数了

Page 68: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 68

析构函数举例析构函数举例class DoubleArray{

int low;

int high;

double *storage;

public:

DoubleArray(int lh, int rh):low(lh), high(rh)

{ storage = new double [high - low + 1]; }

bool insert(int index, double value);

bool fatch(int index, double &value);

~DoubleArray() {delete [] storage; }

};

Page 69: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 69

DoubleArrayDoubleArray 类的使用类的使用#include <iostream>using namespace std;#include "DoubleArray.h"int main(){ DoubleArray array(20,30); int i; double value; for (i=20; i<=30; ++i) { cout<< " 请输入第 " << i << " 个元素 :"; cin >> value; array.insert(i, value); } while (true) { cout << " 请输入要查找的元素序号( 0 表示结束) :"; cin >> i; if (i == 0) break; if (array.fatch(i, value)) cout << value << endl; else cout << " 下标越界 \n"; } return 0;}

Page 70: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 70

拷贝构造函数拷贝构造函数 在创建一个对象时,可以用一个同类的对象对

其初始化。这是需要调用一个特殊的构造函数,称为拷贝构造函数。

拷贝构造函数以一个同类对象引用作为参数,它的原型为:

类名( const < 类名 > & ); 用户可以根据自己的需要定义拷贝构造函数

Page 71: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 71

缺省的拷贝构造函数缺省的拷贝构造函数

如果用户没有定义拷贝构造函数,系统会定义一个缺省的拷贝构造函数。该函数将已存在的对象原式原样地复制给新成员

Page 72: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 72

自定义拷贝构造函数自定义拷贝构造函数Classname(const classname &ob)

{//…..}

例:Class point{ int x, y;

public: point(int a, int b) { x = a; y = b;}

point(const point &p)

{x = 2 * p.x; y = 2 * p.y;}

}

Page 73: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 73

何时需要自定义拷贝构造函数何时需要自定义拷贝构造函数 一般情况下,默认的拷贝构造函数足以满足要求。 但某些情况下可能需要设计自己的拷贝构造函数。 例如,我们希望对 DoubleArray 类增加一个功能,能够

定义一个和另一个数组完全一样的数组。但默认的拷贝构造函数却不能胜任。如果正在构造的对象为 arr1 ,作为参数的对象是 arr2 ,调用默认的拷贝构造函数相当于执行下列操作:

arr1.low = arr2.low;

arr1.high = arr2.high;

arr1.storage = arr2.storage;

前两个操作没有问题,第三个操作中, storage 是一个指针,第三个操作意味着使 arr1 的 storage 指针和 arr2 的 storage 指针指向同一块空间。

Page 74: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 74

使用同一块空间的问题使用同一块空间的问题

一个对象的修改将会影响另一个对象 如果两个对象的作用域不同,当一个对

象析构时,另一个对象也将丧失它的空间

Page 75: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 75

DoubleArrayDoubleArray 类的拷贝构造函数类的拷贝构造函数

DoubleArray(const DoubleArray &arr)

{ low = arr.low;

high = arr.high;

storage = new double [high – low + 1];

for (int i = 0; i < high –low + 1; ++i)

storage[i] = arr.storage[i];

}

Page 76: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 76

拷贝构造函数的应用场合拷贝构造函数的应用场合

对象定义时

函数调用时,把对象作为参数传给值传递的形式参数

把对象作为返回值时

Page 77: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 77

对象定义时对象定义时

将初始值放在圆括号中,直接调用与实参类型相匹配的构造函数。如

DoubleArray array2(array1);

用“ =”符号

DoubleArray array = array1;

Page 78: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 78

自定义拷贝构造函数举例自定义拷贝构造函数举例class point{ int x, y;

public: point(int a, int b){x=a; y=b;}

point(const point &p) {x=2*p.x; y=2*p.y;}

void print() {cout<<x<<" "<<y<<endl;}

};

void main()

{point p1(10, 20), p2(p1), p3 = p1, p4(1, 2);

p1.print(); p2.print(); p3.print(); p4.print();

p4 = p1; p4.print();

}

10 20

20 40

20 40

1 2

10 20

Page 79: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 79

把对象作为参数传给函数时 把对象作为参数传给函数时

如有函数: void f(DoubleArray array);

函数调用 f(arr);

将创建一个形式参数对象 array ,并调用拷贝构造函数用对象 arr 初始化 array

注意:如果是引用传递就没有这个构造过程了

Page 80: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 80

把对象作为返回值时 把对象作为返回值时 如有函数 DoubleArray f() { DoubleArray a; … return a; } 当执行到 return 语句时,会创建一个 DoubleArra

y 类的临时对象,并调用拷贝构造函数用对象 a 初始化该临时对象,并将此临时对象的值作为返回值。

Page 81: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 81

类变量的生命周期类变量的生命周期 与普通的内置类型的变量完全相同Time gTime;int main()

{

Time lTime1;

static Time sTime;

Time lTime2;

}创建顺序:遇到变量定义时调用构造函数

1 、 gTime 构造函数 2 、 lTime1 构造函数 3 、 sTime 构造函数 4 、 lTime2 构造函数

Page 82: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 82

类变量的生命周期类变量的生命周期Time gTime;int main()

{ Time lTime1;

static Time sTime;

Time lTime2;}消失顺序:1 、局部变量先消失,然后是静态局部变量,

最后是全局变量;2 、后创建的先消失;

1 、 lTime2 析造函数 2 、 lTime1 析造函数3 、 sTime 析造函数 4 、 gTime2 析造函数

Page 83: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 83

变量生命周期的验证变量生命周期的验证 CreateAndCreateAndDestroyDestroy 类定义类定义

class CreateAndDestroy

{

public:

CreateAndDestroy( int, string ); // constructor

~CreateAndDestroy(); // destructor

private:

int objectID; // ID number for object

string message; // message describing object

}; // end class CreateAndDestroy

Page 84: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 84

CreateAndDestroy::CreateAndDestroy( int ID, string messageString ){ objectID = ID; message = messageString;

cout << "Object " << objectID << " constructor runs " << message << endl;}

CreateAndDestroy::~CreateAndDestroy(){ cout << ( objectID == 1 || objectID == 6 ? "\n" : "" );

cout << "Object " << objectID << " destructor runs " << message << endl; }

Page 85: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 85

CreateAndDestroy first( 1, "(global before main)" );

int main()

{ cout << "\nMAIN FUNCTION: EXECUTION BEGINS" << endl;

CreateAndDestroy second( 2, "(local automatic in main)" );

static CreateAndDestroy third( 3, "(local static in main)" );

create();

cout << "\nMAIN FUNCTION: EXECUTION RESUMES" << endl;

CreateAndDestroy fourth( 4, "(local automatic in main)" );

cout << "\nMAIN FUNCTION: EXECUTION ENDS" << endl;

return 0;

}

用户程序用户程序

Page 86: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 86

void create( void ){ cout << "\nCREATE FUNCTION: E

XECUTION BEGINS" << endl; CreateAndDestroy fifth( 5, "(local au

tomatic in create)" ); static CreateAndDestroy sixth( 6, "(l

ocal static in create)" ); CreateAndDestroy seventh( 7, "(local

automatic in create)" ); cout << "\nCREATE FUNCTION: E

XECUTION ENDS" << endl;}

1 con1 conMAIN…MAIN…2 con2 con3 con3 conCREATE…BECREATE…BE5 con5 con6 con6 con7 con7 conCREATE…CREATE…ENDEND7 de7 de5 de5 deMAIN…REMAIN…RE4 con4 conMAIN…ENDMAIN…END4 de4 de2 de2 de6 de6 de3 de3 de1 de1 de

Page 87: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 87

void create( void );

int main()

{ create();

create();

return 0;

}

CREATE…BECREATE…BE5 con5 con6 con6 con7 con7 conCREATE…ENDCREATE…END7 de7 de5 de5 deCREATE…BECREATE…BE5 con5 con7 con7 conCREATE…ENDCREATE…END7 de7 de5 de5 de6 de6 de

测试静态的局部变量

Page 88: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 88

void create( void )

{ cout << "\nCREATE FUNCTION: EXECUTION BEGINS"

<< endl;

CreateAndDestroy fifth( 5, "(local automatic in create)" );

static CreateAndDestroy sixth( 6, "(local static in create)" );

CreateAndDestroy seventh( 7, "(local automatic in create)" );

cout << "\nCREATE FUNCTION: EXECUTION ENDS“

<< endl;

}

Page 89: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 89

第第 1010 章 创建功能更强的类型章 创建功能更强的类型 从面向过程到面向对象 类的定义 对象的使用 对象的构造与析构 常量对象与 const 成员函数 常量数据成员 静态数据成员与静态成员函数 友元

Page 90: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 90

constconst 对象对象

const 对象的定义

const MyClass obj (参数表); const 对象不能被赋值,只能初始化,而且一定要初始化,否则无法设置它的值。

Page 91: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 91

如何保证数据成员不被修改如何保证数据成员不被修改

数据成员一般都由成员函数修改。当定义了一个常量对象后,如何使用这个对象?

C++规定:对 const 对象只能调用 const

成员函数

Page 92: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 92

constconst 成员函数成员函数 任何不修改数据成员的函数都应该声明为 const 类型。如果在编写 const 成员函数时,不慎修改了数据成员,或者调用了其他非const 成员函数,编译器将指出错误,这无疑会提高程序的健壮性。

class A { int x;

public:

A(int i) {x=i;}

int getx() const

{return x;}

};

Page 93: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 93

class A { int x; public: A(int i) {x=i;}

int getx() const; };

int A::getx() const { return x; }

必须加

Page 94: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 94

class A {

int x;

public:

A(int i) {x=i;}

int getx() const

{ x =7; //错误,修改了数据成员 x

return x;}

};

Page 95: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 95

第第 1010 章 创建功能更强的类型章 创建功能更强的类型 从面向过程到面向对象 类的定义 对象的使用 对象的构造与析构 常量对象与 const 成员函数 常量数据成员 静态数据成员与静态成员函数 友元

Page 96: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 96

常量成员常量成员 const 数据成员只在某个对象生存期内是常量,而对于整个

类而言却是可变的。同一类的不同的对象其 const 数据成员的值可以不同。

常量成员的声明 在该成员声明前加上 const 。如 class abc {

const int x;

};

Page 97: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 97

常量数据成员常量数据成员 不能在类声明中初始化 const 数据成员。

class A

{

//错误,企图在类声明中初始化 const 数据成员const int SIZE = 200;

int array[SIZE]; //错误,未知的 SIZE

};

Page 98: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 98

constconst 数据成员的初始化数据成员的初始化 const 数据成员的初始化只能在类构造函数的初始化表中进行,

不能在构造函数中对他赋值。 例:

class A{

A(int size); // 构造函数const int SIZE;

}A::A(int size) : SIZE(size) // 构造函数的初始化表{…}A a(100); // 对象 a 的 SIZE 的值为 100A b(200); // 对象 b 的 SIZE 的值为 200

Page 99: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 99

第第 1010 章 创建功能更强的类型章 创建功能更强的类型 从面向过程到面向对象 类的定义 对象的使用 对象的构造与析构 常量对象与 const 成员函数 常量数据成员 静态数据成员与静态成员函数 友元

Page 100: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 100

静态成员静态成员 假设类 SavingAccount专门用于存放存款帐户,它包括

存户的姓名、地址、存款额、利率等成员变量:class SavingAccount{

char m_name[20]; // 存户姓名 char m_addr[60]; // 存户地址 double m_total; // 存款额

double m_rate; // 利率 …};

这家银行采用浮动利率,每个帐户的利息根据当天的挂牌利率计算。这时 m_rate 就不适合成为每个帐号一个资料,否则每天一开市,光把修改所有帐户的 m_rate 的值,就花掉不少时间。 m_rate 应该独立于各对象之外,成为整个类个对象共享的资料。

Page 101: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 101

静态成员变量的声明静态成员变量的声明 解决方法:在 m_rate前面加上 staticclass SavingAccount{

char m_name[20]; // 存户姓名char m_addr[60]; // 存户地址double m_total; // 存款额

static double m_rate; // 利率…

}

Page 102: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 102

静态成员变量静态成员变量 静态成员变量不属于对象的一部分,而是类

的一部分; 静态成员变量的初始化不能放在类的构造函

数中; 类定义并不分配空间,空间是在定义对象时分配

但静态数据成员属于类,因此定义对象时并不为静态成员分配空间。

Page 103: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 103

静态成员变量的定义静态成员变量的定义 为静态成员分配空间称为静态成员的定义 静态成员的定义一般出现在类的实现文件中。

如在 SavingAccount 类的实现文件中,必须要如下的定义:

double SavingAccount::rate = 0.05;

该定义为 rate分配了空间,并给它赋了一个初值 0.05 。

如果没有这个定义,连接器会报告一个错误。

Page 104: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 104

静态变量的使用静态变量的使用 可以通过作用域操作符从类直接调用。如:

SavingAccount::rate 但从每个对象的角度来看,它似乎又是对

象的一部分,因此又可以从对象引用它。如有个 SavingAccount 类的对象 obj ,则可以用: obj.rate

由于是整个类共享的,因此不管用哪种调用方式,得到的值都是相同的

Page 105: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 105

静态成员函数静态成员函数 成员函数也可以是静态的。静态的成员函数是为

类的全体对象服务,而不是为某个类的特殊对象服务

由于静态成员函数不需要借助任何对象就可以被调用,所以编译器不会为它暗加一个 this 指针。因此,静态成员函数无法处理类中的非静态成员变量。

静态成员函数的声明只需要在类定义中的函数原型前加上保留词 static 。

Page 106: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 106

静态成员函数的用途静态成员函数的用途

定义静态成员函数的主要目的是访问静态的数据成员。

如在 SavingAccount 类中,当利率发生变化时,必须修改这个静态数据成员的值。为此可以设置一个静态的成员函数

static void SetRate(double newRate) {ra

te = newRate;}

Page 107: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 107

静态成员函数使用说明静态成员函数使用说明

静态成员函数可定义为内嵌的,也可在类外定义。在类外定义时,不用 static 。

静态成员函数的访问:可以通过类作用域限定符或通过对象访问

类名 :: 静态成员函数名()

对象名 . 静态成员函数名()

Page 108: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 108

静态成员函数实例静态成员函数实例 11

class goods

{int weight;

static int total_weight;

public: goods(int w);

~goods();

int weight();

static int totalweight();

};

int goods::total_weight = 0;

goods::goods(int w)

{weight=w;

total_weight+=w;}

goods::~goods()

{total_weight-=weight;}

int goods::weight()

{return weight;}

int totalweight()

{return total_weight;}

Page 109: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 109

静态成员函数实例静态成员函数实例 22

在程序执行的某个时刻,有时需要知道某个类已创建的对象个数,现在仍存活的对象个数。

类设计: 数据成员:为了实现这个功能,我们可以在类中定

义两个静态的数据成员: obj_count 和 obj_living 。 成员函数:要实现计数功能,我们可以在创建一个

对象时,对这两个数各加 1 。当撤销一个对象时, o

bj_living减 1 。为了知道某一时刻对象个数的信息,可以定义一个静态的成员函数返回这两个值。

Page 110: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 110

类定义类定义#ifndef _StaticSample_h#define _StaticSample_h#include <iostream>using namespace std;class StaticSample {private:

static int obj_count;static int obj_living;

public:StaticSample() {++obj_count; ++obj_living;}~StaticSample() {--obj_living;}static void display() // 静态成员函数

{cout << "总对象数: " << obj_count << "\t 存活对象数: " << obj_living << endl;}};int StaticSample::obj_count = 0; // 静态数据成员的定义及初始化int StaticSample::obj_living = 0; // 静态数据成员的定义及初始化#endif

Page 111: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 111

StaticSampleStaticSample 的应用的应用#include "StaticSample.h"int main(){StaticSample::display(); //通过类名限定调用成员函数 StaticSample s1, s2; StaticSample::display(); StaticSample *p1 = new StaticSample, *p2 = new StaticSam

ple; s1.display(); //通过对象调用静态成员函数 delete p1; p2->display(); //通过指向对象的指针调用静态成员函数 delete p2; StaticSample::display(); return 0;}

Page 112: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 112

执行结果执行结果

总对象数: 0 存活对象数: 0

总对象数: 2 存活对象数: 2

总对象数: 4 存活对象数: 4

总对象数: 4 存活对象数: 3

总对象数: 4 存活对象数: 2

Page 113: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 113

静态的常量成员静态的常量成员

静态的常量成员:整个类的所有对象的共享常量

静态的常量成员的声明:static const 类型 数据成员名 = 常量表达式;

注意 const 数据成员和 static const 数据成员的区别。

Page 114: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 114

静态常量成员实例静态常量成员实例 例如,需要在某个类中需要用到一个数组,而该数组的大小对所有对象都是相同的,则在类中可指定一个数组规模,并创建一个该规模的数组。如下所示: class sample {

static const int SIZE = 10;

int storage[SIZE];

};

Page 115: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 115

老版本的兼容老版本的兼容 某些旧版本的 C++ 不支持静态常量 解决方法:用不带实例的枚举类型 如:

class sample {

enum {SIZE = 10};

int storage[SIZE];

};

Page 116: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 116

第第 1010 章 创建功能更强的类型章 创建功能更强的类型 从面向过程到面向对象 类的定义 对象的使用 对象的构造与析构 常量对象与 const 成员函数 常量数据成员 静态数据成员与静态成员函数 友元

Page 117: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 117

友元友元

类的私有成员只能通过它的成员函数来访问。

友元函数是一扇通往私有成员的后门。 友元可以是一个一般函数(友元函数),

也可以是另一个类的成员函数(友元成员),还可以是整个类(友元类)。

Page 118: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 118

友元特点友元特点 友元关系是授予的而不是索取的。也就是说,如果

函数 f 要成为类 A 的友元,类 A 必须显式声明函数 f 是他的友元,而不是函数 f 自称是类 A 的友元。

友元关系不是对称关系,如果类 A声明了类 B 是它的友元,并不意味着类 A 也是类 B 的友元。

友元关系不是传递关系。如果类 A 是类 B 的友元,类 B 是类 C 的友元,并不意味着类 A 是类 C 的友元。

Page 119: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 119

友元函数的声明友元函数的声明

用关键词 friend说明友元函数 说明位置:可以定义在类外部,也可定

义在类内部。可声明在 public部分,也可声明在 private部分。

Page 120: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 120

友元函数的声明实例友元函数的声明实例 定义在类内

#include <iostream.h>#include <string.h>class girl { char name[10]; int age; public:girl(char *n, int d) {strcpy(name,n); age=d;} friend void disp(girl &x) // 定义 { cout<<x.name<<“ “ <<x.age<<endl;}

};main() {girl e("abc", 15); disp(e); }

定义在类外

#include <iostream.h>#include <string.h>class girl { char name[10]; int age; public:girl(char *n, int d) {strcpy(name,n);

age=d;} friend void disp(girl &x); }; void disp(girl &x)// 定义 {cout<<x.name<<" “ <<x.age<<endl;} main() {girl e("abc", 15); disp(e); }

一般必须有参数

Page 121: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 121

友元成员友元成员 是其他某个类的成员函数 可以访问 friend声明语句所在类的私有成员

和公有成员的成员函数 类 A 的成员函数作为类 B 的友元函数时,必

须先定义类 A ,再定义类 B 。在定义类 A 和B前必须先声明类 B 。

格式: friend 函数返回类型 类名标识符 ::函数名 (参数列表 );

Page 122: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 122

友元成员实例友元成员实例class boy; //类的声明class girl { char name[10]; int age; public:girl(char *n, int d) {strcpy(name,n); age=d;}

void disp(boy &x); };class boy { char name[10]; int age; public:boy(char *n, int d) {strcpy(name,n);age=d;}

friend void girl::disp(boy &); };void girl::disp(boy &x) {cout<<name<<" "<<age<<endl; cout<<x.name<<" "<<x.age<<endl;}void main() {girl e("abc", 15); boy b("cde",20); e.disp(b); }

abc 15cde 20

Page 123: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 123

友元类友元类 整个类作为另一个类的友元。 当 A 类被说明为类 B 的友元时,类 A

的所有函数都是类 B 的友元函数 声明方法: class Y{….}; class X{ …. friend Y;// 或 friend class Y ……};

Page 124: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 124

友元类实例友元类实例class boy;class girl { char name[10]; int age; public:girl(char *n, int d) {strcpy(name,n); age=d;}

void disp(boy &x); };class boy { char name[10]; int age; public:boy(char *n, int d) {strcpy(name,n);age=d;}

friend class girl; };void girl::disp(boy &x) {cout<<name<<" "<<age<<endl;

cout<<x.name<<" "<<x.age<<endl;} void main() {girl e("abc", 15); boy b("cde",20); e.disp(b); }

abc 15cde 20

Page 125: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 125

友元声明说明友元声明说明

友元关系可以写在类定义中的任何地方 但一个较好的程序设计的习惯是将所有

的友元关系的声明放在最前面的位置,并且不要在他的前面添加任何访问控制说明

Page 126: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 126

class girl { friend void disp(girl &x); private: char name[10]; int age; public: girl(char *n, int d) {strcpy(name,n); age=d;} };

void disp(girl &x)// 友元函数的定义 {cout<<x.name<<" "<<x.age<<endl;}

int main() { girl e("abc", 15); disp(e); return 0; }

Page 127: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 127

总结 总结 本章介绍了面向对象的程序设计的基本

思想。面向对象程序设计的基本思想是如何根据应用的需求创造合适的类型,用该类型的对象来解决特定的问题。

本章主要介绍了如何定义一个类,如何通过访问控制实现信息的封装。如何定义类的对象,如何实现对象的初始化,如何操作对象。

Page 128: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 128

第第 1111 章 运算符重载 章 运算符重载

什么是运算符重载 运算符重载的方法 几个特殊的运算符的重载 自定义类型转换运算符 运算符重载实例

Page 129: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 129

什么是运算符重载什么是运算符重载 使系统内置的运算符可以用于类类型 例如: + 运算符能够实现 2个对象间的

加。例如:类 A的对象 a1、 a2、 a3,希望:

a3 = a1 + a2 ; 即:分别把对象 a1和 a2的各个数据成

员值对应相加,然后赋给对象 a3。

Page 130: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 130

问题的提出问题的提出

把某些事交给系统去做,用户只要知道相加就可

扩充运算符的功能增强了 C++ 语言的可扩充性使用户定义的类更像系统的内置类型

Page 131: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 131

运算符重载的限制运算符重载的限制

不是所有的运算符都能重载 重载不能改变运算符的优先级和结合性 重载不能改变运算符的操作数个数 不能创建新的运算符

Page 132: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 132

可以重载的运算符可以重载的运算符 +         -         *         /         %         ^         &        |    ~        !         =         <        >        +=        -=        *=    /=        %=        ^=        &=        |=        <<        >>        >>=    <<=       ==        !=        <=        >=        &&        ||        ++    --        ->*        , ->       []        ()        new       delete    new[]     delete[]

Page 133: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 133

不能重载的运算符不能重载的运算符

  .        .*        ::        ?:        sizeof

Page 134: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 134

第第 1111 章 运算符重载 章 运算符重载

什么是运算符重载 运算符重载的方法 几个特殊的运算符的重载 自定义类型转换运算符 运算符重载实例

Page 135: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 135

运算符重载的方法运算符重载的方法 运算符重载就是写一个函数解释某个运算符在

某个类中的含义 要使得系统能自动找到重载的这个函数,函数名必须要体现出和某个被重载的运算符的联系。

C++ 中规定,重载函数名为 operator@

其中, @ 为要重载的运算符。如要重载“ +”运算符,该重载函数名为 operator+ 。要重载赋值运算符,函数名为 operator= 。

Page 136: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 136

函数原型函数原型 运算符的重载不能改变运算符的运算对象数。因此,重载

函数的形式参数个数(包括成员函数的隐式指针 this )与运算符的运算对象数相同

运算符重载可以重载成成员函数也可以重载成全局函数实现。重载成全局函数时,最好把此函数设为友员函数

如果作为类的成员函数,它的形式参数个数比运算符的运算对象数少 1 。这是因为成员函数有一个隐含的参数 this 。在 C++ 中,把隐含参数 this 作为运算符的第一个参数。

当把一个一元运算符重载成成员函数时,该函数没有形式参数。

把一个二元运算符重载成成员函数时,该函数只有一个形式参数,就是右操作数,当前对象是左操作数。

Page 137: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 137

重载实例重载实例

为 rational 类增加“ +” 和“ *” 以及比较的重载函数,用以替换现有的 add 和multi 函数

Page 138: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 138

方案一:重载成成员函数方案一:重载成成员函数class Rational {private:

int num;int den;void ReductFraction();

public:Rational(int n = 0, int d = 1) { num = n; den = d;}

Rational operator+(const Rational &r1) const; Rational operator*(const Rational &r1) const;

bool operator<(const Rational &r1) const; bool operator==(const Rational &r1) const; bool operator>(const Rational &r1) const; bool operator<=(const Rational &r1) const; bool operator>=(const Rational &r1) const; bool operator!=(const Rational &r1) const; void display() { cout << num << '/' << den; }

}

Page 139: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 139

函数实现函数实现Rational Rational::operator+(const Rational &r1) const{ Rational tmp; tmp.num = num * r1.den + r1.num * den; tmp.den = den * r1.den; tmp.ReductFraction(); return tmp;}

Rational Rational::operator*(const Rational &r1) const{ Rational tmp; tmp.num = num * r1.num; tmp.den = den * r1.den; tmp.ReductFraction(); return tmp;}

Page 140: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 140

bool Rational::operator<(const Rational &r1) const{ return num * r1.den < den * r1.num; }

bool Rational::operator==(const Rational &r1) const{ return num == r1.num && den == r1.den;}

bool Rational::operator>(const Rational &r1) const{ return num * r1.den > den * r1.num; }

bool Rational::operator<=(const Rational &r1) const{ return num * r1.den <= den * r1.num; }

bool Rational::operator>=(const Rational &r1) const{ return num * r1.den >= den * r1.num; }

bool Rational::operator!=(const Rational &r1) const{ return !(*this == r1);}

Page 141: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 141

方案二:重载成友员函数方案二:重载成友员函数class Rational { friend Rational operator+(const Rational &r1, const Rational &r2); friend Rational operator*(const Rational &r1 , const Rational

&r2); friend bool operator<(const Rational &r1 , const Rational &r2) ;

friend bool operator==(const Rational &r1 , const Rational &r2); friend bool operator>(const Rational &r1 , const Rational &r2) ; friend bool operator<=(const Rational &r1 , const Rational &r2); friend bool operator>=(const Rational &r1 , const Rational &r2); friend bool operator!=(const Rational &r1 , const Rational &r2) ;

private: int num;int den;void ReductFraction();

public:Rational(int n = 0, int d = 1) { num = n; den = d;}void display() { cout << num << '/' << den;}

};

Page 142: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 142

函数的实现函数的实现Rational operator+(const Rational &r1, const Rational &r2){ Rational tmp; tmp.num = r1.num * r2.den + r2.num * r1.den; tmp.den = r1.den * r2.den; tmp.ReductFraction(); return tmp;}

Rational operator*(const Rational &r1, const Rational &r2){ Rational tmp; tmp.num = r1.num * r2.num; tmp.den = r1.den * r2.den; tmp.ReductFraction(); return tmp; } 其他函数实现略

Page 143: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 143

重载后有理数类的使用重载后有理数类的使用int main(){ Rational r1(1,6), r2(1,6), r3; r3 = r1 + r2; r1.display(); cout << " + "; r2.display(); cout << " = "; r3.display(); cout << endl; r3 = r1 * r2; r1.display(); cout << " * "; r2.display(); cout << " = "; r3.display(); cout << endl; return 0;}

Page 144: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 144

全局函数 全局函数 vsvs 成员函数成员函数 大多数运算符都可以重载成成员函数或全局函数。

赋值( = )、下标( [] )函数调用(())和成员访问( -> )必须重载成成员函数。

具有赋值意义的运算符,如复合的赋值运算符以及 ++

和 -- ,不一定非要定义为成员函数,但最好定义为成员函数。

具有两个运算对象的运算符最好重载为全局函数,这样可以使得应用更加灵活。如果把加运算定义成全局函数, r

是有理数类的对象,则 2+r 是一个合法的表达式。

Page 145: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 145

第第 1111 章 运算符重载 章 运算符重载

什么是运算符重载 运算符重载的方法 几个特殊的运算符的重载 自定义类型转换运算符 运算符重载实例

Page 146: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 146

几个特殊的运算符的重载 几个特殊的运算符的重载

赋值运算符 下标运算符 函数调用运算符 ++ 和—运算符的重载 重载函数的原型设计考虑 输入输出运算符重载

Page 147: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 147

赋值运算符赋值运算符 对任一类,如果用户没有自定义赋值运算符函数,那么系统为其生成一个缺省的赋值运算符函数,在对应的数据成员间赋值。

一般情况下,这个缺省的赋值运算符重载函数能满足用户的需求。但是,当类含有类型为指针的数据成员时,可能会带来一些麻烦。

Page 148: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 148

对对 DoubleArrayDoubleArray 类对象执行类对象执行array1 = array2array1 = array2 的问题的问题

会引起内存泄漏 使这两个数组的元素存放于同一块空间中 当这两个对象析构时,先析构的对象会释放存储数组元素的空间。而当后一个对象析构时,无法释放存放数组元素的空间

Page 149: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 149

赋值运算符“赋值运算符“ =”=” 的原型的原型 赋值运算符只能重载成成员函数 函数原型: X &X::operator=(const X &source)

{

// 赋值过程 }

一旦创建了对象 x1, x2, 可以用 x1 = x2赋值。

Page 150: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 150

DoubleArrayDoubleArray 类的类的赋值运算符重载函数赋值运算符重载函数

DoubleArray &DoubleArray::operator= (const DoubleArray &right){ if (this == &right) return *this; delete [ ] storage; low = right.low; high = right.high; storage = new double[high - low + 1]; for (int i=0; i <= high - low; ++i) storage[i] = right.storage[i]; //复制数组元素 return *this;}

Page 151: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 151

赋值运算符重载要点赋值运算符重载要点 一般来讲,需要自定义拷贝构造函数的类也需要

自定义赋值运算符重载函数。 在赋值运算符重载函数中,已经将参数的值赋值

给了当前对象,那为什么还需要返回值呢?记住,在 C++ 中,赋值是一个运算,它可以形成一个表达式,而该表达式的结果值就是赋给左边的对象的值。因此,赋值运算符重载函数必须返回赋给左边的对象值。

Page 152: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 152

赋值运算符重载和拷贝构造函数赋值运算符重载和拷贝构造函数

一般来讲,需要拷贝构造函数的类也需要重载赋值运算符

定义对象时给对象赋初值调用的是拷贝构造函数

程序的语句部分中的赋值语句调用的是赋值运算符重载函数

Page 153: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 153

几个特殊的运算符的重载 几个特殊的运算符的重载

赋值运算符 下标运算符 函数调用运算符 ++ 和—运算符的重载 重载函数的原型设计考虑 输入输出运算符重载

Page 154: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 154

下标运算符重载下标运算符重载

能否象普通的数组那样通过下标运算操作 Dou

bleArray 类的对象,这样可以使 DoubleArray

类更像一个功能内置的数组。 可以通过重载下标运算符( [] )来实现 下标运算符是二元运算符,第一个运算数是数组名,第二个运算数是下标值

下标运算符必须重载成成员函数

Page 155: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 155

DoubleArrayDoubleArray 类的类的 [ ][ ] 重载重载

double & DoubleArray::operator[](int index)

{ if (index < low || index > high)

{cout << " 下标越界 "; exit(-1); }

return storage[index - low];

}

Page 156: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 156

DoubleArrayDoubleArray 类的使用类的使用 定义: DoubleArray array(20, 30); 数组输入: for (i=20; i<=30; ++i) {

cout << "请输入第 " << i << " 个元素 :";

cin >> array[i];

} 数组输出: for (i=20; i<=30; ++i)

cout << array[i] << '\t';

Page 157: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 157

几个特殊的运算符的重载 几个特殊的运算符的重载

赋值运算符 下标运算符 函数调用运算符 ++ 和—运算符的重载 重载函数的原型设计考虑 输入输出运算符重载

Page 158: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 158

函数调用运算符函数调用运算符

函数调用运算符()是一个二元运算符。它的第一个运算对象是函数名,第二个参数是形式参数表。运算的结果是函数的返回值。

一个类重载了函数调用运算符,就可以把这个类的对象当做函数来使用

Page 159: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 159

函数调用运算符重载函数调用运算符重载

函数调用运算符必须重载成成员函数

函数调用运算符重载函数的原型为

函数的返回值 operator() ( 形式参数表 );

Page 160: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 160

函数调用运算符重载实例函数调用运算符重载实例 在 DoubleArray 类增加一个功能:取数组中

的一部分元素形成一个新的数组 例如,在一个下标范围为 10 到 20 的数组 ar

r 中取出下标为第 12 到 15 的元素,形成一个下标范围为 2 到 5 的数组存放在数组 arr1

中,可以调用

arr1 = arr(12, 15, 2) 。

Page 161: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 161

DoubleArray operator()(int start, int end, int lh)

{

if (start > end || start < low || end > high )

{ cout << " 下标越界 "; exit(-1); }

DoubleArray tmp(lh, lh + end - start);

for (int i = 0; i < end - start + 1; ++i)

tmp.storage[i] = storage[start + i - low];

return tmp;

}

Page 162: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 162

几个特殊的运算符的重载 几个特殊的运算符的重载

赋值运算符 下标运算符 函数调用运算符 ++ 和—运算符的重载 重载函数的原型设计考虑 输入输出运算符重载

Page 163: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 163

““++”++” 和“和“ --”--” 重载重载 ++、 - -:是一元操作符 这两个操作符可以是前缀,也可以是后缀。而且前缀和后缀的含义是有区别的。所以,必须有两个重载函数。

问题:两个重载函数有相同的原型 区分方法: 前缀:一元操作符。 后缀:二元操作符。

Page 164: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 164

““++”++” 和“和“ --”--” 重载 重载 cont.cont.

成员函数重载 ++ob 重载为: ob.operator++()

ob-- 重载为: ob.operator--(int) 友元函数重载 ++ob 重载为: operator++(X &ob)

ob-- 重载为: operator--(X &ob, int) 调用时,参数 int 一般传递给值 0 。

Page 165: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 165

++++ 、、 ---- 重载实例重载实例

设计一个会报警的计数器类。该计数器从 0开始计数,当到达预先设定好的报警值时,计数器会发出报警消息,计数器的值不再增加。

Page 166: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 166

类定义类定义class Counter {

int value; // 计数器的值int alarm; //报警值

public:

Counter(int a) {value = 0; alarm = a;}

Counter & operator++(); //前缀的 ++ 重载Counter operator++(int); // 后缀的 ++ 重载void print() {cout << value << endl; }

};

Page 167: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 167

类实现类实现Counter & Counter::operator++(){ if (value == alarm) cout << " 已超过报警值 \n"; else { ++value; if (value == alarm) cout << " 已到达报警值 \n";

} return *this;}

Counter Counter::operator++(int x){ Counter tmp = *this; // 保存对象修改前的状态 if (value == alarm) cout << " 已超过报警值 \n"; else { ++value; if (value == alarm) cout << " 已到达报警值 \n";

} return tmp; //返回修改前的状态}

Page 168: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 168

类的使用类的使用int main()

{ Counter cnt(3); // 定义一个 Counter 类的对象,报警值为 3

cnt.print(); /显示对象的当前值,此时输出为 0

++cnt;

cnt.print(); // 此时输出为 1

(++cnt).print(); //调用前缀的 ++ ,输出 2

(cnt++).print(); //调用后缀的 ++ ,当前对象的 value 已经 // 加 1 ,报警。但输出的是 2

cnt.print(); // 输出值为 3

return 0;

}

Page 169: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 169

几个特殊的运算符的重载 几个特殊的运算符的重载

赋值运算符 下标运算符 函数调用运算符 ++ 和—运算符的重载 重载函数的原型设计考虑 输入输出运算符重载

Page 170: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 170

重载函数的原型设计考虑重载函数的原型设计考虑 参数设计

对于任何函数的参数,如果仅需要从参数中读,而不改变它,一般用 const引用来传递。

只有会修改左值参数的运算符,如赋值运算符,左值参数不是常量,所以用地址传递

返回值的类型设计 运算符的结果产生一个新值,就需要产生一个作为返回

值的新对象 对于逻辑运算符,人们希望至少得到一个 int 或 bool 的返回值

所有的赋值运算符(如, = , +=等)均改变左值,应该能够返回一个刚刚改变了的左值的非常量引用

Page 171: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 171

值返回时的优化值返回时的优化 在返回一个对象时,通常有两种写法。如某

函数返回一个 Rational 类的对象,它的值为两个参数的成员对应相加。它的两种写法为 return Rational( left.num + right.num,

left.den + right.den); Rational tmp;

tmp.num = left.num + right.num;

tmp.den = left.den + right.den;

return tmp;

Page 172: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 172

两种写法的比较两种写法的比较 前者的意思是“创建一个临时对象,并返回

它” 。它只调用了一次构造函数。 而后者,先创建了一个对象 tmp ,这将调用

构造函数,然后对 tmp赋值,最后返回 tmp 。而在返回 tmp 时,又要创建一个临时对象,并调用拷贝构造函数用 tmp 对它进行初始化。在函数执行结束时,还要调用析构函数析构tmp 。

Page 173: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 173

几个特殊的运算符的重载 几个特殊的运算符的重载

赋值运算符 下标运算符 函数调用运算符 ++ 和—运算符的重载 重载函数的原型设计考虑 输入输出运算符重载

Page 174: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 174

输入输出运算符重载输入输出运算符重载

输入输出运算符必须被重载成全局函数。 输出运算符的重载 输入运算符的重载

借助于流插入运算符 (>>)和流提取运算符 (<<)输入和输出用户自定义类的对象

Page 175: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 175

输出重载函数的原型输出重载函数的原型

ostream & operator<<(ostream & os,

const ClassType &obj)

{ os << 要输出的内容; return os;

}

Page 176: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 176

实例实例

ostream& operator<<(ostream &os, const Rational& obj) // 输出重载函数{ os << obj.num << '/' << obj.den; return os;}

如定义:Rational r(2,6);执行 cout << r; 的结果是 1/3 。

为 Rational类重载输出

Page 177: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 177

输入输出运算符重载输入输出运算符重载

输入输出运算符必须被重载成全局函数。 输出运算符的重载 输入运算符的重载

借助于流插入运算符 (>>)和流提取运算符 (<<)输入和输出用户自定义类的对象

Page 178: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 178

输入重载函数的原型输入重载函数的原型

istream & operator>>(istream & is,

ClassType &obj)

{ is >> 要输入的内容; return is;

}

Page 179: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 179

实例实例

istream& operator>>(istream &in, Rational& obj)

// 输入重载函数{ in >> obj.num >> obj.den;

obj.ReductFraction();

return in;

}

如定义: Rational r;

可以用 cin >> r 从键盘输入 r 的数据。如输入为: 1 3

执行 cout << r; 的结果是 1/3 。

为 Rational类重载输入

Page 180: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 180

第第 1111 章 运算符重载 章 运算符重载

什么是运算符重载 运算符重载的方法 几个特殊的运算符的重载 自定义类型转换运算符 运算符重载实例

Page 181: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 181

类型转换类型转换 ---- 系统预定义类型间的转换系统预定义类型间的转换

隐式类型转换 ※ 赋值时 ※ 运算时 显式类型转换 ※ 强制转换法:(类型名)表达式 ※ 函数法:类型名(表达式)

Page 182: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 182

自定义类型转换运算符自定义类型转换运算符

类类型能否和其他的类类型或内置类型互相转换?

内置类型之所以能互相转换是因为系统预先制定了转换的规则,并写好了完成转换的程序。

类类型与其它类类型或内置类型之间如何转换,编译器预先无法知道。类的设计者必须定义转换的方法。

Page 183: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 183

类型转换类型转换

内置类型到类类型的转换 类类型到其它类型的转换

Page 184: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 184

内置类型到类类型的转换内置类型到类类型的转换

利用构造函数进行转换。 例如,对于 Rational 类的对象 r ,可以执行 r=2 。

此时,编译器隐式地调用 Rational 的构造函数,传给它一个参数 2 。构造函数将构造出一个 num=2 , den= 1 的 Ratio

nal 类的对象,并将它赋给 r 。

Page 185: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 185

explicitexplicit 构造函数构造函数 任何单参数的构造函数都可以被编译器用来执行

隐式转换,即把内置类型转换成对应的类类型。 在某些情况下,隐式转换是不受欢迎的。 将单参数的构造函数定义为 explicit ,将告诉编译器不允许执行隐式转换。

如将 Ratioanal 类的构造函数定义成 explicit Rational(int n1 = 0, int n2 = 1)

则对于 Rational 类的对象 r1 和 r2 ,执行 r1 = 2 + r2;编译器就会报错

Page 186: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 186

类型转换类型转换

内置类型到类类型的转换 类类型到其它类型的转换

Page 187: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 187

类类型到内置类型或其他类类型类类型到内置类型或其他类类型的转换的转换

可以通过类型转换函数实现 类型转换函数必须重载成成员函数 类型转换函数的格式 operator 目标类型名 ( ) const { … return ( 结果为目标类型的表达式 ); } 类型转换函数的特点

无参数,无返回值 是 const 函数

Page 188: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 188

RationalRational 类到类到 doubledouble 的转换的转换 转换函数的定义: operator double () const

{ return (double(num)/den);}

有了这个函数,我们可以将一个 Ration

al 类的对象 r赋给一个 double 类型的变量 x 。如 r 的值为( 1 , 3 ),经过赋值 x = r 后, x 的值为 0.333333

Page 189: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 189

经过运算符重载后的经过运算符重载后的 RationalRational 类类class Rational {

friend istream& operator>>(istream &in, Rational& obj);

friend ostream& operator<<(ostream &os, const Rational& obj);

friend Rational operator+(const Rational &r1, const Rational &r2);

friend Rational operator*(const Rational &r1, const Rational &r2);

private:

int num;

int den;

void ReductFraction();

public:

Rational(int n = 0, int d = 1) { num = n; den = d;}

operator double () const { return (double(num)/den);}

};

Page 190: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 190

RationalRational 类的使用类的使用#include <iostream.h>#include "Rational.h"int main(){ Rational r1, r2, r3, r4; double x; cout << " 输入 r1: "; cin >> r1; cout << " 输入 r2: "; cin >> r2; r3 = r1 + r2; cout << r1 << '+' << r2 << " = " << r3 << endl; r3 = r1 * r2; cout << r1 << '*' << r2 << " = " << r3 << endl; r4 = (r1 + r2) * r3; cout << "(r1 + r2) * r3 的值为: " << r4 << endl; x = 5.5 - r1; cout << "5.5 - r1 的值为: " << x << endl; cout << (r1 < r2 ? r1 : r2) << endl; return 0;}

输入 r1: 1 3

输入 r2: 2 6

1/3+1/3 = 2/3

1/3*1/3 = 1/9

(r1 + r2) * r3 的值为 2/27

5.5 - r1 的值为: 5.16667

1/3

Page 191: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 191

第第 1111 章 运算符重载 章 运算符重载

什么是运算符重载 运算符重载的方法 几个特殊的运算符的重载 自定义类型转换运算符 运算符重载实例

Page 192: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 192

运算符重载实例运算符重载实例 完善 DoubleArray 类

Page 193: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 193

DoubleArray.hDoubleArray.h#ifndef _array_h#define _array_h#include <iostream.h>class DoubleArray{

friend ostream &operator<<(ostream &os, const DoubleArray &obj);

friend istream &operator>>(istream &is, DoubleArray &obj);friend bool operator==(const DoubleArray &obj1,

const DoubleArray &obj2);private: int low; int high; double *storage;

Page 194: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 194

public: DoubleArray(int lh = 0, int rh = 0):low(lh), high(rh) { storage = new double [high - low + 1]; } DoubleArray(const DoubleArray &arr);

DoubleArray &operator=(const DoubleArray &right);

double & operator[](int index); const double & operator[](int index) const;

DoubleArray operator()(int start, int end, int lh);

~DoubleArray() {delete [] storage; }};#endif

Page 195: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 195

DoubleArray.cppDoubleArray.cpp//文件名: DoubleArray.cpp

//DoubleArray 类的实现#include <cassert>

#include "DoubleArray.h“

DoubleArray::DoubleArray(const DoubleArray &arr)

{ low = arr.low;

high = arr.high;

storage = new double [high - low + 1];

for (int i = 0; i < high -low + 1; ++i)

storage[i] = arr.storage[i];

}

Page 196: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 196

operator=operator=DoubleArray &DoubleArray::operator= (const DoubleArray & a){ if (this == &a) return *this; delete [] storage; low = a.low; high = a.high; storage = new double[high - low + 1]; for (int i=0; i <= high - low; ++i) storage[i] = a.storage[i];

return *this;}

Page 197: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 197

operator[]operator[]

double & DoubleArray::operator[](int index)

{ assert(index >= low && index <= high);

return storage[index - low];

}

const double & DoubleArray::operator[]

(int index) const

{ assert(index >= low && index <= high);

return storage[index - low];

}

Page 198: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 198

operator<<operator<<

ostream &operator<<(ostream &os, const Double

Array &obj)

{ os << " 数组内容为: \n";

for (int i=obj.low; i<=obj.high; ++i)

os << obj[i] << '\t';

os << endl;

return os;

}

Page 199: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 199

operator>>operator>>

istream &operator>>( istream &is, DoubleArray &obj)

{ cout << "请输入数组元素 [" << obj.low

<< ", " << obj.high << "]:\n";

for (int i=obj.low; i<=obj.high ; ++i)

is >> obj[i] ;

return is;

}

Page 200: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 200

operator==operator==

bool operator==(const DoubleArray &obj1, const DoubleArray &obj2)

{ if (obj1.low != obj2.low

|| obj1.high != obj2.high)

return false;

for (int i = obj1.low; i<=obj1.high; ++i)

if (obj1[i] != obj2[i]) return false;

return true;

}

Page 201: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 201

operator()operator()

DoubleArray DoubleArray::operator() (int start, int end, int lh){ assert (start <= end && start >= low && end <= high );

DoubleArray tmp(lh, lh + end - start);

for (int i = 0; i < end - start + 1; ++i) tmp.storage[i] = storage[start + i - low];

return tmp;}

Page 202: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 202

MainMain 函数函数int main()

{ DoubleArray array1(20,30), array2;

cin >> array1;

cout << "array1 "; cout << array1;

array2 = array1;

cout << "执行 array2 = array1, array2 " << array2;

cout << "array1 == array2 是 "

<< ((array1 == array2) ? "true" : "false") << endl;

array2[25] = 0;

cout << "执行 array[25] = 0 后 , array1 == array2 是 "

<< ((array1 == array2) ? "true" : "false") << endl;

array2 = array1(22, 25, 2);

cout << "执行 array2 = array1(22, 25, 2) 后 , array2 的值为: "

<< array2;

return 0;

}

Page 203: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 203

执行结果执行结果请输入数组元素 [20 , 30] :1 2 3 4 5 6 7 8 9 10 11array1 的内容为:1 2 3 4 5 6 7 8 9 10 11执行 array2 = array1 , array2 的内容为:1 2 3 4 5 6 7 8 9 10 11array1 == array2 是 true执行 array2[25] = 0 后, array1 == array2 是 fals

e执行 array2 = array1(22, 25, 2) 后 , array2 的值

为: 3 4 5 6

Page 204: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 204

小结 小结 运算符重载的作用 如何选择用成员函数或全局函数 如何写一个重载函数 介绍了一种区分 ++ 和—的前后缀应用

的方法 通过运算符重载实现类类型和内置类型及其他类类型之间的转换

Page 205: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 205

第第 1212 章 组合与继承章 组合与继承

组合

继承

虚函数与多态性

纯虚函数与抽象类

多继承

面向对象设计范例

Page 206: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 206

组合组合

组合就是把用户定义类的对象作为新类的数据成员

组合表示一种聚集关系,是一种部分和整体( is a part of )的关系

必须用初始化列表去初始化对象成员

Page 207: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 207

组合实例组合实例

定义一个复数类,而复数的虚部和实部都用有理数表示

Page 208: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 208

类定义类定义class Complex{

friend Complex operator+(Complex x, Complex y);friend istream& operator>>(istream &is,

Complex &obj); friend ostream& operator<<(ostream &os, const Complex &obj);

Rational real; // 实部Rational imag; //虚部

public:Complex(int r1 = 0, int r2 = 1, int i1= 0, int i2 = 1):

real(r1, r2), imag(i1, i2) {}};

Page 209: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 209

成员函数的实现成员函数的实现Complex operator+(Complex x, Complex

y) { Complex tmp; // 利用 Rational 类的加法重载函数完成

实部和虚部的相加 tmp.real = x.real + y.real; tmp.imag = x.imag + y.imag; return tmp;}

Page 210: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 210

istream& operator>>(istream &is, Complex &obj) { cout << "请输入实部: "; is >> obj.real; // 利用 Rational 类的输入重载实现实部的输入 cout << "请输入虚部: "; is >> obj.imag; // 利用 Rational 类的输入重载实现虚部的输入 return is;}

ostream& operator<<(ostream &os, const Complex &obj){ // 利用 Rational 类的输出重载实现实部和虚部的输出 os << '(' << obj.real << " + " << obj.imag << "i" << ')'; return os;}

Page 211: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 211

复数类的使用复数类的使用int main(){Complex x1,x2,x3; cout << "请输入 x1 : \n"; cin >> x1; cout << "请输入 x2: \n"; cin >> x2; x3 = x1 + x2; cout << x1 << " + " << x2 << " =   " << x3 << endl; return 0;}

Page 212: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 212

第第 1212 章 组合与继承章 组合与继承

组合

继承

虚函数与多态性

纯虚函数与抽象类

多继承

面向对象设计范例

Page 213: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 213

派生类的概念派生类的概念 继承是面向对象程序设计的一个重要特征,它允许在已有类的基础上创建新的类

基类、父类 派生类、导出类或子类 继承可以让程序员在已有类的基础上通过增加

或修改少量代码的方法得到新的类,从而较好地解决代码重用的问题。

Page 214: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 214

派生类派生类

单继承的格式 基类成员在派生类中的访问特性 派生类对象的构造、析构与赋值操作 重定义基类的函数 派生类作为基类 将派生类对象隐式转换为基类对象

Page 215: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 215

派生类的定义派生类的定义 一般格式: class 派生类名:派生方法 基类名 {//派生类新增的数据成员和成员函数 }; 派生方法:1. 公有派生 : public

2. 私有派生: private

3. 保护派生: protected

Page 216: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 216

派生实例派生实例class base { int x; public: void setx(int k);}class derived1:public base { int y; public: void sety(int k);}

Derived1 有两

个数据成员: x ,

y 。

有两个成员函

数: setx 和 se

ty

Page 217: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 217

派生类对基类成员的访问派生类对基类成员的访问 派生类的成员函数不能访问基类的私有数

据成员 protected访问特性

protected 成员是一类特殊的私有成员,它不可以被全局函数或其他类的成员函数访问,但能被派生类的成员函数访问

protected 成员破坏了类的封装,基类的 protected 成员改变时,所有派生类都要修改

Page 218: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 218

派生类派生类

单继承的格式 基类成员在派生类中的访问特性 派生类对象的构造、析构与赋值操作 重定义基类的函数 派生类作为基类 将派生类对象隐式转换为基类对象

Page 219: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 219

基类成员的访问说明符 继承类型                     public 继承 protected 继承 private 继承public          在派生类中为 public    在派生类中为 protected        在派生类中为 private

                    可以由任何非 static     可以直接由任何非 static        可以直接由任何非 static                    成员函数、友元函数和 成员函数、友元函数 成员函数、友元函数 非成员函数访问 访问 访问protecetd       在派生类中为 proteced  在派生类中为 protected        在派生类中 private

                    可以直接由任何非 static  成员函数、友元函数访问private         在派生类中隐藏 在派生类中隐藏 在派生类中隐藏

可以通过基类的 public  或 protected 成员函数或非 static 成员函数和友元函数访问

                                 

派生类对基类成员的访问性 派生类对基类成员的访问性

Page 220: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 220

  Derived1

不可访问 Int x

private Int y

public Setx()Sety()

class base { int x; public: void setx(int k);}class derived1:public base { int y; public: void sety(int k);}

Page 221: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 221

继承实例继承实例

定义一个二维平面上的点类型,可以设置点的位置,获取点的位置。在此基础上,扩展出一个三维空间上的点类型。

Page 222: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 222

point_2dpoint_2d 的定义的定义

class point_2d

{private: int x,y;

public:

void setpoint2(int a, int b) {x = a; y = b;}

int getx() {return x;}

int gety() {return y;}

};

Page 223: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 223

point_3dpoint_3d 的定义的定义

class point_3d:public point_2d

{int z;

public:

void setpoint3(int a,int b,int c) {setpoint2(a,b); z=c;}

int getz() {return z;}

}

Page 224: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 224

point_3dpoint_3d 的讨论的讨论 point_3d 的组成:有三个数据成员: x , y 和 z ,

有五个公有的成员函数: setpoint2, setpoint3, get

x, gety 和 getz 。 point_3d 的成员函数无法直接访问基类的 x 和 y ,因此在 setpoint3 函数中必须调用在 point_2d 的公有成员函数 setpoint2 实现。

point_3d 类的使用和普通类完全一样,用户不用去管这个类是用继承方式从另外一个类扩展而来,还是完全直接定义的。

Page 225: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 225

Point_3dPoint_3d 的使用的使用int main(){point_2d p1; point_3d p2;

p1.setpoint2(1, 2); cout << "p1: (" << p1.getx() << ", " << p1.gety() << ")" << endl;

p2.setpoint3(1, 2, 3); cout << "p2: (" << p2.getx() << ", " << p2.gety() << ", " << p2.getz() << ")" << endl;

return 0;}

P1: (1, 2)

P2: (1, 2, 3)

Page 226: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 226

派生类派生类

单继承的格式 基类成员在派生类中的访问特性 派生类对象的构造、析构与赋值操作 重定义基类的函数 派生类作为基类 将派生类对象隐式转换为基类对象

Page 227: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 227

派生类的构造函数和析构函数 派生类的构造函数和析构函数 由于派生类继承了其基类的成员,所以在

建立派生类的实例对象时,必须初始化基类继承的数据成员。派生类对象析构时,也必须析构基类对象。

派生类不继承基类的构造函数、析构函数和赋值运算符,但是派生类的构造函数、析构函数和赋值运算符能调用基类的构造函数、析构函数和赋值运算符。

Page 228: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 228

派生类的构造函数派生类的构造函数

基类成员的初始化由基类的构造函数完成。派生类的构造函数调用基类的构造函数完成基类成员的初始化。

派生类构造函数可以隐式调用基类缺省的构造函数,也可以在派生类的构造函数的初始化列表显式地调用基类的构造函数。

Page 229: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 229

构造函数的格式构造函数的格式 派生类构造函数的格式:

派生类构造函数名(参数表): 基类构造函数名(参数表){ 。。。 }

基类构造函数中的参数表通常来源于派生类构造函数的参数表,也可以用常数值。

如果构造派生类对象时调用的是基类的缺省构造函数,则可以不要初始化列表。

如果派生类新增的数据成员中含有对象成员,则在创建对象时,先执行基类的构造函数,再执行成员对象的构造函数,最后执行自己的构造函数体。

Page 230: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 230

派生类对象的析构派生类对象的析构 派生类的析构函数值析构自己新增的数

据成员,基类成员的析构由基类的析构函数析构

派生类析构函数会自动调用基类的析构函数

派生类对象析构时,先执行派生类的析构函数,再执行基类的析构函数

Page 231: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 231

派生类构造实例派生类构造实例 定义一个二维平面上的点类,并从它派生出一个圆类

Page 232: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 232

point2.hpoint2.h

// Definition of class Point

#ifndef POINT2_H

#define POINT2_H

class Point {public:    Point( int = 0, int = 0 );  // default constructor   ~Point();   // destructorprotected:     // accessible by derived classes   int x, y;   // x and y coordinates of Point};

#endif

Page 233: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 233

point2.cpppoint2.cpp#include "point2.h"

// Constructor for class PointPoint::Point( int a, int b ) { x = a; y = b;

   cout << "Point constructor:"      << '[' << x << ", "<< y << ']' << endl;

}

// Destructor for class PointPoint::~Point() { cout << "Point destructor:  "

     << '[' << x << ", "<< y << ']' << endl; }

Page 234: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 234

circle2.hcircle2.h#ifndef CIRCLE2_H#define CIRCLE2_H

#include "point2.h"

class Circle : public Point { public:

  // default constructor   Circle( double r = 0.0, int x = 0, int y = 0 );

   ~Circle(); private:

   double radius; };

#endif

Page 235: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 235

circle2.cppcircle2.cpp#include "circle2.h"

// Constructor for Circle calls constructor for PointCircle::Circle( double r, int a, int b )

  : Point( a, b )  // call base-class Constructor {   radius = r;  // should validate

cout << "Circle constructor: radius is"        << radius << "[" << x << ", "<< y << ']' << endl;

}

// Destructor roi class CircleCircle::~Circle() {    cout << "Circle destructor: radius is "

        << radius << " [ " << x << ", "<< y << ']'  << endl; }

Page 236: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 236

CircleCircle 类的应用类的应用#include "point2.h“#include "circle2.h"

int main() {    // Show constructor and destructor calls for Point

  {      Point p( 11, 22 ); } cout << endl;Circle circle1( 4.5, 72, 29 );cout << endl;Circle circle2( 10, 5, 5 );cout << endl;return 0;

}

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 ]

Page 237: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 237

派生类构造函数的构造规则派生类构造函数的构造规则 若基类使用缺省或不带参数的构造函数,则在派生类定义构造函数是可略去:基类构造函数名(参数表)。此时若派生类也不需要构造函数,则可不定义构造函数。

当基类构造函数需要参数,而派生类本身并不需要构造函数时,派生类还必须定义构造函数。该函数只是起了一个参数传递作用。

如果省略了派生类的构造函数,那么就由派生类的默认构造函数调用基类的默认构造函数。

Page 238: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 238

派生类实例派生类实例

定义一个图书馆系统中的读者类,每个读者的信息包括:卡号、姓名、单位、允许借书的数量以及已借书记录。学生最多允许借 5 本书,教师最多允许借 10

本书。

Page 239: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 239

设计过程设计过程 系统中有两类读者:学生读者和教师读者。 这两类读者有一部分内容是相同的:卡号、姓名和单位。

可将两类读者的共同部分内容设计成一个基类。

学生读者和教师读者从基类派生,增加已借书的数量以及已借书记录两个数据成员,并将允许借书的数量定义为整个类共享的常量

Page 240: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 240

基类的设计基类的设计class reader{

int no;char name[10];char dept[20];

public:reader(int n, char *nm, char *d) { no = n; strcpy(name, nm); strcpy(dept, d); }

};

Page 241: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 241

教师读者类的设计教师读者类的设计class readerTeacher :public reader{

enum {MAX = 10};

int borrowed;

int record[MAX];

public:

readerTeacher(int n, char *nm, char *d):reader(n, nm, d)

{ borrowed = 0;}

bool bookBorrow(int bookNo);

bool bookReturn(int bookNo);

void show(); //显示已借书信息};

Page 242: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 242

学生读者类的设计学生读者类的设计class readerStudent :public reader {

enum { MAX = 5};

int borrowed;

int record[MAX];

public:

readerStudent(int n, char *nm, char *d):reader(n, nm, d)

{ borrowed = 0; }

bool bookBorrow(int bookNo);

bool bookReturn(int bookNo);

void show(); //显示已借书信息};

Page 243: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 243

派生类派生类

单继承的格式 基类成员在派生类中的访问特性 派生类对象的构造、析构与赋值操作 重定义基类的函数 派生类作为基类 将派生类对象隐式转换为基类对象

Page 244: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 244

重定义基类的函数 重定义基类的函数 派生类是基类的扩展,可以是保存的数据内容的

扩展,也可以是功能的扩展。 当派生类对基类的某个功能进行扩展时,他定义

的成员函数名可能会和基类的成员函数名重复。 如果只是函数名相同,而原型不同时,系统认为派生类中有两个重载函数。如果原型完全相同,则派生类的函数会覆盖基类的函数。这称为重定义基类的成员函数。

Page 245: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 245

实例实例

定义一个圆类型,用于保存圆以及输出圆的面积和周长。在此类型的基础上派生出一个球类型,可以计算球的表面积和体积。

Page 246: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 246

圆类的设计圆类的设计

数据成员:圆的半径 成员函数:由于需要提供圆的面积和周

长,需要提供两个公有的成员函数。除了这些之外,还需要一个构造函数

Page 247: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 247

圆类的定义圆类的定义class circle {protected: double radius;public:

circle(double r = 0) {radius = r;}double getr() {return radius;}double area() { return 3.14 * radius * radius; }double circum() { return 2 * 3.14 * radius;}

};

Page 248: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 248

球类的定义球类的定义class ball:public circle {

public:

ball(double r = 0):circle(r) {}

double area()

{ return 4 * 3.14 * radius * radius; }

double volumn()

{ return 4 * 3.14 * radius * radius * radius / 3; }

};

Page 249: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 249

BallBall 类的构造函数类的构造函数

Ball 类没有新增加数据成员,因而不需要构造函数。但基类的构造函数需要参数,所以 ball 类必须写构造函数

Ball 类构造函数的作用是为 circle 类的构造函数传递参数

Page 250: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 250

BallBall 类的类的 areaarea 函数函数

Ball 类包含了两个原型完全一样的 area

函数。一个是自己定义的,一个是从 cir

cle 类继承来的 当对 ball 类的对象调用 area 函数时,调

用的是 ball 类自己定义的 area 函数

Page 251: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 251

派生类引用基类的同名函数派生类引用基类的同名函数 派生类中重新定义基类的成员函数时,

它的功能往往是基类功能的扩展。为完成扩展的工作,派生类版本通常要调用基类中的该函数版本。

引用基类的同名函数必须使用作用域运算符,否则会由于派生类成员函数实际上调用了自身而引起无穷递归。这样会使系统用光内存,是致命的运行时错误。

Page 252: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 252

实例实例 在 circle 类的基础上定义一个 cylinder 类。

可以计算圆柱体的表面积和体积 设计考虑:

存储圆柱体可以在圆的基础上增加一个高度。圆柱体的表面积是上下两个圆的面积加上它的侧面积

圆柱体的体积是底面积乘上高度。而求圆的面积的函数在 circle 类中已经存在。

Page 253: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 253

CylinderCylinder 类的定义类的定义class cylinder:public circle {

double height;

public:

cylinder(double r = 0, double h = 0):circle(r)

{height = h;}

double geth() {return height;}

double area()

{ return 2 * circle::area() + circum() * height; }

double volumn() { return circle::area() * height ; }

};

Page 254: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 254

BallBall 和和 cylindercylinder 类的使用类的使用int main(){circle c(3); ball b(2); cylinder cy(1,2);

cout << "circle: r=" << c.getr() << endl; cout << "area=" << c.area() << "\tcircum=" << c.circum() << endl;

cout << "ball: r=" << b.getr() << endl; cout << "area=" << b.area() << "\tvolumn=" << b.volumn() << endl;

cout << "cylinder: r=" << cy.getr() << "\th = " << cy.geth() << endl; cout << "area=" << cy.area() << "\tvolumn=" << cy.volumn() << endl; return 0;}

Page 255: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 255

执行结果执行结果

circle: r=3

area=28.26 circum=18.84

ball: r=2

area=50.24 volumn=33.4933

cylinder: r=1

area=18.84 volumn=6.28

Page 256: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 256

派生类派生类

单继承的格式 基类成员在派生类中的访问特性 派生类对象的构造、析构与赋值操作 重定义基类的函数 派生类作为基类 将派生类对象隐式转换为基类对象

Page 257: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 257

派生类作为基类派生类作为基类 基类本身可以是一个派生类,如:

class base { … }

class d1:public base {…}

class d2:public d1 {…}

每个派生类继承他的直接基类的所有成员。 如果派生类的基类是一个派生类,则每个派生类只负责

他的直接基类的构造,依次上溯。 当构造 d2 类的对象时,会先调用 d1 的构造函数,而 d

1 的构造函数执行时又会先调用 base 的构造函数。因此,构造 d2 类的对象时,最先初始化的是 base 的数据成员,再初始化 d1 新增的成员,最后初始化 d2 新增的成员。

析构的过程正好相反。

Page 258: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 258

实例实例#include <iostream>using namespace std;

class base{ int x; public: base(int xx) {x=xx; cout<<"constructing base\n";} ~base() {cout<<"destructint base\n";} };class derive1:public base{ int y;public:

derive1(int xx, int yy): base(xx) {y = yy; cout<<"constructing derive1\n";} ~derive1() {cout<<"destructing derive1\n";} };

Page 259: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 259

class derive2:public derive1{ int z;public:

derive2(int xx, int yy, int zz):derive1(xx, yy) {z = zz;cout<<"constructing derive2\n";} ~derive2() {cout<<"destructing derive2\n";} };main(){derive2 op(1, 2, 3); return 0;}

constructing baseconstructing derive1constructing derive2 destructing derive2destructing derive1destructint base

Page 260: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 260

派生类派生类

单继承的格式 基类成员在派生类中的访问特性 派生类对象的构造、析构与赋值操作 重定义基类的函数 派生类作为基类 将派生类对象隐式转换为基类对象

Page 261: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 261

将派生类对象隐式转换为基类对象将派生类对象隐式转换为基类对象

将派生类对象赋给基类对象 基类指针指向派生类对象 基类的对象引用派生类的对象

Page 262: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 262

将派生类对象赋给基类对象将派生类对象赋给基类对象 派生类中的基类部分赋给此基类对象,派生类

新增加的成员就舍弃了。赋值后,基类对象和派生类对象再无任何关系。

class base { public: int a; };class d1:public base{ public: int b; };

d1 d;d.a = 1; d.b = 2;base bb = d;cout << bb.a; // 输出 1bb.a = 3;cout << d.a; // 输出 1

Page 263: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 263

基类指针指向派生类对象基类指针指向派生类对象 尽管该指针指向的对象是一个派生类对象,但由于它本身是一个基类的指针,它只能解释基类的成员,而不能解释派生类新增的成员。因此,只能访问派生类中的基类部分。

通过指针修改基类对象时,派生类对象也被修改。d1 d;d.a = 1; d.b = 2;base *bp = &d;cout << bp->a; // 输出 1Bp->a = 3;cout << d.a; // 输出 3

Page 264: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 264

基类的对象引用派生类的对象基类的对象引用派生类的对象 给派生类中的基类部分取个别名。 基类对象改变时,派生类对象也被修改。

d1 d;d.a = 1; d.b = 2;base &bb = d;cout << bb.a; // 输出 1bb.a = 3;cout << d.a; // 输出 3

Page 265: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 265

class Shape {public:

void printShapeName() {cout<<“Shape”<<endl;} };

class Point : public Shape {public:

void printShapeName() {cout<<“Point”<<endl;}}

class Circle : public Point {public:

void printShapeName() {cout<<“Circle”<<endl;}}

class Cylinder : public Circle { public:

void printShapeName() {cout<<“Cylinder”<<endl;}}

实例

Page 266: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 266

将派生类对象赋给基类对象将派生类对象赋给基类对象int i;

Point aPoint;

Circle aCircle;

Cylinder aCylinder;

Shape shapes[3]= {aPoint, aCircle, aCylinder};

for (i=0;i<3;i++) shapes[i].printShapeName();

ShapeShapeShape

Page 267: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 267

基类指针指向派生类对象基类指针指向派生类对象

int i;

Point aPoint;

Circle aCircle;

Cylinder aCylinder;

Shape *pShape[3]= { &aPoint, &aCircle,

&aCylinder };

for (i=0;i<3;i++) pShape[i]->printShapeName();

ShapeShapeShape

Page 268: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 268

基类的对象引用派生类的对象基类的对象引用派生类的对象int i;Point aPoint;Circle aCircle;Cylinder aCylinder;

Shape &shape1= aPoint;

shape1.printShapeName();

shape

Page 269: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 269

注意注意 不能将基类对象赋给派生类对象,除非在基类

中定义了向派生类的类型转换函数 不能将基类对象地址赋给指向派生类对象的指针

也不能将指向基类对象的指针赋给指向派生类对象的指针。如果程序员能确保这个基类指针指向的是一个派生类的对象,则可以用 reinterpret_cast 类型的强制类型转换。表示程序员知道这个风险

Page 270: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 270

第第 1212 章 组合与继承章 组合与继承

组合

继承

虚函数与多态性

纯虚函数与抽象类

多继承

面向对象设计范例

Page 271: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 271

虚函数与多态性虚函数与多态性

多态性

虚函数

虚析构函数

Page 272: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 272

多态性多态性

多态性:不同对象收到相同的消息时产生不同的动作。

多态性的作用:便于系统功能的扩展

Page 273: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 273

多态性的实现多态性的实现

静态联编:编译时已决定用哪一个函数实现某一动作。

动态联编:直到运行时才决定用哪一个函数来实现动作

Page 274: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 274

静态联编静态联编

函数重载:用同一名字实现访问一组相关的函数

运算符重载 重载函数是通过“名字压延”方法来实

现。即在编译时将函数名和参数结合起来创造一个新的函数名,用新的名字替换原有名字。

Page 275: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 275

运行时多态性运行时多态性 运行时多态性是指必须等到程序动态运行

时才可确定的多态性,主要通过继承结合动态绑定获得。这与类的继承密切相关。因为存在类型的兼容性,所以有些函数只有在运行时才能确定是调用父类的还是子类的函数。在 C++中,使用虚函数( Virtual Functions)来实现。

Page 276: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 276

虚函数与多态性虚函数与多态性

多态性

虚函数

虚析构函数

Page 277: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 277

虚函数虚函数 虚函数提供动态重载方式,允许函数调用与函数体之间

的联系在运行时才建立。 虚函数的定义:在基类中用关键词 virtual说明,并在派生类中重新定义的函数称为虚函数。在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数与参数类型的顺序都必须与基类中的原型完全相同。

当把一个函数定义为虚函数时,等于告诉编译器,这个成员函数在派生类中可能有不同的实现。必须在执行时根据传递的参数来决定调用哪一个函数

Page 278: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 278

虚函数的使用虚函数的使用

虚函数是与基类指针指向派生类对象,或基类对象引用派生类对象结合起来实现多态性。

当基类指针指向派生类对象或基类对象引用派生类对象时,对基类指针或对象调用基类的虚函数,系统会到相应的派生类中寻找此虚函数的重定义。如找到,则执行派生类中的函数。如没有找到,则执行基类的虚函数。

Page 279: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 279

虚函数虚函数class Shape {public:

virtual void printShapeName() {cout<<“Shape”<<endl;} };

class Point:public Shape {public:

virtual void printShapeName() {cout<<“Point”<<endl;}}

class Circle:public Point {public:

virtual void printShapeName() {cout<<“Circle”<<endl;}}

class Cylinder:public Circle { public:

virtual void printShapeName() {cout<<“Cylinder”<<endl;}}

Page 280: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 280

将派生类对象赋给基类对象将派生类对象赋给基类对象int i;

Point aPoint;

Circle aCircle;

Cylinder aCylinder;

Shape shapes[3]= {aPoint, aCircle, aCylinder};

for (i=0;i<3;i++) shapes[i].printShapeName();

ShapeShapeShape

Page 281: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 281

基类指针指向派生类对象基类指针指向派生类对象int i;

Point aPoint;

Circle aCircle;

Cylinder aCylinder;

Shape *pShape[3]= { &aPoint, &aCircle,

&aCylinder };

for (i=0;i<3;i++) pShape[i]->printShapeName();

PointCircleCylinder

Page 282: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 282

基类的对象引用派生类的对象基类的对象引用派生类的对象

int i;

Point aPoint;

Circle aCircle;

Cylinder aCylinder;

//Shape *pShape[3]= {&aPoint, &aCircle, &aCylinder};

Shape &shape1= aPoint;

//for (i=0;i<3;i++) pShape[i]->printShapeName();

shape1.printShapeName();

Point

Page 283: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 283

使用虚函数的注意事项 使用虚函数的注意事项 在派生类中重新定义虚函数时,它的原型必须

与基类中的虚函数完全相同。否则编译器会把它认为是重载函数,而不是虚函数的重定义。

派生类在对基类的虚函数重定义时,关键字 vi

rtual 可以写也可以不写。不管 virtual写或者不写,该函数都被认为是虚函数。但最好是在重定义时写上 virtual 。

Page 284: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 284

例例

正方形是一类特殊的矩形,因此,可以从 rectangle 类派生一个 square 类。在这两个类中,都有一个显示形状的函数。

Page 285: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 285

class rectangle { int w, h;public: rectangle(int ww, int hh): w(ww), h(hh) {} virtual void display() {cout << “this is a rectangle\n”;}};

class square:public rectangle {public: square(int ss): rectangle(ss, ss) {} void display() //虚函数 {cout << “this is a square\n”;}};

Page 286: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 286

虚函数与多态性虚函数与多态性

多态性

虚函数

虚析构函数

Page 287: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 287

为什么需要虚析构函数为什么需要虚析构函数

构造函数不能是虚函数,但析构函数可以是虚函数,而且最好是虚函数

如果派生类新增加的数据成员中含有指针,指向动态申请的内存,那么派生类必须定义析构函数释放这部分空间。但如果派生类的对象是通过基类的指针操作的,则 delete 基类指针指向的对象就会造成内存泄漏。

Page 288: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 288

解决方案解决方案

将基类的析构函数定义为虚函数。 当析构基类指向的派生类的对象时,找

到基类的析构函数。由于基类的析构函数是虚函数,又会找到派生类的析构函数,执行派生类的析构函数。

Page 289: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 289

虚析构函数的继承性虚析构函数的继承性

和其他的虚函数一样,析构函数的虚函数的性质将被继承。

如果继承层次树中的根类的析构函数是虚函数的话,所有派生类的析构函数都将是虚函数。

Page 290: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 290

第第 1212 章 组合与继承章 组合与继承

组合

继承

虚函数与多态性

纯虚函数与抽象类

多继承

面向对象设计范例

Page 291: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 291

纯虚函数纯虚函数

纯虚函数:是一个在基类中说明的虚函数,它在该基类中没有定义,但要在它的派生类里定义自己的版本,或重新说明为纯虚函数

纯虚函数的一般形式 virtual 类型 函数名(参数表) =0

Page 292: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 292

纯虚函数实例纯虚函数实例class shape{ protected:

double x, y; public:

shape(double xx, double yy) {x=xx; y=yy;} virtual double area() = 0;

virtual void display()     {cout << "This is a shape. The position is ("       << x << ", " << y << ")\n";}};

Page 293: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 293

抽象类抽象类 抽象类:如果一个类中至少有一个纯虚函数,则该类被称

为抽象类 抽象类使用说明: ※ 抽象类只能作为其他类的基类,不能建立抽象类的对象。 ※ 可以声明指向抽象类的指针或引用,此指针可指向它的派生类,进而实现多态性

※ 抽象类不能用作参数类型、函数返回类型或显式转换类型

※ 如果派生类中给除了基类所有纯虚函数的实现,则该派生类不再是抽象类,否则仍为抽象类

Page 294: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 294

抽象类的意义抽象类的意义 保证进入继承层次的每个类都具有纯虚

函数所要求的行为,这保证了围绕这个继承层次所建立起来的软件系统能正常运行,避免了这个继承层次的用户由于偶尔的失误(忘了为它所建立的派生类提供继承层次所要求的行为)而影响系统正常运行

Page 295: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 295

抽象类实例抽象类实例 下面程序用于计算各类形状的总面积#include <iostream.h>class shape{ public: virtual void area()=0;};class rectangle:public shape{float w,h; public:rectangle(float ww, float hh){w=ww; h=hh;}   void area() {cout<<"\narea is:"<<w*h;}};

class circle:public shape{float r; public:circle(float rr) {r=rr;}

  void area(){cout<<"\narea is:"<<3.14*r*r;}};void main(){shape *ptr; rectangle ob1(5,10); circle ob2(10);   ptr=&ob1; ptr->area();   ptr=&ob2; ptr->area();}

Page 296: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 296

第第 1212 章 组合与继承章 组合与继承

组合

继承

虚函数与多态性

纯虚函数与抽象类

多继承

面向对象设计范例

Page 297: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 297

多重继承多重继承 一个派生类有多个基类时称为多重继承

多继承时,派生类包含所有基类的成员

A B C

D

Page 298: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 298

多重继承的定义格式多重继承的定义格式

class 派生类名: 基类表 {

新增派生类的数据成员和成员函数 };

基类表为:派生方法 1 基类名 1 , 派生方法 2 基类名 2 ,……,派生方法n 基类名 n

Page 299: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 299

多继承的访问特性多继承的访问特性

与单继承相同 取决于每个基类的派生方法,和成员在

基类中的访问特性

Page 300: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 300

多重继承的访问特性多重继承的访问特性 与单继承规则相同,例: #include <iostream.h> class X{int a; public: void setX(int x) {a=x;} void showX() {cout<<a; } }; class Y{int b; public: void setY(int x) {b=x;} void showY() {cout<<b;} }; class Z: public X, private Y{ int c; public: void setZ(int x, int y) {c=x; setY(y);} void showZ() {cout<<c;} }; main() {Z obj; obj.setX(3); obj.showX(); obj.setY(4); obj.showY(); // 非法 obj.setZ(5,6); obj.showZ(); }

类 z 有三个数据成员和六个成员函数

Page 301: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 301

多重继承的构造函数和析构函数多重继承的构造函数和析构函数 构造函数的定义形式 派生类构造函数名(参数表):基类 1

构造函数名(参数表),基类 2 构造函数名(参数表),。。。基类 n 构造函数名(参数表) { }

执行顺序:先执行基类(按照继承的次序,而不是构造函数的初始化列表的次序),再执行派生类。

析构的次序和构造相反

Page 302: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 302

多重继承实例多重继承实例#include<iostream.h>class A{ int i; public:

A(int ii=0){ i=ii; cout<<"A... i="<<i<<endl; }   void show() { cout<<"A::show() i="<<i<<endl;} };class B{ int i; public:

B(int ii=0){ i=ii; cout<<"B... i="<<i<<endl; }   void show() { cout<<"B::show() i="<<i<<endl; } };class C: public A, public B { int i; public: C(int i1=0, int i2=0, int i3=0) :A(i1), B(i2) { i=i3;  cout<<"C... i="<<i<<endl;} void show() {cout<<"C::show() i="<<i<<endl;} };void main(){ C c(1,2,3); c.show();}

Page 303: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 303

执行结果执行结果

A... i=1

B... i=2

C... i=3

C::show() i=3

Page 304: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 304

多重继承的主要问题多重继承的主要问题

二义性多个基类有相同的的成员名

有两个以上的基类有共同的基类

Page 305: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 305

二义性二义性 如果多个基类中有相同的成员名,则派生类在引用

时就具有二义性。例: class A{ public: void f();}; class B{ public: void f(); void g();}; class C: public A, public B { public: void g(); void h();}; 如有: C x; 则 x.f () 有二义性。

Page 306: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 306

x.f()x.f() 的解决方法的解决方法 方法一: C 类的成员在引用 f 时说明是 A

的 f 还是 B 的 f 。如: x.A::f(); 或 x.B::f

();

其缺陷是对 C 的用户不利。 C 用户必须知道自己是从哪些基类派生出来的。

方法二:在 C 类声明中指出基类名。如在 C

中可声明: void ha() {A::f(); }

void hb() { B::f();}

Page 307: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 307

二义性二义性 ---cont.---cont.

如果一个派生类从多个基类派生,而这些基类又有公共的基类,则对该基类中声明的名字进行访问时,可能会产生二义性。这个问题由虚基类来解决。

Page 308: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 308

二义性二义性 ---cont.---cont.

如:Class B{ public: int b;};

Class B1: public B{ private: int b1;};

Class B2: public B{ private: int b2;};

Class C: public B1, public B2

{public: int f();

private: int d;};

定义: C c;

下面对 b 的访问是有二义性的:

c.b

c.B::b

下面对 b 的访问是正确的:

c.B1::b

b.B2::b

Page 309: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 309

二义性实例二义性实例class A{public: void fun(){cout<<"A:fun()"<<endl; } };class B1:public A {public: void fun1(){cout<<"B1:fun1()"<<endl; } };class B2:public A {public: void fun1(){cout<<"B2:fun1()"<<endl; } };class D:public B 1,public B 2 {};void main(){ D obj; //obj.fun1(); // 不可以执行各有 fun1() 函数 ) obj.B1::fun1(); obj.B2::fun1(); //obj.fun(); // 不可以执行 //obj.A::fun(); // 不可以执行:二义性 obj.B1::fun(); //无二义性:可以执行 }

Page 310: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 310

虚基类虚基类 用途:当一个派生类是从多个基类派生,

而这些基类又有一个公共的基类。则在这个派生类中访问公共基类中的成员时会有二义性问题。如上例中的 B, B1, B2和 C ,他们的派生关系是:

B B

B1 B2

C

如果 B 只有一个拷贝的话,那么在 C 中对 B 成员的访问就不会有二义性。

Page 311: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 311

虚基类的概念虚基类的概念 使公共的基类只产生一个拷贝。 虚基类的定义用关键词 virtual 。如: Class B{ public: int b;};

Class B1: virtual public B{ private: int b1;};

Class B2: virtual public B{ private: int b2;};

Class C: public B1, public B2

{public: int f();

private: int d;};

这样 B1, B2公用了一个 B 的拷贝, 对 B 成员的引用就不会产生二义性。

B

B1 B2

C

Page 312: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 312

虚基类的初始化虚基类的初始化 保证虚基类对象只被初始化一次。 虚基类的构造由最终的类的构造函数负责。 例如,在构造 C 的对象时,由 C 的构造函数负责调用 B 的构造函数,而不是由 B1 、 B2来调用。

构造次序:先执行 B 的构造函数,再执行 B1 、 B

2 的构造函数,最后执行 C 的构造函数。 析构次序与构造次序相反。

Page 313: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 313

虚基类的初始化实例虚基类的初始化实例#include <iostream.h>class B{ int a; public: B(int sa) {a=sa; cout<<"constructing B\n";}};class B1:virtual public B{ int b;

public: B1(int sa, int sb):B(sa) {b=sb; cout<<"constructing B1\n";} };

class B2:virtual public B{ int c;public: B2(int sa, int sb):B(sa) {c=sb; cout<<"constructing B2\n";}};

class C: public B1, public B2{ int d;public: C(int sa, int sb, int sc, int sd):

B(sa), B1(sa, sb), B2(sa,sc) {d=sd; cout<<"constructing C\n";}};main() {C obj(2,4,6,8); return 0;}

Page 314: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 314

虚基类的初始化实例虚基类的初始化实例 执行结果: constructing B

constructing B1

constructing B2

constructing C

Page 315: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 315

第第 1212 章 组合与继承章 组合与继承

组合

继承

虚函数与多态性

纯虚函数与抽象类

多继承

面向对象设计范例

Page 316: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 316

龟兔赛跑问题龟兔赛跑问题

确定对象:乌龟、兔子和裁判员 类的确定:乌龟类、兔子类和裁判员类 乌龟类没有数据成员,只有一个成员函

数 move

兔子类没有数据成员,只有一个成员函数 move

Page 317: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 317

乌龟类的定义乌龟类的定义class Tortoise {public:

int move(){ int probability = rand() * 10 / (RAND_MAX + 1);

if (probability < 5) return 3; // 快走 else if (probability < 7) return -6; // 后滑 else return 1; //慢走

}};

Page 318: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 318

兔子类的定义兔子类的定义class Hare {public:

int move(){ int probability = rand() * 10 / (RAND_MAX + 1);

if (probability < 2) return 0; //睡觉 else if (probability < 4) return -9; // 大后滑 else if (probability < 5) return 14; // 快走 else if (probability < 8) return 3;//小步跳 else return -2; //慢后滑 }};

Page 319: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 319

裁判员类的设计裁判员类的设计 裁判员的功能:

记录乌龟和兔子在比赛中的位置输出它们的位置判断比赛是否结束并给出比赛的结果

裁判类:两个数据成员,分别为乌龟和兔子的位置 5 个成员函数:

更新乌龟和兔子的位子 update输出乌龟壳兔子的当前位置 print判断比赛是否结束 isFinish并给出胜者输出报表头部的函数重新开始比赛。

Page 320: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 320

裁判员类的定义裁判员类的定义class Judger {

int hare, tortoise; enum { RACE_END = 100 }; // 赛道长度

public:Judger() {

hare = tortoise = 0;srand(time(NULL));

}void restart() { hare = tortoise = 0;} // 重新开始比赛void print_title() const { // 显示输出信息的头部

cout << " hare\ttortoise\n";cout << " ======= ========\n";

}

Page 321: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 321

void update(int h, int t) { // 更新兔子和乌龟的当前位置hare += h;tortoise += t;

}void print() const { // 输出乌龟和兔子的当前位置

cout << hare << '\t' << tortoise << endl;}bool isFinish(int &winner) const {//0:兔子赢, 1:乌龟赢 , 2 :平局

if (hare >= RACE_END || tortoise >= RACE_END) {if (hare > tortoise) winner = 0;else if (hare < tortoise) winner = 1; else winner = 2;

return true;}else return false;

}};

Page 322: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 322

龟兔赛跑过程模拟 龟兔赛跑过程模拟 int main(){ Tortoise tort; Hare hare;

Judger judger;int winner;judger.print_title();while (!judger.isFinish(winner)) {

judger.update(hare.move(), tort.move());judger.print();

}switch (winner) { // 输出比赛结果 case 0:cout << "兔子赢了! "; break; case 1:cout << "乌龟赢了! "; break; default:cout << "平局 ";}return 0;

}

Page 323: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 323

统计统计 nn次龟兔赛跑的结果 次龟兔赛跑的结果 int main(){ Tortoise tort; Hare hare; Judger judger; int winner, num, win_cnt_hare = 0, win_cnt_tort = 0, tie_cnt = 0; cout << " 需要模拟多少次比赛? "; cin >> num; for (int i = 0; i < num; ++i) { // 模拟 num 次比赛

judger.restart(); while (!judger.isFinish(winner)) // 模拟一场比赛

judger.update(hare.move(), tort.move());switch (winner) { // 记录比赛结果 case 0: ++win_cnt_hare; break; case 1: ++win_cnt_tort; break; default: ++tie_cnt;}

} cout << "乌龟赢 兔子赢 平局 \n"; cout << win_cnt_tort << '\t' << win_cnt_hare << '\t' << tie_cnt << endl; return 0;}

Page 324: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 324

问题问题

如果要统计若干个兔子的若干场比赛的结果该怎么办?

Page 325: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 325

小结 小结 面向对象程序设计的一个重要的目标是代

码重用。 代码重用的两种方法:组合和继承。

组合是将某一个已定义类的对象作为当前类的数据成员,则对此数据成员操作的代码得到了重用。

继承是在已有类(基类)的基础上加以扩展,形成一个新类,称为派生类。在派生类定义时,只需要实现扩展功能,而基类有的功能得到了重用。

Page 326: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 326

第第 1313 章 泛型机制—模板章 泛型机制—模板

类模板的定义 类模板的实例化 模板的编译 非类型形参和参数的默认值 类模板的友元 类模板作为基类

Page 327: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 327

类模板的定义类模板的定义

类模板允许用户为类定义一种模式,使得类中的某些数据成员、某些成员函数的参数或返回值能取任意数据类型。

定义格式:

template <class 标识符〉

class 类名 {//…};

Page 328: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 328

类模板实例类模板实例

定义一个泛型的、可指定下标范围的、安全的数组

Page 329: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 329

类模板定义类模板定义template <class T>class Array{ int low; int high; T *storage;public: Array(int lh = 0, int rh = 0): low(lh), high(rh) {storage = new T [high - low + 1]; } Array(const Array &arr);

Array &operator=(const Array & a);

T & operator[](int index);

~Array() {delete [] storage; }};

Page 330: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 330

类模板的成员函数的定义 类模板的成员函数的定义 类模板的成员函数都是函数模板,模板参数与类模板相同 形式:

Template< 模板形参 >

返回类型 类模板名 < 形式参数 >:: 成员函数名(函数的形参表)

{ 函数体 }

Array 类的成员函数的格式

template <class T>

返回类型 Array<T>:: 函数名(形式参数表)

{ 函数体 }

Page 331: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 331

ArrayArray 的成员函数的实现 的成员函数的实现 template <class T>

Array<T> &Array<T>::operator=(const Array<T> & a)

{ if (this == &a) return *this; //防止自己复制自己 delete [] storage; // 归还空间 low = a.low;

high = a.high;

storage = new T [high - low + 1];

for (int i=0; i <= high - low; ++i) storage[i] = a.storage[i];

return *this;

}

Page 332: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 332

template <class T>Array<T>::Array(const Array<T> &arr){ low = arr.low; high = arr.high; storage = new T [high - low + 1]; for (int i = 0; i < high -low + 1; ++i) storage[i] = arr.storage

[i];}

template <class T>T & Array<T>::operator[](int index){ if (index < low || index > high) {cout << " 下标越界 "; exit(-1);

} return storage[index - low];}

Page 333: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 333

第第 1313 章 泛型机制—模板章 泛型机制—模板

类模板的定义 类模板的实例化 模板的编译 非类型形参和参数的默认值 类模板的友元 类模板作为基类

Page 334: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 334

类模板的实例化 类模板的实例化 编译器从模板生成一个特定的类或函数的过程

称为模板的实例化。 类模板实例化后形成一个模板类。 类模板的实例化格式如下:

类模板名 < 模板的实际参数 > 对象名; 如:

Array<int> array1 ( 20 , 30 ) ;

Array<double> array2(10, 20);

Page 335: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 335

模板类的对象的使用模板类的对象的使用

我们可以用下列语句输入 array2 的值:for (i=10; i<=20; ++i) array2[i] = 0.1 * i;

也可以用下列语句输出 array1 的值:for (i=20; i<=30; ++i) cout << array1[i] << '\t';

Page 336: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 336

第第 1313 章 泛型机制—模板章 泛型机制—模板

类模板的定义 类模板的实例化 模板的编译 非类型形参和参数的默认值 类模板的友元 类模板作为基类

Page 337: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 337

模板的编译模板的编译 编译模板时,编译器进行三个阶段的检查

第一阶段是编译模板定义本身。这个阶段编译器只是检查一些诸如漏掉分号、变量名拼写错误之类的语法错误。

第二阶段是编译器看到模板使用时。对于函数模板,检查实际参数的数目和类型是否恰当。对于类模板可以检测出提供的模板的实际参数的数目是否正确。

第三阶段是实例化。编译器彻底编译模板。 所以,调试包含类模板的程序时,必须在定义了类

模板的对象并且对对象调用了所有的成员函数后,才能说明类模板的语法是正确的。

Page 338: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 338

第第 1313 章 泛型机制—模板章 泛型机制—模板

类模板的定义 类模板的实例化 模板的编译 非类型形参和参数的默认值 类模板的友元 类模板作为基类

Page 339: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 339

非类型形参非类型形参

模板的形式参数不一定都是类型,也可以是非类型的参数。

在模板实例化时,类型参数用一个系统内置类型的名字或一个用户已定义类的名字作为实际参数,而非类型参数将用一个值作为实际参数。非类型的模板实参的值必须是编译时的常量。

Page 340: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 340

非类型形参实例非类型形参实例

定义了一个安全的、可指定下标范围的、且下标范围必须是编译时的常量的类模板 Array 。(相当于 C++ 中的普通数组)

Page 341: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 341

设计考虑设计考虑 在定义类模板时,数组的大小是不知道的,但在编译时必须给出。

这可以通过非类型参数实现:该类模板有三个模板参数:数组元素的类型、数组下标的上下界。前者为类型参数,后者为非类型参数。

Page 342: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 342

类定义类定义template <class T, int low, int high>class Array{

T storage[high - low + 1];public: T & operator[](int index) ;};

template <class T, int low, int high>T & Array<T, low, high>::operator[](int index){ if (index < low || index > high) {cout << " 下标越界 "; exit(-1); } return storage[index - low];}

Page 343: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 343

类模板的使用类模板的使用

定义一个下标范围为 10 到 20 的整型数组,可用下列语句:

Array<int, 10, 20> array;

Page 344: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 344

参数的默认值参数的默认值 模板参数和普通的函数参数一样,也可

以指定默认值。如果前例中的类模板 Array经常被实例化为整型数组,则可在类模板定义时指定缺省值:template <class T = int> class Array

{ … }; 当要定义整型数组 array 时,就可以不

指定模板的实参:Array<> array;

Page 345: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 345

第第 1313 章 泛型机制—模板章 泛型机制—模板

类模板的定义 类模板的实例化 模板的编译 非类型形参和参数的默认值 类模板的友元 类模板作为基类

Page 346: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 346

类模板的友元类模板的友元

类模板可以声明两种友元:声明普通的类或全局函数为所定义的类模

板的友元

声明某个类模板或函数模板的实例是所定

义类模板的友元

Page 347: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 347

普通友元 普通友元 定义普通类或全局函数为所定义类模板的友元

的声明格式如下所示:template <class type>

class A {

friend class B;

friend void f();

};

该定义声明了类 B 和全局函数 f 是类模板 A的友元。 B 的所有的成员函数和全局函数 f 可以访问类模板 A 的所有实例的私有成员。

Page 348: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 348

特定实例的友元 特定实例的友元 定义一:

template <class T> friend class B; // 类模板声明template <class T> friend void f(const T &); //f 的声明template <class type> class A {friend class B <int>;friend void f (const int &);…};

将类模板 B 的一个实例,即模板参数为 int 时的那个实例作为类模板 A 的所有实例的友元。将函数模板 f 对应于模板参数为 int 的实例作为类模板A 所有实例的友元。

Page 349: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 349

特定实例的友元特定实例的友元 定义二

template <class T> friend class B;

template <class T> friend void f(const T &);

template <class type>

class A {

friend class B <type>;

friend void f (const type &);

};

这些友元定义说明了类模板 A 的特定实例与使用同一模板实参的类模板 B 和函数模板 f 的实例是友元。

Page 350: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 350

类模板的友元实例类模板的友元实例 为类模板 Array 增加一个输出运算符重载

函数,可以直接输出数组的所有元素。如有定义Array<int> array(10,20);

如程序中为该数组元素赋的值是 10 到 20 。即,将 10赋给 array[10] , 11赋给 array[11] ,…。可以直接用 cout << array; 输出这 11 个元素的值。

Page 351: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 351

实现考虑实现考虑

要直接输出数组的所有元素,可以为类模板 Array 重载“ <<”运算符。

由于 Array 是一个类模板,可用于不同类型的数组,因此,该输出运算符重载函数也应该是函数模板。

当 array 和 operator<<取相同实参时, ar

ray 的实例将 operator<< 的实例作为友元

Page 352: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 352

重载函数模板的实现重载函数模板的实现

template<class type>

ostream &operator<<(ostream &os,

const Array<type> &obj)

{ os << endl;

for (int i=0; i < obj.high - obj.low + 1; ++i)

os << obj.storage[i] << '\t';

return os;

}

Page 353: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 353

增加了输出重载的类模板增加了输出重载的类模板 ArrayArraytemplate <class T> class Array; // 类模板 Array 的声明template<class T> ostream &operator<<(ostream &os, const Array<T>&obj); // 输出重载声明template <class T>class Array{ friend ostream &operator<<(ostream &, const Array<T> &); private: int low; int high; T *storage; public: 。。。}

Page 354: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 354

类模板的友元实例类模板的友元实例

定义一个单链表,可以存放任意类型的数据。将单链表的所有操作都封装在单链表类中,使得单链表的用户只需要知道单链表可以做那些操作,而不用去管这些操作是如何实现的。

Page 355: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 355

实现考虑实现考虑 单链表中的每个元素存放在一个单独的节

点中 存储一个单链表就是存储一个指向单链表头节点的指针。

单链表的实现必须有两个类:节点类单链表类

由于我们要求的单链表是可以处理任何类型,因此这两个类都是模板。

Page 356: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 356

类模板类模板 nodenode 的定义 的定义 template <class elemType> class linkList;template <class T> ostream &operator<<(ostream &, const linkList<T> &);template <class elemType> class node ;template <class elemType>class node {

friend class linkList<elemType>;friend ostream &operator<<( ostream &, const linkList<elemType> &);

private: elemType data; node <elemType> *next; public:

node(const elemType &x, node <elemType> *N = NULL) { data = x; next = N;}

node( ):next(NULL) {}~node() {}

};

Page 357: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 357

linkListlinkList 的定义 的定义 template <class elemType>

class linkList

{ friend ostream &operator<<( ostream &,

const linkList<elemType> &);

protected:

node <elemType> *head;

void makeEmpty();

public:

linkList() { head = new node<elemType>; }

~linkList() {makeEmpty(); delete head;}

void create(const elemType &flag);

};

Page 358: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 358

类模板类模板 linkListlinkList 成员函数的实成员函数的实现 现

template <class elemType>

void linkList<elemType>::makeEmpty()

{

node <elemType> *p = head->next, *q;

head->next=NULL;

while (p != NULL) { q=p->next; delete p; p=q;}

}

Page 359: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 359

template <class elemType>void linkList<elemType>::create(const elemType &flag){ elemType tmp; node <elemType> *p, *q = head; cout << "请输入链表数据, " << flag << "表示结束 " << endl; while (true) {

cin >> tmp; if (tmp == flag) break;

p = new node<elemType>(tmp); q->next = p; q = p;

} q->next = NULL;}

Page 360: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 360

linkListlinkList 类模板中的输出运算符类模板中的输出运算符重载函数的实现重载函数的实现

template <class T>

ostream &operator<<(ostream &os,

const linkList<T> &obj)

{ node <T> *q = obj.head->next;

os << endl;

while (q != NULL){ os << q->data; q = q->next; }

return os;

}

Page 361: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 361

链表类的使用链表类的使用 定义:

linkList<int> intList;

该定义产生了类模板的一个整型实例 如果想创建这个单链表,可以调用 create 函

数:intList.create(0);

该调用将输入链表中的元素值,直到输入 0 为止。 要输出链表的所有元素,可以直接输出:

cout << intList;

Page 362: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 362

链表类的另一种实现链表类的另一种实现 由于结点类只有链表类要用,因此可以

将结点类作为链表类的内嵌类。 这样既可以保证结点类对于链表类的私

有性,访问也更加方便。

Page 363: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 363

第第 1313 章 泛型机制—模板章 泛型机制—模板

类模板的定义 类模板的实例化 模板的编译 非类型形参和参数的默认值 类模板的友元 类模板作为基类

Page 364: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 364

类模板作为基类 类模板作为基类 类模板可以作为继承关系中的基类。自

然,从该基类派生出来的派生类还是一个类模板,而且是一个功能更强的类模板。

类模板的继承和普通的继承方法基本类似。只是在涉及到基类时,都必须带上模板参数。

Page 365: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 365

类模板继承实例类模板继承实例 从 linkList 类模板派生一个栈的类模板 栈是一类特殊的线性表,它的插入和删除都只

能在表的一段进行。允许插入和删除的一端称为栈顶,另一端称为栈底。栈的常用操作有进栈( push )和出栈( pop )。

由于栈是一个特殊的线性表,因此可以将栈建立在表的基础上。在单链表中,我们可以将表头定义为栈顶,表尾定义为栈底。因此,栈就是在单链表的基础上增加两个操作: push 和pop 。

Page 366: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 366

栈的类模板的定义 栈的类模板的定义 template <class elemType>class stack : public linkList<elemType>{public: void push(const elemType &data) { node <elemType> *p = new node<elemType>(data); p->next = head->next; head->next = p; } bool pop(elemType &data) //栈为空时返回 false 。 { node <elemType> *p = head->next; if ( p == NULL) return false; head->next = p->next; data = p->data; delete p; return true; }};

Page 367: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 367

类模板类模板 stackstack 的使用的使用 定义一个整型栈: stack<int> st; 进栈: for (int i = 1; i < 10; ++i) st.push(i); 出栈: while (st.pop(i)) cout << i << '\t'; 执行结果: 9 8 7 6 5 4 3 2 1

Page 368: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 368

小结 小结 本章介绍了 C++ 中的泛型程序设计的工

具 ---- 模板。 模板是独立于类型的蓝图,编译器可以根据模板产生多种特定类型的实例。

模板分为函数模板和类模板。 本章中,我们学习了如何写一个类模板,

如何实例化类模板,使之成为一个模板类,如何定义及使用模板的友元。

Page 369: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 369

第第 1414 章 输入输出与文件章 输入输出与文件 输入输出是指程序与外部设备交换信息 C++ 把输入输出看成是一个数据流 输入流:外围设备流向内存的数据 输出流:内存流向外围设备的数据 在 C++ 中,输入输出不是语言所定义的部分,

而是由标准库提供。 C++ 的输入输出分为:

基于控制台的 I/O

基于文件的 I/O 基于字符串的 I/O

Page 370: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 370

输入输出与文件输入输出与文件

流与标准库 输入输出缓冲 基于控制台的 I/O

基于文件的 I/O

基于字符串的 I/O

Page 371: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 371

流的概念及用途流的概念及用途  I/O操作是以对数据类型敏感的方式执行的。 C++ 的 I/O

操作是以字节流的形式实现的。流实际上就是字节序列。 C++ 提供了低级和高级 I/O 功能。低级 I/O 功能通常只在

设备和内存之间传输一些字节。高级 I/O 功能把若干个字节组合成有意义的单位,如整数、浮点数、字符、字符串以及用户自定义类型的数据。

C++ 提供了无格式 I/O 和格式化 I/O两种操作。无格式 I/O

传输速度快,但使用起来较为麻烦。格式化 I/O 按不同的类型对数据进行处理,但需要增加额外的处理时间,不适于处理大容量的数据传输。

Page 372: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 372

流与标准库流与标准库头文件 类型

iostream istream 从流中读取ostream 写到流中去iostream 对流进行读写,从 istream 和 ostream 派生

fstream ifstream 从文件中读取,由 istream 派生而来ofstream 写到文件中去,由 ostream 派生而来fstream 对流进行读写,由 iostream 派生而来

sstream istringstream 从 string 对象中读取,由 istream 派生而来ostringstream 写到 string 对象中去,由 ostream 派生而来stringstream 对 string 对象进行读写,由 iostream 派生而来

Page 373: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 373

类的继承关系类的继承关系ios

istream ostream

ifstream ofstreamiostream

fstream

istringstream ostringstream

stringstream

Page 374: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 374

输入输出与文件输入输出与文件

流与标准库 输入输出缓冲 基于控制台的 I/O

基于文件的 I/O

基于字符串的 I/O

Page 375: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 375

输入输出缓冲输入输出缓冲 C++ 的输入输出是基于缓冲实现的 每个 I/O 对象管理一个缓冲区,用于存储程序读写的数据 当用户在键盘上输入数据时,键盘输入的数据是存储在输

入缓冲区中,当执行“ >>”操作时,从输入缓冲区中取数据存入变量,如缓冲区中无数据,则等待从外围设备取数据放入缓冲区

“<<” 是将数据放入输出缓冲区。如有下列语句: os << “please enter the value:”;

系统将字符串常量存储在与流 os 关联的缓冲区中

Page 376: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 376

输出缓冲区的刷新输出缓冲区的刷新 程序正常结束。作为 main 函数返回工作的一部分,将真正

输出缓冲区的内容,清空所有的输出缓冲区; 当缓冲区已满时,在写入下一个值之前,会刷新缓冲区; 用标准库的操纵符,如行结束符 endl ,显式地刷新缓冲区; 在每次输出操作执行结束后,用 unitbuf操纵符设置流的内部状态,从而清空缓冲区;

可将输出流与输入流关联起来。在这种情况下,在读输入流时,将刷新其关联的输出缓冲区。在标准库中,将 cout 和 c

in 关联在一起,因此每个输入操作都将刷新 cout 关联的缓冲区。

Page 377: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 377

输入输出与文件输入输出与文件

流与标准库 输入输出缓冲 基于控制台的 I/O

基于文件的 I/O

基于字符串的 I/O

Page 378: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 378

基于控制台的基于控制台的 I/OI/O 标准的输入输出流对象

cin 是类 istream 的对象,它与标准输入设备 (通常指键盘 )连在一起。

cout 是类 ostream 的对象,它与标准输出设备 (通常指显示设备 )连在一起。

cerr 是类 osteam 的对象,它与标准错误输出设备连在一起。

clog 是类 ostream 的对象,它与标准错误输出设备连在一起。

Page 379: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 379

基于控制台的基于控制台的 I/OI/O

输出流

输入流

格式化输入 / 输出

Page 380: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 380

输出流输出流 C++ 的类 ostream 提供了格式化输出和无格式输出的功能

输出功能包括用流插入运算符输出标准类型的数据;用成员函数 put 输出字符;成员函数 write 的无格式化输出;输出特定形式数值

Page 381: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 381

输出标准类型的数据输出标准类型的数据

标准类型的数据用流插入运算符 << 输出

格式:

cout << 数据项 ; C++ 能自动判别数据类型,并根据数据

类型解释内存单元的信息,把它转换成字符显示在显示器上。

Page 382: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 382

输出流输出流#include <iostream>using namespace std;int main(){int a = 5, *p = &a; double x = 1234.56; char ch = 'a'; cout << "a = " << a << endl; cout << "x = " << x << endl; cout << "ch = " << ch << endl; cout << "*p = " << *p << endl; cout << "p = " << p << endl; return 0; }

a = 5x = 1234.56ch = a*p = 5p = 0012FF7C

地址用十六进制输出

Page 383: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 383

指针输出的特例指针输出的特例 如果输出的指针变量是一个指向字符的

指针时, C++并不输出该指针中保存的地址,而是输出该指针指向的字符串。

如果确实想输出这个指向字符的指针变量中保存的地址值,可以用强制类型转换,将它转换成 void* 类型

Page 384: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 384

#include <iostream>

using namespace std;

int main()

{char *ptr = "abcdef";

cout << "ptr 指向的内容为: " << ptr << endl;

cout << "ptr 中保存的地址为: " << (void*)ptr << endl;

return 0;

} ptr指向的内容为: abcdefptr中保存的地址为: 0046C04C

Page 385: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 385

用成员函数用成员函数 putput 输出字符输出字符

cout.put(‘A’); 将字符 A显示在屏幕上,并返回当前对象。 连续调用 put函数: cout.put(‘A’).put(‘\n’); 该语句在输出字符 A后输出一个换行符。圆点运算符 (.)从左向右结合。cout.put(65);用 ASCII码值表达式调用 put函数,

语句也输出字符 A。

Page 386: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 386

writewrite 的无格式输出的无格式输出 调用成员函数 write 可实现无格式输出。它有两个参数。第一个参数是一个指向字符的指针,第二个参数是一个整型值。这个函数把一定量的字节从字符数组中输出。这些字节都是未经任何格式化的,仅仅是以原始数据形式输出。例如: char buffer[]  =“HAPPY BIRTHDAY”; cout.write(buffer, 10  ); 输出 buffer 的 10 个字节

函数调用: cout.write(“ABCDEFGHIJKLMNOPQRSTUVWXYZ”, 10); 显示了字母表中的前 10 个字母。

Page 387: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 387

基于控制台的基于控制台的 I/OI/O

输出流

输入流

格式化输入 / 输出

Page 388: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 388

输入流输入流

流读取运算符 >>

Get 函数 Getline 函数 其他函数

Page 389: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 389

流读取运算符流读取运算符 >>>> 输入流最常用的操作是流读取运算符。 流读取运算符通常会跳过输入流中的空格、 tab键、换行符等空白字符。

当遇到输入流中的文件结束符时,流读取运算符返回 0(fal

se);否则,流读取运算符返回对调用该运算符的对象的引用。

流读取运算符在读入 EOF 时返回 0 的特性使得它经常被用作为循环的判别条件,以避免选择特定的表示输入结束的值

EOF 在各个系统中有不同的表示。在 windows 中是 Ctri+

z

Page 390: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 390

实例实例

统计某次考试的最高成绩。假定事先不知道有多少个考试成绩,在输入结束时用户会输入表示成绩输入完毕的文件结束符。当用户输入文件结束符时, while 循环结构中的条件 (cin>

>grade) 将变为 0( 即 false) 。

Page 391: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 391

#include <iostream>

using namespace std;

int main()

{int grade, highestGrade = -1;

cout << "Enter grade (enter end-of-file to end): ";

while ( cin >> grade) {

if ( grade > highestGrade) highestGrade = grade;

cout << "Enter grade (enter end-of-file to end): ";

}

cout << "\n\nHighest grade is: "<< highestGrade << endl;

return 0;

}

Page 392: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 392

输出结果:Enter grade (enter end-of-file to end): 67

Enter grade (enter end-of-file to end): 87

Enter grade (enter end of file to end): 73

Enter grade (enter end-of-file to end): 95

Enter grade (enter end-of-file to end): 34

Enter grade (enter end-of-file to end): 99

Entergrade (enter end-of-file to end): ^ z

Heighest grade is: 99

Page 393: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 393

成员函数成员函数 getget

Get 函数用于读入字符或字符串 get 函数有三种格式:

不带参数带一个参数带三个参数

Page 394: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 394

不带参数的不带参数的 getget 函数函数

不带参数的 get 函数从当前对象读入一个字符,包括空白字符以及表示文件结束的 EOF ,并将读入值作为函数的返回值返回。如下列语句

while((ch = cin.get()) !=EOF) cout<< ch;

将输入的字符回显在显示器上,直到输入 EOF 。

Page 395: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 395

#include <iostream>

Using namespace std;

int main()

{ char c;

   while ( ( c = cin.get() ) != EOF )

     cout.put( c );

   cout << "\nEOF in this system is: "<< c;

   return 0;

}

Page 396: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 396

输出结果:

Enter a sentence followed by end-of-file:

Testing the get and put member functions^z

Testing the get and put member functions

EOF in this system is: -1

Page 397: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 397

带一个参数的 get 函数

例如,下面的循环语句将输入一个字符串,存入字符数组 ch ,直到输入回车。

cin.get(ch[0]);

for (i = 0; ch[i] != '\n'; ++i) cin.get(ch[i+1]);

ch[i] = '\0';

带一个字符类型的引用参数,它将输入流中的下一字符(包括空白字符)存储在参数中,它的返回值是当前对象的引用。

Page 398: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 398

带有三个参数的带有三个参数的 getget 成员函数成员函数 参数分别是接收字符的字符数组、字符数组的

大小和分隔符 (默认值为‘ \n’) 。 函数或者在读取比指定的最大字符数少一个字符后结束,或者在遇到分隔符时结束。

为使字符数组 ( 被程序用作缓冲区 ) 中的输入字符串能够结束,空字符会被插入到字符数组中。函数不把分隔符放到字符数组中,但是分隔符仍然会保留在输入流中。

Page 399: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 399

要输入一行字符,可用下列语句: cin.get(ch, 80, ’\n’); 或 cin.get(ch, 80); 要输入一个以句号结尾的句子,可用下面的语句: cin.get(ch, 80, ’.’); 当遇到输入结束符时,程序插入一个’ \0’ 作为输入字符串的结束标记,输入结束符没有放在字符数组中,而是保留在输入流中,下一个和输入相关的语句会读入这个输入结束符。如对应于语句

cin.get(ch, 80, ’.’); 用户输入 abcdef.↙ 则 ch 中保存的是字符串“ abcdef” ,而“ .”仍保留

在输入缓冲区中。如果继续调用 cin.get(ch1); 或 cin >> ch1 ; 则字符变量 ch1 中保存的是“ .” 。

Page 400: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 400

#include <iostream>Using namespace std;int main(){  const int SIZE = 80; char buffer1[ SIZE ], buffer2[ SIZE ] ;   cout << "Enter a sentence:\n";   cin >> buffer1;   cout << "\nThe string read with cin was:\n"        << buffer1 << "n\n";  cin.get( buffer2, SIZE );  cout << "The string read with cin.get was:\n"        << buffer2 << endl;  return 0; }

Page 401: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 401

输出结果:Enter a sentence:

Contrasting string input with cin and cin.get

The string read with cin was:

Contrasting

The string read with cin.get was:

string input with cin and cin.get              

Page 402: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 402

成员函数 getline

与带三个参数的 get函数类似,它读取一行信息到字符数组中,然后插入一个空字符。所不同的是, getline要去除输入流中的分隔符 ( 即读取字符并删除它 ),但是不把它存放在字符数组中。

Page 403: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 403

#include <iostream>

Using namespace std;

int main()

{ const SIZE = 80;

char buffe[ SIZE ];

  cout << "Enter a sentence:\n";

  cin.getline( buffer, SIZE );

  cout << "\nThe sentence entered is:\n" << buffer

<< endl;

  return 0;

}

Page 404: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 404

输出结果:Enter a sentence:

Using the getline member function

The sentence entered is:

Using the getline member function            

Page 405: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 405

用用 ReadRead 函数输入函数输入 调用成员函数 read 可实现无格式输入。它有两个参数。第一个参

数是一个指向字符的指针,第二个参数是一个整型值。这个函数把一定量的字节从输入缓冲区读入字符数组,不管这些字节包含的是什么内容。例如: char buffer[80] ; cin.read(buffer, 10  ); 读入 10 个字节,放入 buffer

如果还没有读到指定的字符数,遇到了 EOF ,则读操作结束。此时可以用成员函数 gcount 统计输入的字符个数

Page 406: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 406

#include <iostream>using namespace std;int main(){char buffer[ 80 ]; cout << "Enter a sentence:\n"; cin.read( buffer, 20 ); cout << "\nThe sentence entered was:\n"; cout.write( buffer, cin.gcount() ); cout << endl; cout << " 一共输入了 " << cin.gcount() << " 个字符 \n"; return 0;}

Page 407: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 407

输出结果:Enter a sentence:

Using the read, write, and gcount member functions

The sentence entered was:

Using the read,write

一共输入了 20 个字符

Page 408: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 408

基于控制台的基于控制台的 I/OI/O

输出流

输入流

格式化输入 / 输出

Page 409: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 409

格式化输入格式化输入 // 输出输出 C++ 提供了大量的用于执行格式化输入 / 输出的流操纵算子和成员函数。

功能 :

整数流的基数: dec 、 oct 、 hex 和 setbas

e

设置浮点数精度 :precision 、 setprecision

设置域宽 :setw 、 width

设置域填充字符 :fill 、 setfill

Page 410: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 410

设置整型数的基数设置整型数的基数 输入输出流中的整型数默认为十进制表示。为了使流中

的整型数不局限于十进制,可以插入 hex操纵符将基数设为十六进制,插入 oct操纵符将基数设为八进制,也可以插入 dec操纵符将基数重新设为十进制

也可以通过流操纵符 setbase来改变流的基数。该操纵符有一个整型参数,它的值可以是 16 , 10 或 8 ,表示将整型数的基数设为十六进制,十进制或八进制

使用任何带参数的流操纵符,都必须包含头文件 iomanip

流的基数值只有被显式更改时才会变化,否则一直沿用原有的基数。

Page 411: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 411

hexhex 、、 octoct 、、 decdec 和和 setbasesetbase #include <iostream>#include <iomanip>using namespace std;int main(){int n; cout << "Enter a octal number: "; cin >> oct >> n; cout << "octal " << oct << n << " in hexdecimal is:" << hex << n << '\n' ; cout << "hexdecimal " << n << " in decimal is:" << dec << n << '\n' ; cout << setbase(8) << "octal " << n <<" in octal is:" << n << endl; return 0; }

Enter a octal number: 30Octal 30 in hexdecimal is: 18Hexdecimal 18 in decimal is: 24Octal 30 in octal is: 30

Page 412: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 412

设置浮点数精度设置浮点数精度 设置浮点数的精度(即,实型数的有效位

数)可以用流操纵符 setprecision 或基类ios 的成员函数 precision来实现。

一旦调用了这两者之中的某一个,将影响所有输出的浮点数的精度,直到下一个设置精度的操作为止。

这个操纵符和成员函数都有一个参数,表示有效位数的长度。

Page 413: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 413

#include <iostream>#include <iomanip>using namespace std;int main(){double x = 123.456789, y = 9876.54321; for (int i = 9; i > 0; --i) {cout.precision(i); cout << x << '\t' << y << endl;}

// 或写成 for (int i = 9; i > 0; --i) // cout << setprecision(i) << x << '\t' << y << endl; return 0;}

Page 414: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 414

执行结果执行结果123.456789 9876.54321123.45679 9876.5432123.4568 9876.543123.457 9876.54123.46 9876.5123.5 9877123 9.88e+0031.2e+002 9.9e+0031e+002 1e+004

Page 415: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 415

设置域宽设置域宽 域宽是指数据所占的字符个数。 设置域宽可以用基类的成员函数 width ,也可以

用流操纵符( setw )。 width 和 setw 都包含一个整型的参数,表示域宽。

设置域宽可用于输入,也可用于输出。设置宽度是适合于下一次输入或输出,之后的操作的宽度将被设置为默认值。

当没有设置输出宽度时, C++ 按实际长度输出。如整型变量 a=123 , b=456 ,则输出

cout << a << b; 将输出 123456 。

Page 416: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 416

一旦设置了域宽,该输出必须占满域宽。如果输出值的宽度比域宽小,则插入填充字符填充。默认的填充字符是空格。如果实际宽度大于指定的域宽,则按实际宽度输出。如语句

cout << setw(5) << x << setw(5) << y << endl;

的输出为 123 456

每个数值占 5 个位置,前面用空格填充。

Page 417: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 417

设置域宽也可用于输入。当输入是字符串时,如果输入的字符个数大于设置的域宽时, C++ 只读入域宽指定的字符个数。如有定义

char a[9] , b[9] ;

执行语句 cin >> setw(5) >> a >> setw(5) >> b;

用户在键盘上的响应为 abcdefghijklm

则字符串 a 的值为“ abcd” ,字符串 b 的值为“ defg” 。

Page 418: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 418

其他流操纵符其他流操纵符流操纵符 描述skipws 跳过输入流中的空白字符,使用流操纵符 noskipws 复位该

选项left 输出左对齐,必要时在右边填充字符right 输出右对齐,必要时在左边填充字符showbase 指名在数字的前面输出基数,以 0 开头表示八进制, 0x 或

0X 表示十六进制。使用流操纵符 noshowbase 复位该选择

uppercase 指明当显示十六进制数时使用大写字母,并且在科学计数法输出时使用大写字母 E 。可以用流操纵符 nouppercase 复位

showpos 在正数前显示加号( + ),可以用流操纵符 noshowpos 复位

scientic 以科学计数法输出浮点数fixed 以定点小数形式输出浮点数setfill 设置填充字符,它有一个字符型的参数

Page 419: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 419

用户自定义的流操纵算子用户自定义的流操纵算子

程序员可以定义自己的流操纵符 例如,定义输出流操纵符格式如下: ostream &操纵符名( ostream &os ) { 需要执行的操作 }

Page 420: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 420

#include <iostream>

using namespace std;

ostream &tab(ostream &os) {return os << '\t';}

int main()

{int a=5,b=7;

cout << a << tab << b <<endl;

return 0;

} 5 7

Page 421: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 421

输入输出与文件输入输出与文件

流与标准库 输入输出缓冲 基于控制台的 I/O

基于文件的 I/O

基于字符串的 I/O

Page 422: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 422

基于文件的基于文件的 I/OI/O

文件的概念 文件和流 文件的顺序访问 文件的随机访问 访问有记录概念的文件

Page 423: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 423

文件的概念文件的概念 文件是驻留在外存储器上、具有标识名的一组信息集合,用来永久保存数据。

与文件相关的概念有: 数据项(字段) 记录 文件 数据库

如在一个图书管理系统中,有一个数据库。这个数据库由书目文件、读者文件及其它辅助文件组成。书目文件中保存的是图书馆中的所有书目信息,每本书的信息构成一条记录。每本书需要保存的信息有:书名、作者、出版年月、分类号、 ISBN号、图书馆的馆藏号以及一些流通信息。其中书名是一个字段,作者也是一个字段。

Page 424: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 424

基于文件的基于文件的 I/OI/O

文件的概念 文件和流 文件的顺序访问 文件的随机访问 访问有记录概念的文件

Page 425: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 425

文件和流文件和流 C++ 语言把每一个文件都看成一个有序的字节流

(把文件看成 n 个字节) 每一个文件以文件结束符 (end-of-file marker) 结束 当打开一个文件时,该文件就和某个流关联起来 与这些对象相关联的流提供程序与特定文件或设备之

间的通信通道 例如. cin 对象 ( 标准输入流对象 ) 使程序能从键盘输入数

据, cout 对象 ( 标准输出流对象 ) 使程序能向屏幕输出数据。

Page 426: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 426

文件访问过程文件访问过程

定义一个流对象 打开文件:将流对象与文件关联起来 访问文件 关闭文件 :切断流对象与文件的关联

Page 427: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 427

定义一个流对象定义一个流对象

C++ 有三个文件流类型: ifstream :输入文件流 ofstream :输出文件流 fstream :输入输出文件流

如: ifstream infile;

Page 428: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 428

打开文件打开文件

用流对象的成员函数 open打开文件 用流对象的构造函数打开文件 无论是成员函数 open 还是通过构造函

数,都有两个参数:打开的文件名文件打开模式

如果文件打开失败,返回 0

Page 429: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 429

文件打开模式文件打开模式文件打开模式名 含义

in 打开文件,做读操作

out 打开文件,做写操作

app 在每次写操作前,找到文件尾

ate 打开文件后,立即将文件定位在文件尾

trunc 打开文件时,清空文件

binary 以二进制模式进行输入输出操作

Page 430: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 430

默认打开方式默认打开方式

ifstream流对象是以 in 模式打开 ofstream流关联的文件则以 out 模式打开

ofstream 对象以 in 和 out 方式打开

Page 431: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 431

文件打开文件打开 打开输入文件:

ifstream infile;

infile.open(“file1”); 或 infile.open(“file1”, ifstream::in);

也可以利用构造函数直接打开:ifstream infile(“file1”); 或 ifstream infile(“file1” , ifstream::in);

打开输出文件ofstream outfile;

outfile.open(“file2”); 或 outfile.open(“file2”, ofstream::out);

也可以利用构造函数直接打开:ofstream outfile(“file2”); 或 ofstream outfile(“file2” , ofstream::ou

t);

打开输入输出文件fstream iofile(“file3”);

fstream iofile(“file3”, fstream:: in | fstream::out);

Page 432: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 432

文件关闭文件关闭

用成员函数 close

main 函数执行结束时,会关闭所有打开的文件

良好的程序设计习惯:文件访问结束时,关闭文件

Page 433: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 433

基于文件的基于文件的 I/OI/O

文件的概念 文件和流 文件的顺序访问 文件的随机访问 访问有记录概念的文件

Page 434: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 434

文件的顺序访问文件的顺序访问 C++文件的读写和控制台读写一样,可以用流提取运算符

“ >>” 从文件读数据,也可以用流插入运算符” <<” 将数据写入文件,也可以用文件流的其他成员函数读写文件,如 get 函数, put 函数等。

在读文件操作中,经常需要判断文件是否结束(文件中的数据是否被读完)。这可以通过基类 ios 的成员函数 eof

来实现。 eof 函数不需要参数,返回一个整型值。当读操作遇到文件结束时,该函数返回 1 ,否则返回 0 。另一种判断读结束的方法是用流提取操作的返回值。当“ >>”操作成功时,返回 true 。

Page 435: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 435

文件访问实例文件访问实例 将数字 1 到 10写入文件 file ,然后从 file

中读取这些数据,把它们显示在屏幕上。 首先用输出方式打开文件 file 。如文件 file

不存在,则自动创建一个,否则打开磁盘上的文件,并清空。用一个循环依次将 1 到10 用流插入符插入文件,并关闭文件。然后,再用输入方式打开文件 file ,读出所有数据,并输出到屏幕上。

Page 436: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 436

#include <iostream>#include <fstream>using namespace std;int main(){ ofstream out("file"); ifstream in; int i; if (!out) {cerr << "create file error\n"; return 1;} for (i = 1; i <= 10; ++i) out << i << ' '; out.close(); in.open("file"); if (!in) {cerr << "open file error\n"; return 1;} while (in >> i) cout << i << ' '; in.close(); return 0;}

Page 437: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 437

执行结果执行结果 执行该程序后,文件 file 中的内容为 1 2 3 4 5 6 7 8 9 10 该程序的输出结果是 1 2 3 4 5 6 7 8 9 10

Page 438: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 438

包含各种类型数据的文件操作 包含各种类型数据的文件操作 #include <fstream>

#include <iostream>

using namespace std;

int main()

{ofstream fout("test");

if (!fout){cerr <<"cannot open output file\n"; return 1;}

fout<<10<<" "<<123.456<< "\"This is a text file\"\n";

fout.close();

return 0;

}

Page 439: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 439

执行结果执行结果 文件中的内容为 10 123.456"This is a text file"

Page 440: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 440

读文件读文件#include <fstream>#include <iostream>using namespace std;int main(){ ifstream fin("test"); char s[80]; int i; float x;

if (!fin) {cout << "cannot open input file\n"; return 1;} fin >> i >> x >> s; cout << i << " " << x << s; fin.close(); return 0; }

10 123.456"This

fin >> i >> x; fin.getline(s, 80, ‘\n’);

Page 441: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 441

基于文件的基于文件的 I/OI/O

文件的概念 文件和流 文件的顺序访问 文件的随机访问 访问有记录概念的文件

Page 442: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 442

文件定位指针文件定位指针 文件定位指针:是一个 long 类型的数据 ,指出

当前读写的位置 C++文件有两个定位指针:读指针和写指针 当文件以输入方式打开时,读指针指向文件中的

第一个字节。 文件以输出方式打开时,写指针指向文件中的第

一个字节。 当文件以添加方式打开时,写指针指向文件尾。

Page 443: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 443

文件的随机访问文件的随机访问 指定文件定位指针的值,从任意指定位置开始读写

获取文件定位指针的当前位置 :成员函数 tellg 和 tellp

设置文件定位指针的位置:成员函数seekg 和 seekp

Page 444: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 444

成员函数成员函数 seekgseekg 和和 seekp seekp

seekg 和 seekp 都有两个参数:第一个参数通常为 long 类型的整数,表示偏移量;第二个参数指定移动的起始位置

寻找方向: ios::beg(默认 ) :相对于流的开头 ios::cur :相对于流当前位置 ios::end :相对于流结尾

Page 445: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 445

文件位置指针的例子文件位置指针的例子    // position to the nth byte of fileObject

    // assumes ios::beg    fileObject.seekg( n );

    // position n bytes forward in fileObject    fileObject.seekg( n, ios::cur );

    // position y bytes back from end of fileObject    fileObject.seekg( y, ios::end );

    // position at end of fileObject    fileObject.seekg( 0, ios::end );

Page 446: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 446

随机读写实例随机读写实例#include <iostream>#include <fstream>using namespace std;int main(){ fstream in("file"); int i; if (!in) {cerr << "open file error\n"; return 1;} in.seekp(10); in << 20; in.seekg(0); while (in >> i) cout << i << ' '; in.close(); return 0;}

执行后: 1 2 3 4 5 207 8 9 10

执行前: 1 2 3 4 5 6 7 8 9 10

Page 447: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 447

基于文件的基于文件的 I/OI/O

文件的概念 文件和流 文件的顺序访问 文件的随机访问 访问有记录概念的文件

Page 448: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 448

访问要求

立即访问到文件甚至大型文件中指定的记录

可以在不破坏其他数据的情况下把数据插入到随机访问文件中。

也能在不重写整个文件的情况下更新和删除以前存储的数据。

Page 449: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 449

实现考虑实现考虑 要求记录长度是固定的。 可以使用 istream 中的 read 函数和 ostream 中的 writ

e 函数能够做到。 如 number 是整型变量

写入: outFile.write(reinterpret_cast<const char * > (&number)), s

izeof(number));

读出: inFile.read(reinterpret_cast<char * > (&number)), sizeof(n

umber));

Page 450: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 450

实例:图书馆的书目管理系统实例:图书馆的书目管理系统 如果每本书需要记录的信息有:

馆藏号(整型数):要求自动生成 书名(最长 20 个字符的字符串) 借书标记。借书标记中记录的是借书者的借书证号,假

设也是整型数。 该系统需要实现的功能有:

初始化系统 添加书 借书 还书 显示书库信息

Page 451: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 451

文件设计文件设计

设计一个文件 book ,该文件中的每个记录保存一本书的信息。

文件中的记录可按馆藏号的次序存放,这样可方便实现添加书和借还书的功能。添加书时,只要将这本书对应的记录添加到文件尾。借还书时,可以根据馆藏号计算记录的存储位置,修改相应的记录。

Page 452: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 452

bookbook 类设计类设计 数据成员:

馆藏号、书名、借书标记 为了提供馆藏号自动生成,需要保存系统中最大

的馆藏号。这个值可以作为书目类的静态成员。 成员函数:

构造函数 借书 还书 显示书的详细信息 静态成员初始化 静态成员值加 1

Page 453: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 453

Book.hBook.h#ifndef _book_h#define _book_h#include <cstring>#include <iostream>#include <iomanip>#include <fstream>using namespace std;class book {

int no;char name[20];int borrowed;static int no_total;

public:

Page 454: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 454

book(const char *s = "") {no = no_total; borrowed = 0; strcpy(name,s);}

void borrow(int readerNo) //借书{ if (borrowed != 0) cerr << " 本书已被借,不能重复借 \n"; else borrowed = readerNo; }

void Return() { // 还书 if (borrowed == 0) cerr << " 本书没有被借,不能还 \n"; else borrowed = 0; }

void display() //显示书的信息{ cout << setw(10) << no << setw(20) << name << setw(10) << borrowed << endl;}

static void resetTotal() {no_total = 0;} //最大馆藏号复位

static void addTotal() {++no_total;} //馆藏号加 1};

int book::no_total = 0; // 静态数据成员的定义#endif

Page 455: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 455

系统分解系统分解

系统可分解成五大功能,每个功能用一个函数实现。

Main 函数显示菜单,根据用户的选择调用相应的函数

Page 456: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 456

MainMain 函数函数#include "book.h"

void initialize(); // 系统初始化void addBook(); //添加新书void borrowBook(); //借书void returnBook(); // 还书void displayBook(); //显示所有的书目信息int main()

{int selector;

Page 457: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 457

while (true) {cout << "0 -- 退出 \n";cout << "1 -- 初始化文件 \n";cout << "2 -- 添加书 \n";cout << "3 -- 借书 \n";cout << "4 -- 还书 \n";cout << "5 -- 显示所有书目信息 \n";cout << "请选择( 0-5 ): "; cin >> selector;if (selector == 0) break;switch (selector)

{case 1: initialize(); break; case 2: addBook(); break; case 3: borrowBook(); break; case 4: returnBook(); break; case 5: displayBook(); break; } }

return 0;}

Page 458: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 458

InitializeInitialize 的实现的实现

void initialize() {

ofstream outfile("book");

book::resetTotal();

outfile.close();

}

Page 459: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 459

addBookaddBook 的实现的实现void addBook() {

char ch[20]; book *bp; ofstream outfile("book",ofstream::app);

book::addTotal(); cout << "请输入书名: "; cin >> ch;

bp = new book(ch); outfile.write( reinterpret_cast<const char *>(bp), sizeof(*bp));

delete bp; outfile.close();

}

Page 460: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 460

borrowBookborrowBookvoid borrowBook() {int bookNo, readerNo; fstream iofile("book"); book bk; cout << "请输入书号和读者号: "; cin >> bookNo >> readerNo; iofile.seekg((bookNo - 1) * sizeof(book)); iofile.read( reinterpret_cast<char *> (&bk), sizeof(book) ); bk.borrow(readerNo); iofile.seekp((bookNo - 1) * sizeof(book)); iofile.write( reinterpret_cast<const char *>(&bk), sizeof(book)); iofile.close(); }

Page 461: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 461

returnBookreturnBookvoid returnBook() {int bookNo; fstream iofile("book"); book bk;

cout << "请输入书号: "; cin >> bookNo ; iofile.seekg((bookNo - 1) * sizeof(book)); iofile.read( reinterpret_cast<char *> (&bk), sizeof(book) ); bk.Return(); iofile.seekp((bookNo - 1) * sizeof(book)); iofile.write( reinterpret_cast<const char *>(&bk), sizeof(book)); iofile.close(); }

Page 462: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 462

displayBookdisplayBookvoid displayBook()

{ifstream infile("book");

book bk;

infile.read( reinterpret_cast<char *> (&bk), sizeof(book) );

while (!infile.eof()) {

bk.display();

infile.read( reinterpret_cast<char *> (&bk), sizeof(book) );

}

infile.close();

}

Page 463: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 463

输入输出与文件输入输出与文件

流与标准库 输入输出缓冲 基于控制台的 I/O

基于文件的 I/O

基于字符串的 I/O

Page 464: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 464

基于字符串的基于字符串的 I/OI/O iostream 标准库支持内存中的输入输出,只要

将流与存储在程序内存中的 string 对象捆绑起来即可

标准库定义了三种类型的字符串流: istringstream :由 istream派生而来,提供读

string 的功能。 ostringstream :由 ostream派生而来,提供写

string 的功能。 stringstream :由 iostream派生而来,提供读写

string 的功能。

Page 465: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 465

字符串流使用实例 字符串流使用实例 #include <iostream>#include <sstream>#include <string>using namespace std;int main(){ string ch; ostringstream os(ch); // 或 ostringstream os; for (int i = 0; i<=20; ++i) os << i << ' '; cout << os.str(); cout << endl; istringstream is(os.str()); while (is >> i) cout << i << '\t'; return 0;}

Page 466: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 466

小结 小结 输入输出是程序中不可缺少的一部分。在 C++ 中,

输入输出功能是以标准库的形式来提供。输入输出操作分为控制台 I/O ,文件 I/O 以及字符串 I/O 。由于文件 I/O 和字符串 I/O 类都是从控制台 I/O 类继承的,因此,这三种 I/O 的操作方式是相同的。

本章介绍了如何利用 iostream 库进行格式化的输入输出,介绍了如何利用文件永久保存信息,并以图书馆系统为例,介绍了实现文件的随机读写。

Page 467: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 467

第第 1515 章 异常处理章 异常处理

写函数库的程序员可以检测到库函数运行时的错误(如数组访问越界),但通常却不知道应该如何处理这些错误

异常处理的基本想法是,让一个函数在发现了自己无法处理的错误时抛出一个异常,希望它的(直接或间接)调用者能够处理这个问题。

Page 468: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 468

异常处理异常处理 传统错误处理方法 异常处理机制

抛出异常捕获异常处理异常

异常规格说明

Page 469: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 469

传统错误处理方法传统错误处理方法 可以处理的错误在发生错误的地方就地处理 在检查到一个在局部无法处理的问题时,一个函数可

以: 终止程序 abort() /exit() 返回一个表示“错误”的值。

int 返回一个合法值,让程序处于某种非法的状态。

errno 调用一个预先准备好在出现“错误”的情况下用的函数。

Page 470: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 470

处理错误的传统方法处理错误的传统方法 处理错误的传统方法:错误处理代码是在整个系统

代码中分布的。代码中可能出错的地方都要当场进行错误处理。在写程序时,必须知道所有的错误该如何处理 好处 : 程序员阅读代码时能够直接看到错误处理情况,确定是否实现了正确的错误检查。

问题 : 代码中受到错误处理的“污染”,使应用程序本身的代码更加晦涩难懂,难以看出代码功能是否正确实现。这样就使代码的理解和维护更加困难。

Page 471: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 471

int main(){ double a, b, c, x1, x2, dlt;

cout << "请输入 3 个参数: " << endl; cin >> a >> b >> c;

if (a == 0) cout << " 不是一元二次方程 " << endl; else { dlt = b * b - 4 * a * c; if (dlt >= 0) { x1 = (-b + sqrt(dlt)) / 2 / a; x2 = (-b - sqrt(dlt)) / 2 / a; cout << "x1=" << x1 << " x2=" << x2 << endl; } else cout << "无根 " << endl; }

return 0;}

解决问题的思路反而看不清了!

Page 472: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 472

面向对象中的异常处理面向对象中的异常处理

面向对象中,程序员经常做的是一些工具(类的设计与实现)

这些工具能够检测出错误,但往往不知道该如何处理错误。错误的处理是由工具的使用者决定

需要一种机制能将检测到的错误告诉使用者

Page 473: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 473

异常处理异常处理

传统错误处理方法 异常处理机制

抛出异常捕获异常处理异常

异常规格说明

Page 474: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 474

异常处理异常处理 C++ 的新异常处理特性:

异常处理将检测发现错误的代码与处理错误的代码分开来。程序员的工作也可做相应分工(例如,库函数程序员负责检测异常,而调用库函数的另一程序员则负责捕获与处理异常)。

使程序员可以删除程序执行“主线条”中的错误处理代码,从而提高程序的可读性和可维护性。

Page 475: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 475

异常处理机制异常处理机制

包括三个方面:异常抛出异常捕获异常处理

Page 476: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 476

异常处理异常处理

传统错误处理方法 异常处理机制

抛出异常捕获异常处理异常

异常规格说明

Page 477: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 477

抛出异常抛出异常 如果程序发生异常情况,而在当前的环境中获取不到异常

处理的足够信息,我们可以创建一包含出错信息的对象并将该对象抛出当前环境,发送给更大的环境中。这称为异常抛出。

例 1 throw myerror(“something bad happened”); myerror 是一个类,它以字符串变量为参数

例 2 throw int (5) int 是一个内部类型, 5 是一个 int 类型的常数

Page 478: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 478

throwthrow 与其操作数与其操作数 抛出异常的语句形式:

throw < 可选的操作数 >;

throw通常指定一个操作数(或不指定操作数的特殊情况)。 throw 的操作数可以是任何类型,如果操作数是个对象,则称为异常对象。也可以抛出条件表达式而不是抛出对象,可以抛出不用于错误处理的对象。

抛出异常的过程类似于 return 的过程,生成和初始化 thro

w操作数的一个临时副本,传回调用它的函数,退出函数,回收所有局部对象。

Page 479: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 479

异常抛出实例 – 异常类定义异常抛出实例 – 异常类定义 // Class DivideByZeroException to be used in exception

// handling for throwing an exception on a division by zero.

class DivideByZeroException {

public:

DivideByZeroException()

: message( "attempted to divide by zero" ) { }

const char *what() const { return message; }

private:

const char *message;

};

Page 480: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 480

异常抛出实例 – 异常抛出异常抛出实例 – 异常抛出

double Div(int x, int y )

{

if ( y == 0 )

throw DivideByZeroException();

return static_cast< double > ( x ) / y;

}

Page 481: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 481

异常处理异常处理 传统错误处理方法 异常处理机制

抛出异常捕获异常处理异常

异常规格说明

Page 482: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 482

异常捕获异常捕获

一个函数抛出异常,它必须假定该异常能被捕获和处理。异常捕获机制使得C++ 可以把问题集中在一处解决。

Page 483: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 483

启动异常捕获机制启动异常捕获机制 异常处理代码的一般形式:try{

可能抛出异常的代码}

catch( 类型 1 参数 1) { 处理该异常的代码 }

catch( 类型 2 参数 2) { 处理该异常的代码 }

… …

Page 484: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 484

catchcatch 捕获异常捕获异常 异常捕获和处理放在 catch块中 , 形式如下:

catch ( <捕获的异常类型 > < 可选参数 > ){

<异常处理器代码 >

}

catch 处理器定义自己的范围。 catch 在括号中指定要捕获的对象类型。 catch 处理器中的参数可以命名也可以无名。如果是命名参数,则可以在处理器中引用这个参数。如果是无名参数 ( 只指定匹配抛出对象类型的类型 ) ,则信息不从抛出点传递到处理器中,只是把控制从抛出点转到处理器中.许多异常都可以这样。

catch (…) 捕获任意类型的异常。

Page 485: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 485

异常捕获原理异常捕获原理

如果一个异常信号被抛出,异常处理器中第一个参数与异常抛出对象相匹配的函数将捕获该异常信号,然后进入相应的 catch 语句,执行异常处理程序。

Page 486: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 486

除零异常的捕获除零异常的捕获int main()

{ int number1, number2;

double result;

cout << "Enter two integers (end-of-file to end): ";

while ( cin >> number1 >> number2 ) {

try { result = Div( number1, number2 );

cout << "The quotient is: " << result << endl;

} catch ( DivideByZeroException ex ) {

cout << "Exception occurred: " << ex.what() << '\n‘; }

cout << "\nEnter two integers (end-of-file to end): “;

}

cout << endl;

return 0;

}

Page 487: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 487

输出结果输出结果Enter tow integers (end-of-file to end); l00 7

The quotient is: 14.2857

Enter tow integers (end-of-file to end); 100 0

Exception occurred: attempted to divide by zero

Enter tow integers (end-of-file to end); 33 9

The quotient is: 3.66667

Enter tow integers {end-of-file to end):

Page 488: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 488

异常抛出与检测实例二异常抛出与检测实例二

int Div(int x, int y)

{ if (y==0) throw y;

return x/y;

}

抛出的异常不一定是对象,可以是一个结果为内置类型的表达式

Page 489: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 489

int main()

{ try

{ cout << Div(6,3) << endl;

cout << Div(10,0) << endl;

cout << Div(5,2) << endl;

}

catch (int) {cout << "divide by zero" << endl; }

cout << "It’s Over" << endl;

return 0;

}

2

divide by zero

It’s Over

Page 490: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 490

解一元二次方程解一元二次方程#include <iostream.h>#include <cmath>

class noRoot {};class divByZero {};

double Sqrt(double x){ if (x < 0) throw noRoot();

return sqrt(x);}

double div(double x, double y){ if (y == 0) throw divByZero();

return x / y;}

Page 491: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 491

int main(){ double a, b, c, x1, x2, dlt;

cout << "请输入 3 个参数: " << endl; cin >> a >> b >> c;

try { dlt = Sqrt(b * b - 4 * a * c); x1 = div(-b + dlt, 2 * a); x2 = div(-b - dlt, 2 * a); cout << "x1=" << x1 << " x2=" << x2 << endl; } catch (noRoot) { cout << "无根 " << endl; }

catch (divByZero) {cout << " 不是一元二次方程 " << endl; }

return 0;}

Page 492: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 492

异常处理异常处理 传统错误处理方法 异常处理机制

抛出异常捕获异常处理异常

异常规格说明

Page 493: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 493

异常规格说明异常规格说明 传统函数声明: void f();

函数可以抛出任何异常。 void f() throw();

函数不会有异常抛出。 Void f() throw(toobig, toosmall, divzero);

函数会抛出 toobig, toosmall, divzero三种异常

Page 494: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 494

#include <iostream>using namespace std;class up{};class down{};void f(int i) throw(up, down);

int main(){ for (int i=1;i<=3;++i) try { f(i); } catch (up) {cout << "up catched" << endl; } catch (down) {cout << "down catched" << endl;} return 0;}

void f(int i) throw(up,down){ switch(i) {case 1: throw up(); case 2: throw down(); }}

up catcheddown catched

Page 495: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 495

小结小结

为了提高程序的鲁棒性,程序需要对各种可能的异常进行处理

某些错误需要异地处理 C++ 的异常机制由 try 、 throw 和 catch

构成

Page 496: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 496

第第 1616 章 容器和迭代器章 容器和迭代器 容器是特定类型的对象的集合,也就是为了保存一组对象而设计的类

容器一般提供插入、删除、查找以及访问容器中的所有对象等功能

用户不必关心容器中的对象是如何保存的。用户只需要使用容器提供的插入操作将对象放入容器,用删除操作将对象从容器中删除

数组是容器的一种实现,链表也是容器的一种实现

Page 497: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 497

迭代器迭代器 给出数组的下标可以访问数组的某一元素,给出一个指向

某一结点的指针可以访问链表中的一个元素。 访问一个容器中的对象必须有一个指向容器中某一个对象

位置的信息 容器中的对象位置是什么类型的信息?如果容器是用数组

实现,则是一个整型数。如果容器是用单链表实现,则是一个指向单链表结点的指针。

对象位置的类型与容器的实现方式有关。因此,通常为每种容器定义一个表示其中变量位置的类型,称为迭代器。

Page 498: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 498

迭代器迭代器

迭代器常常与容器一起使用。迭代器对象相当于是指向容器中对象的指针。

迭代器对象“穿行”于容器,容器中的某一元素执行某种操作。

可以将迭代器看成一种抽象的指针,迭代器进一步隐藏数据的存储方式。

Page 499: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 499

迭代器常用操作迭代器常用操作

迭代器对象赋值 迭代器对象的比较 让迭代器移到当前对象的下一对象 取迭代器指向的对象

Page 500: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 500

迭代器的优点迭代器的优点 在数组中,要访问所有结点可以用

For (i=1;i<=length;++i) Cout << array[i] <<endl;

在链表中,可以用For (node *p=first; p!=NULL; p=p->next) Cout << p->data << endl;

各种结构的抽象实现就是采用迭代器For (ListItr Itr(L); +Itr; ++Itr) Cout << Itr() <<endl;

与某一元素相关的操作可以让迭代器完成,容器类只完成对表整体的操作。迭代器一般作为相应类的友元类或内嵌类。

Page 501: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 501

迭代器实例 –顺序表中的迭代器迭代器实例 –顺序表中的迭代器 设计一个用数组实现的容器。该容器可以通过简单的 [] 运算符重载达到同时访问表元素的问题,但也可以用迭代器访问。

功能: 可以将数据依次放入容器。当数组不够大时,容器自动扩展数组。 删除最后放入的对象 在迭代器指出的位置插入一对象 删除迭代器指出的位置的对象 迭代器的常规操作:设置迭代器的初始位置,向后移一位置,判断迭代器指向的位置是否有对象,取迭代器指向的对象。

Page 502: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 502

设计考虑设计考虑 将迭代器类设置成容器类的内嵌类 容器的行为:

放入一对象 删除一对象 在指定位置放入一对象 删除指定位置的对象 取容器的首尾位置

迭代器的行为 设置迭代器的值 迭代器向后移动 比较两迭代器是否相同

Page 503: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 503

类的设计类的设计template <class T>class seqlist {private: int size; int current_size; T *storage; void doubleSpace();

public:seqlist(int s = 10):size(s)

{storage = new T[size]; current_size = 0;}~seqlist() {delete [] storage;}

Page 504: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 504

void push_back(const T &x) { if (size == current_size) doubleSpace(); storage[current_size++] = x; }

void pop_back( ) { if (current_size == 0) cerr << " 这是一个空容器 \n"; else --current_size; }

T &operator[](int i) { return storage[i]; }

Page 505: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 505

class Itr { T *pos;

public: Itr(T *obj = NULL) {pos = obj;} Itr &operator++() { ++pos; return *this;} T &operator*() { return *pos;} bool operator!=(const Itr &p)

{return pos != p.pos;}

friend class seqlist<T>;};

Page 506: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 506

Itr begin() {return Itr(storage);}

Itr end()

{ return Itr(storage + current_size); }

void insert(Itr &p, const T &x) ;

void erase(const Itr &p);

};

Page 507: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 507

doubleSpacedoubleSpacetemplate <class T>

void seqlist<T>::doubleSpace()

{ T *tmp = storage;

size *= 2;

storage = new T[size];

for (int i = 0; i < current_size; ++i)

storage[i] = tmp[i];

delete [] tmp;

}

Page 508: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 508

insertinserttemplate <class T>void seqlist<T>::insert( Itr &p, const T &x) { T *q;

if (size == current_size) { int offset = p.pos - storage; doubleSpace(); p.pos = storage + offset;

} q = storage + current_size;

while (q > p.pos) { *q = *(q-1); --q;} *p.pos = x; ++current_size;}

Page 509: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 509

eraseerase

template <class T>

void seqlist<T>::erase(const Itr &p)

{ T *q =p.pos ;

--current_size;

while (q < storage + current_size)

{ *q = *(q+1); ++q;}

}

Page 510: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 510

应用应用int main(){ seqlist<int> sq(10); seqlist<int>::Itr itr1; for (int i = 0; i < 10; ++i) sq.push_back(2 * i + 1);

cout << " 用下标运算符输出 :\n"; for (i = 0; i < 10; ++i) cout << sq[i] << '\t';

cout << " 用迭代器输出 :\n"; for (itr1 = sq.begin() ; itr1 != sq.end(); ++itr1) cout << *itr1 << '\t';

Page 511: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 511

输出输出

用下标运算符输出:1 3 5 7 9 11 13 15 17 19

用迭代器输出 :

1 3 5 7 9 11 13 15 17 19

Page 512: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 512

// 插入 0,2,4,6,8,10,12,14,16,18

for (itr1 = sq.begin(), i = 0 ; i < 20;

++itr1, ++itr1, i += 2)

sq.insert(itr1, i);

cout << "插入 0,2,4,6,8,10,12,14,16,18:\n";

for (itr1 = sq.begin() ; itr1 != sq.end(); ++itr1)

cout << *itr1 << '\t';

Page 513: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 513

输出输出

插入 0,2,4,6,8,10,12,14,16,18 :0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

Page 514: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 514

//删除 0 , 2 , 4 , 6 , 8 , 10 , 12 , 14 , 16 , 18

for (itr1 = sq.begin(); itr1 != sq.end(); ++itr1)

sq.erase(itr1);

cout << "删除 0 , 2 , 4 , 6 , 8 , 10 , 12 , 14 , 16 ,18:\n";

for (itr1 = sq.begin() ; itr1 != sq.end(); ++itr1)

cout << *itr1 << '\t';

return 0;

}

Page 515: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 515

输出输出

删除 0 , 2 , 4 , 6 , 8 , 10 , 12 , 14 ,16 , 18 :

1 3 5 7 9 11 13 15 17 19

Page 516: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 516

迭代器实例 –单链表中的迭代器迭代器实例 –单链表中的迭代器 设计一个用单链表实现的容器。可以用迭代器访问

功能: 在迭代器指出的位置后插入一对象,迭代器指向新插入对

删除迭代器指出的位置后的对象

迭代器的常规操作:设置迭代器的初始位置,向后移一位置,判断迭代器指向的位置是否有对象,取迭代器指向的对象。

Page 517: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 517

设计考虑设计考虑

链表中的每一元素存储在一个结点中,因此需要一个结点类。结点类是链表专用的,可设计成链表类的内嵌类。

与容器相关的还有一个迭代器。迭代器也是容器专用的,因此也可以设计成链表的内嵌类。

Page 518: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 518

类的设计类的设计template <class elemType>class linkList {

private:struct node {

elemType data; node *next;

node(const elemType &x, node *N = NULL) { data = x; next = N;}

node():next(NULL) {} ~node() {}

};

Page 519: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 519

private:

node *head;

void makeEmpty();

public:

linkList() { head = new node; }

~linkList()

{ makeEmpty(); delete head;}

Page 520: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 520

class Itr { private: node *current;

public: Itr(node *p) {current = p;}

bool operator()() const { return current != NULL; } bool isHead() const {return current == head;}

const elemType &operator*() const {return current->data;} void operator++() {current = current->next; }

friend class linkList<elemType>; };

Page 521: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 521

void insert(Itr &p, const elemType &x) { p.current->next = new node(x, p.current->next); p.current = p.current->next; }

void erase(Itr &p) { node *q = p.current->next; if (!q) return; p.current->next = q->next; delete q;}

Itr begin() {return Itr(head->next);}Itr GetHead() {return Itr(head);}};

Page 522: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 522

makeEmptymakeEmpty

template <class elemType>

void linkList<elemType>::makeEmpty()

{ node *p, *q;

p = head->next;

head->next = NULL;

while ( p != NULL) { q=p->next; delete p; p = q;}

}

Page 523: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 523

应用应用

int main()

{ linkList<int> lq;

linkList<int>::Itr itr1 = lq.GetHead();

for (int i = 0; i < 10; ++i) lq.insert(itr1, 2 * i + 1);

cout << " 用迭代器输出 :\n";

for (itr1 = lq.begin() ; itr1(); ++itr1)

cout << *itr1 << '\t';

输出与数组实现相同

Page 524: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 524

// 插入 0,2,4,6,8,10,12,14,16,18for (itr1 = lq.GetHead(), i = 0 ; i < 20; ++itr1, i += 2) lq.insert(itr

1, i);

cout << "插入 0,2,4,6,8,10,12,14,16,18:\n";for (itr1 = lq.begin() ; itr1(); ++itr1) cout << *itr1 << '\t';

//删除 0 , 2 , 4 , 6 , 8 , 10 , 12 , 14 , 16 , 18for (itr1 = lq.GetHead(); itr1(); ++itr1) lq.erase(itr1);

cout << "删除 0 , 2 , 4 , 6 , 8 , 10 , 12 , 14 , 16 , 18:\n";

for (itr1 = lq.begin() ; itr1(); ++itr1) cout << *itr1 << '\t';

return 0;}

Page 525: 程序设计

《程序设计》 cs.sjtu 2011-9-7

程序设计 - 525

总结总结

容器用于存放一组对象。用户不必关心容器是如何保存这组数据

迭代器相当于是一个抽象的指针,指向容器中的对象。用户可以通过迭代器访问容器中的对象。