unit 9 — i/o 流 类库 和 异 常处理

70
Unit 9 — I/O 流流流流流流流流 C++ 流流流流流流流流 I/O 流流流 “流”流流流流流 流流流流流流 流 一一 流流流 流流流流流流流流流流流流 / 流流流流流流 / 流流流流流流流9.1 C++ 流流流流流 9.4 流流流流流 / 流流 9.3 流流流流流流流 / 流流 *9.5 流流流流 *9.2 流流 / 流流流流流流流 流流流 流流流流流流 / 流流 9.6 流流流流流

Upload: kostya

Post on 08-Jan-2016

78 views

Category:

Documents


3 download

DESCRIPTION

Unit 9 — I/O 流 类库 和 异 常处理. 第九 章 流类库 与输入 / 输出. C++ 的 标 准 库中包 含了一个 I/O 流类 库, “流 ”是对 数 据从一个对象到另一个对象的传 送过程的抽象,数 据的输入 / 输 出通 过输入 / 输出流来实现的。. 9.1 C++ 的基本流类体系. 9.4 文件的输入 / 输出. * 9.5 字符串流. * 9.2 输入 / 输出的控制格式. 9.3 标准设备的输出 / 输出. 9.6 文件与对象. 9.1 C++ 的基本流类体系. basic_ios. - PowerPoint PPT Presentation

TRANSCRIPT

Page 1: Unit  9  — I/O 流 类库 和 异 常处理

Unit 9 — I/O 流类库和异常处理

  C++ 的标准库中包含了一个 I/O 流类库,“流”是对数据从一个对象到另一个对象的传送过程的抽象,数据的输入 / 输出通过输入 / 输出流来实现的。

9.1 C++ 的基本流类体系 9.4 文件的输入 / 输出

9.3 标准设备的输出 / 输出

*9.5 字符串流 *9.2 输入 / 输出的控制格式

第九章 流类库与输入 / 输出

9.6 文件与对象

Page 2: Unit  9  — I/O 流 类库 和 异 常处理

流类体系:

basic_ios basic_streambuf

basic_istream

basic_ostream

basic_ifstream

basic_iostream

basic_ofstream

basic_fstream

指针

9.1 C++ 的基本流类体系

流类模板的层次并非指的模板之间的继承关系,而旨在表明类模板实例的继承关系。

提供对流进行格式化输入输出和错误处理的操作。 提供完成提取(输入)操作

的成员函数。

提供完成插入(输出)操作的成员函数。

仅为前两者的聚合,未增加成员。

提供输入文件相关操作 提供输出文件相关操作

独立的类模板, basic_ios 包含一个保护型 basic_streambuf 指针成员(聚合), 用于管理一个流的缓冲区。

所有标准 I/O 流类模板在头文件 <iostream> 中说明,包含头文件<ios> 、 <streambuf> 、 <istream> 和<ostream> 。而输入输出文件流则在头文件<fstream> 中说明。

Page 3: Unit  9  — I/O 流 类库 和 异 常处理

输出 /输出标准流对象:9.1 C++ 的基本流类体系

1. C++ 流类库中定义了 4 个全局流对象: cin ,cout , cerr 和 clog ,用于完成人机交互的功能。 cin 标准输入流对象——键盘; cout 标准输出流对象——显示器; cerr 和 clog 标准错误输出流对象——显示器。2. cin 、 cout 和 clog 是带缓冲区的,缓冲区由streambuf 类对象来管理。而 cerr 为非缓冲区流,一旦错误发生立即显示。3. 要使用这四个功能,必须包含 <iostream> 头文件。注意 C++ 标准库 <fstream> 不包括<iostream> ,两者是独立的。

Page 4: Unit  9  — I/O 流 类库 和 异 常处理

对象提取运算符和插入运算符:9.1 C++ 的基本流类体系

1. 重载的提取运算符“ >>” 和插入运算符“ <<” ,执行字符序列的输入 / 输出操作。提取:指输入操作,从流中提取一个字符序列,如“ cin>>a;” 。插入:指输出操作,向流中插入一个字符序列,如“ cout<<a; ” 。2. cin 使用提取运算符; cout 、 cerr 和 clog 使用插入运算符。

文件的输入与输出:文件输入输出完成磁盘文件的读取和永久保存的功能。 Windows 下不同的 C++ 平台,都为文件功能作了扩充,在 VC++ 的 MFC 编程中采用了序列化( Serialization )。

Page 5: Unit  9  — I/O 流 类库 和 异 常处理

9.3 标准设备的输入 / 输出

*9.3.2 标准输入 / 输出成员函数

*9.3.1 提高标准输入 / 输出的健壮性

9.3.3 重载插入和提取运算符

Page 6: Unit  9  — I/O 流 类库 和 异 常处理

9.3.1 提高标准输入 / 输出的健壮性 标准设备输入使用要点:1. cin 为缓冲流。键盘输入的数据首先保存在缓冲区中,

当要提取时,系统将是从缓冲区中拿。如果键盘一次输入数据多于实际要提取的,则多余的数据会留在缓冲区,等着慢慢用;如果输入错了,必须在回车之前修改,一旦回车则数据传到缓冲区中。只有把输入缓冲区中的数据取完后,才要求输入新的数据。不可能用刷新来清除缓冲区,所以不能输错,也不能多输!

2. 输入的数据类型必须与要提取的数据类型一致,否则出错。出错只是在流的状态字 state (枚举类型io_state )中对应位置位(置 1 ),程序继续。所以要提高健壮性,就必须在编程中加入对状态字 state 的判断。

Page 7: Unit  9  — I/O 流 类库 和 异 常处理

9.3.1 提高标准输入 / 输出的健壮性 标准设备输入使用要点:3. 空格和回车都可以作为数据之间的分格符,所以多个数

据可以在一行输入,也可以分行输入。但如果是字符型和字符串,则空格( ASCII 码为 32 )无法用 cin 输入,字符串中也不能有空格。回车符也无法读入。

4. 输入数据以后再输入字符或字符串:如果数后直接加回车,应该用 cin.get() 提取回车。如果还有空格,则要清空缓冲区。

Page 8: Unit  9  — I/O 流 类库 和 异 常处理

9.3.3 重载插入和提取运算符 重载插入和提取运算符:在用户定义类中,将重载的运算符说明为其友元函数:friend istream& operator>>(istream&,className&);friend ostream& operator<<(ostream&,className&);

说明:1. 函数的返回值是对输入或输出流的引用,保证在 cin

和 cout 中可以连续使用“ >>” 或“ <<” 运算符。2. 第 1 个参数是输入或输出流的引用,作为“ >>” 或

“ <<” 的左操作数;第 2 个参数为用户定义类的引用,作为右操作数。

3. 流用作函数参数,必须是引用调用,不能是传值调用。因为要处理的是流本身,而不是其副本。【作业 H8_2】重载插入运算符“ <<”。

【例 9.6】用户定义的复数类 Complex的输入与输出。

Page 9: Unit  9  — I/O 流 类库 和 异 常处理

9.3.3 重载插入和提取运算符 重载【作业 H8_2】的插入运算符“ <<” :重载插入运算符“ <<”声明为 Scholar 类的友元函数:friend ostream & operator<<(ostream & ,const mystring &);

// 流类作为形式参数必须是引用重载插入运算符“ <<” 定义:ostream & operator<<(ostream & o,const Scholar & s){

return o<<s.Name<<'\t'; }

Orderedlist 类的打印函数简化为:template <typename T,int size> void Orderedlist<T,size>::print(){ int i; for(i=0;i<=last;i++){

cout<<pslst[i]; // 取代“ pslst[i].show();”P320:slist[i].key.show()

if (i%5==4) cout<<endl;} cout<<endl;}

输出格式统一,无论对于基本数据类型,还是用户定义类型均通用。

Page 10: Unit  9  — I/O 流 类库 和 异 常处理

9.3.3 重载插入和提取运算符 【例 9.6】用户定义复数类 Complex 的输入 / 输出:#include<iostream>using namespace std;class Complex{ double Real, Image;public: Complex ( double r=0.0, double i=0.0):Real(r),Image(i) {} // 这里省略若干成员函数 , 详见【例 4.7】 friend ostream&operator<<(ostream&s, const Complex&z); friend istream &operator>>(istream&s, Complex&a); // 流类作为形式参数必须是引用};

ostream& operator<<(ostream&s, const Complex &z){

return s<<'('<<z.Real<<','<<z.Image<<')';

}

Page 11: Unit  9  — I/O 流 类库 和 异 常处理

9.3.3 重载插入和提取运算符 istream&operator>>(istream&s,Complex &a){ // 格式为 r,(r),(r,z) double re=0,im=0; char c=0; s>>c; if(c=='(') {// 是否由括号开始 s>>re>>c; // 实部 if(c==',') s>>im>>c; //虚部 if(c!=')') s.clear(ios::failbit); //漏了括号给一个操作失败标志 } else {// 实数 s.putback(c); // 无括号,返回一个字符到输入缓冲区 s>>re; } if(s) a=Complex(re,im); // 当流 s正常时,则复制 return s;}

putback()声明:stream&istream::putback(char);它将最后一次从输入流中取得的字符放回到输入流中。

Page 12: Unit  9  — I/O 流 类库 和 异 常处理

int main(){Complex a,b,c;cout<<" 输入一个实数 "<<endl;cin>>a;cout<<" 输入一个用括号括起来的实

数 "<<endl;cin>>b;cout<<" 输入一个用括号括起来复

数 "<<endl;cin>>c;cout<<"a="<<a<<'\t'<<"b="<<b<<'\

t‘<< "c="<<c<<'\n';return 0;

}

9.3.3 重载插入和提取运算符

Page 13: Unit  9  — I/O 流 类库 和 异 常处理

9.4 文件的输入与输出

文件指的是磁盘文件,分为两类:二进制文件和文本文 件 。 文 本 文 件 也称 ASCII 码 文 件 , 以 字 符( character )为文件存取的最小信息单位;而二进制文件中存取的最小信息单位为字节( Byte )。

C++ 把每一个文件都看成一个有序的字节流,每一个文件以文件结束符( end of file marker )结束。

0 1 2 43 65 7 8 … n-1

… 文件结束符

图 9.2 C++ 把文件看作有序字节的流

文件的基本概念:

Page 14: Unit  9  — I/O 流 类库 和 异 常处理

9.4 文件的输入与输出 当打开一个文件时,该文件就和某个流关联起来了。对文件进行读写实际上受到一个文件定位指针( file position pointer )的控制。 输入流指针也称读指针,每一次提取操作将从读指针当前所指位置开始,每次提取操作自动将读指针向文件尾移动。 输出流指针也称写指针,每一次插入操作将从写指针当前位置开始,每次插入操作自动将写指针向文件尾移动。9.4.1 文件的打开与关闭

9.4.2 文本文件的读写

9.4.3 二进制文件的读写

*9.4.4 文件的随机访问

Page 15: Unit  9  — I/O 流 类库 和 异 常处理

9.4.1  文件的打开与关闭 文件使用基本步骤:1. 建立一个文件流对象;2. 打开一个磁盘文件;3. 对文件进行读写操作;4. 关闭文件。

具体步骤,如下:1 .建立一个文件流对象,这对象又称为内部文件:ifstream ifile ; // 仅输入用ofstream ofile ; // 仅输出用fstream iofile ; //既输入又输出用

Page 16: Unit  9  — I/O 流 类库 和 异 常处理

9.4.1  文件的打开与关闭 2 .使用文件流对象的成员函数打开一个磁盘文件。这样文件流对象和磁盘文件之间就建立了联系。文件流中说明了 3个打开文件的成员函数。void ifstream::open(const char*,int =ios::in, int=filebuf::openprot);void ofstream::open(const char *,int=ios::out,int=filebuf::openprot);void fstream::open(const char*,int=ios::in|ios::out, int=filebuf::openprot);

第 1 参数为要打开的磁盘文件名。第 2 参数为打开方式,有输入( in ),输出( out )等,打开方式在 ios 基类中定义为枚举类型。第 3 参数为指定打开文件的保护方式,一般取默认。

本步骤的实例:iofile.open(“myfile.txt”, ios::in|ios::out);

Page 17: Unit  9  — I/O 流 类库 和 异 常处理

9.4.1  文件的打开与关闭

ifstream 、 ofstream 、 fstream 这 3 个文件流类都重载了一个带默认参数的构造函数,具有 open 函数的功能:ifstream::ifstream(const char*,int=ios::in,int=filebuf::openprot);ofstream::ofstream(const char*,int=ios::out, int=filebuf::openprot);fstream::fstream(const char*,int=ios::in|ios::out,int=filebuf::openprot);

这样 1 , 2 两步也可以合成为一步:fstream iofile(“myfile.txt”,ios::in|ios::out);建议采用!

Page 18: Unit  9  — I/O 流 类库 和 异 常处理

9.4.1  文件的打开与关闭

 接着判断文件打开是否成功,若成功,则文件流对象值非零,不成功则为 0 ( NULL )。文件流对象值物理上就是指它的地址。打开一个文件的完整程序实例:fstream iofile(“myfile.txt”,ios::in|ios::out);if(!iofile) //“ !”为重载的运算符,见 9.3.1节 { cout<<“ 不 能 打 开 文件 :”<<“myfile,txt”<<endl; return -1; //失败,退回操作系统 }

Page 19: Unit  9  — I/O 流 类库 和 异 常处理

9.4.1  文件的打开与关闭

3.使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写,这在下一节中讨论。4.关闭文件。 3 个文件流类各有一个关闭文件的成员函数 :void ifstream::close();void ofstream::close();void fstream::close();

使用很方便,如:iofile.close();

Page 20: Unit  9  — I/O 流 类库 和 异 常处理

9.4.1  文件的打开与关闭   关闭文件的过程:1. 系统把该文件相关联的文件缓冲区中的数据写到文

件中,保证文件的完整,收回与该文件相关的内存空间,供再分配;

2. 然后把磁盘文件名与文件流对象之间的关联断开,以防止对磁盘文件的误操作。再次使用文件时必须重新打开。

注意事项:3. 关闭文件并没有取消文件流对象,该流对象仍可与

其他磁盘文件建立联系。4. 文件流对象在程序结束时,或它的生命期结束时,

由析构函数撤消,同时释放内部分配的预留缓冲区。

Page 21: Unit  9  — I/O 流 类库 和 异 常处理

9.4.2  文本文件的读写

文本文件的顺序读写: 顺序读写可用 C++ 的提取运算符( >> )和插入运算符( << )进行。

【例9.7】复制文件。【例9.8】按行复制文本文件。【例9.9】文本式数据文件的创建与读取数据。

Page 22: Unit  9  — I/O 流 类库 和 异 常处理

9.4.2  文本文件的读写 【例 9.7】复制文件,逐字符复制 int main() { char ch; ifstream sfile("d:\\Ex9_6\\Ex9_6.cpp"); ofstream dfile("e:\\Ex9_6.cpp"); // 只创建文件,不建立子目录 if(!sfile) { cout<<" 不能打开源文件 :"<<"d:\\Ex9_6\\Ex9_6.cpp"<<endl; return -1;} if(!dfile){ cout<<" 不能打开目标文件 :"<<"e:\\Ex9_6.cpp"<<endl; return -1;} sfile.unsetf(ios::skipws); // 关键 ! 把跳过空格控制位复位为 0, 即不 //跳过空格 , 否则空格全部未拷 while (sfile>>ch) dfile<<ch; sfile.close(); // 如没有这两个关闭函数 ,析构函数也可关闭 dfile.close(); return 0; }

Page 23: Unit  9  — I/O 流 类库 和 异 常处理

9.4.2  文本文件的读写 注意事项:1. 必须关闭“跳过空白”字 ios::skipws ,因为提取(“ >>” )符在默认情况下是跳过空白(包括空格,制表符,回车等)的,这样复制的文件会缺少一些字符。2. while( sfile>>ch ) 语句能确定文件是否复制结束。流类成员函数和运算符全是返回本类型的引用,这里就是流文件对象自身,当文件结束时,返回 NULL ,这时不再复制,退出循环。3. 复制是按字节进行的,效率很低,按字节传递开销极大,但该程序能正确复制任意类型的文件,不仅对文本文件(看作按字符),二进制文件(看作按字节)也一样。如果是文本文件,可以按行进行复制。4. !sfile 中的!是重载的运算符,在状态函数中重载,当该操作出现不正常状态,返回 true 。

Page 24: Unit  9  — I/O 流 类库 和 异 常处理

【例 9.8】按行复制文本文件int main(){ char filename[256],buf[100]; fstream sfile,dfile; cout<<" 输入源文件路径名 :"<<endl; cin>>filename; sfile.open(filename,ios::in);//打开一个已存在的文件 while(!sfile) {

cout<<"源文件找不到 ,请重新输入路径名 :"<<endl;sfile.clear(0); // 流不正常,清状态字cin>>filename;sfile.open(filename,ios::in);

} cout<<" 输入目标文件路径名 :"<<endl; cin>>filename; dfile.open(filename,ios::out); // 只能创建文件,不能建立子目录 if (!dfile) { cout<<"目标文件创建失败 "<<endl; return -1; }

9.4.2  文本文件的读写

Page 25: Unit  9  — I/O 流 类库 和 异 常处理

9.4.2  文本文件的读写while (sfile.eof()!=1) { sfile.getline(buf,100); //按行复制, A if (sfile.gcount()<100) dfile<<buf<<‘\n’; //因回车符未送到, B else {dfile<<buf; //=99 个字符 , 还未读到回车符 ,故不加 '\n‘

sfile.clear(0); }// 状态字被置为 0x02 ,必须清0 。} sfile.close(); dfile.close(); return 0;}A 行中 sfile.getline(buf,100) 从源文件读一行字符,或读 99个字符,效率大大提高。 B 行中,因 getline() 从源文件读到行结束(回车换行)符而停止,但回车换行符(’ \n’ )并不放在 buf 中,因此要加一个回车换行符。此程序只能用于文本文件。

课本:while(sfile.getline(buf,100),sfile.eof()!=1) 有误,少执行 1 次循环!课本: sfile.rdstate==0 ,有误,即便未读到回车换行符, rdstate也可能不为零,使得读取中断!

Page 26: Unit  9  — I/O 流 类库 和 异 常处理

【例 9.9】文本文件的创建与读取数据典型的 C++ 数据存入文件和由文件获得数据的方法是把对象存入文件和由文件重构对象。本例重载了提取符“ >>” 完成对象重构,重载了插入运算符“ <<” 完成对象存入文件。 class inventory{ string Description; string No; int Quantity; double Cost; double Retail;public: inventory(string="#",string="0",int=0,double=0,double=0); friend ostream& operator<<(ostream&dist,inventory&iv); friend istream& operator>>(istream&sour,inventory&iv); }; // 流类作为形式参数必须是引用

9.4.2  文本文件的读写

class inventory;ostream & operator<<(ostream &, inventory &);

对于 VC++6.0 编译器,这种声明必不可少 ! 否则编译器报错友元运算符定义中inventory 的私有数据不可访问。

Page 27: Unit  9  — I/O 流 类库 和 异 常处理

inventory::inventory(string des,string no,int quan, double cost,double ret){ Description=des; No=no; Quantity=quan; Cost=cost; Retail=ret;}

ostream &operator<<(ostream&dist,inventory&iv){ dist<<left<<setw(20)<<iv.Description<<setw(10)<<iv.No; dist<<right<<setw(10)<<iv.Quantity<<setw(10)<<iv.Cost <<setw(10)<<iv.Retail<<endl; return dist; } //写入文件是自动把数转为数字串后写入istream&operator>>(istream&sour,inventory&iv){ sour>>iv.Description>>iv.No>>iv.Quantity >>iv.Cost>>iv.Retail; return sour; }// 从文件读出是自动把数字串转为数读出

9.4.2  文本文件的读写

资源获取是由构造函数实现,而资源释放是由析构函数完成。同样可以把由文件重构对象放在构造函数中,把对象存入文件则放在析构函数中。将在 9.6 节介绍。

Page 28: Unit  9  — I/O 流 类库 和 异 常处理

9.4.2  文本文件的读写int main(){ inventory car1("夏利2000","805637928",156,80000,105000),car2; inventory motor1("金城25","93612575",302,10000,13000),motor2; ofstream distfile("d:\\Ex9_9.data"); distfile<<car1<<motor1; distfile.close(); cout<<car1; cout<<motor1; cout<<car2; cout<<motor2; ifstream sourfile("d:\\Ex9_9.data"); // 分两次打开 , 可避免读文件时 , 误改了源文件 sourfile>>car2>>motor2; sourfile.close(); cout<<car2; cout<<motor2; return 0; }

Page 29: Unit  9  — I/O 流 类库 和 异 常处理

9.4.3  二进制文件的读写 对二进制文件进行读写的成员函数:istream&istream::read(char *,int); // 从二进制流提取istream&istream::read(unsigned char*,int);istream&istream::read(signed char *,int);// 第一个参数指定存放有效输入的变量地址;// 第二个参数指定提取的字节数;// 函数从输入流提取指定数量的字节送到指定存储单元ostream&ostream::write(const char *,int);// 向二进制流插入ostream&ostream::write(const unsigned char *,int);ostream&ostream::write(const signed char *,int);// 第一个参数指定输出对象的内存地址 ,// 第二个参数指定插入的字节数 ,// 函数从指定地址开始的指定数量的字节插入输出流

Page 30: Unit  9  — I/O 流 类库 和 异 常处理

9.4.3  二进制文件的读写 【例 9.10】创建二进制数据文件及文件的读取用 Bdatatofile() 和 Bdatafromfile() 取代了文本文件的插入符 << 和提取符 >> ,实现二进制数据的存与取。class inventory{ string Description, No; int Quantity; double Cost; double Retail;public: inventory(string=“#”,string=“#”,int=0,double=0,double=0); friend ostream& operator<<(ostream&dist,inventory&iv); void Bdatatofile(ofstream&dist); // 文件流作形参须引用 void Bdatafromfile(ifstream&sour); };

class inventory;ostream & operator<<(ostream &, inventory &);

对于 VC++6.0 编译器,这种声明必不可少 ! 否则编译器报错友元运算符定义中inventory 的私有数据不可访问。

Page 31: Unit  9  — I/O 流 类库 和 异 常处理

9.4.3  二进制文件的读写void inventory::Bdatafromfile(ifstream&sour){ char k[20]; sour.read(k,20);// 提取 20 个字符到 k 中 Description=k; sour.read(k,10); // 提取 10 个字符到 k 中 No=k; sour.read((char*)&Quantity,sizeof(int));// 取地址强制转换 sour.read((char *)&Cost,sizeof(double)); sour.read((char *)&Retail,sizeof(double));} //参见写过程,读写完全对称 , 次序决不能错

Page 32: Unit  9  — I/O 流 类库 和 异 常处理

9.4.3  二进制文件的读写void inventory::Bdatatofile(ofstream&dist){

dist.write(Description.c_str(),20); dist.write(No.c_str(),10);dist.write((char*)&Quantity,sizeof(int));dist.write((char*)&Cost,sizeof(double));dist.write((char*)&Retail,sizeof(double));}

int main(){ inventory car1("夏利2000","805637928",156,80000,105000),car2; inventory motor1("金城15","93612575",302,10000,13000),motor2; ofstream ddatafile("d:\\Ex9_10.data",ios::out|ios::binary); car1.Bdatatofile(ddatafile); motor1.Bdatatofile(ddatafile); cout<<" 对象 car1:"<<endl; cout<<car1; cout<<" 对象 motor1:"<<endl; cout<<motor1; cout<<" 对象 car2:"<<endl; cout<<car2; cout<<" 对象 motor2:"<<endl; cout<<motor2;

c_str() 为 string 类的成员函数,实现将 string 类字符串转换成char*

Page 33: Unit  9  — I/O 流 类库 和 异 常处理

9.4.3  二进制文件的读写 ddatafile.close(); ifstream sdatafile("d:\\Ex9_10.data",ios::in|ios::binary); // 重新打开文件 , 从头读取数据 car2.Bdatafromfile(sdatafile); // 从文件读取数据复制到 car2 if(sdatafile.eof()==0) cout<<" 读文件成功 "<<endl; cout<<" 对象 car2:"<<endl; cout<<car2; motor2.Bdatafromfile(sdatafile); // 继续从文件读取数据复制到对象 motor2 if(sdatafile.eof()==0) cout<<" 读文件成功 "<<endl; cout<<" 对象 motor2:"<<endl; cout<<motor2; sdatafile.close(); return 0;}

Page 34: Unit  9  — I/O 流 类库 和 异 常处理

9.4.3  二进制文件的读写

二进制文件特点:1. 可以控制字节长度,读写数据时不会出现二义性,可靠性高。同时不知数据格式无法解析数据,保密性好。2. 读函数并不知道文件是否结束,需用 ios::eof() 自行判断文件是否结束,若为 1 则文件结束,此后系统不会再读。3. 如果写完数据后没有关闭文件,直接开始读,则必须把文件定位指针移到文件头。如关闭文件后重新打开,文件定位指针就在文件头。

Page 35: Unit  9  — I/O 流 类库 和 异 常处理

9.6 文件与对象

具体实施:创建对象:在构造函数中打开文件,并读取数据来初始化对象的成员数据; 撤销对象:在析构函数中打开文件,将数据保存到文件中。当撤销对象时,文件流相关资源被自动释放。

对象与文件的关系: 在面向对象的程序设计中,对象的数据成员是数据的临时载体,而对象的操作是数据处理的手段,这些数据只有导入文件才能永久保存。要想再次使用这些数据,必须由这一文件重新创建对象,由对象的操作完成数据处理。最终要把这些处理过的数据重新保存到文件中。

Page 36: Unit  9  — I/O 流 类库 和 异 常处理

【例 9.13 】定义数组类 Array ,可存放若干货物Inventory 对象。 Array 类的对象数组动态建立,初始为 2 个元素,不够用时扩充一倍。用文本数据文件建立数组元素对象,要求放在构造函数中,而数据的保存放在析构函数中。首次程序运行时,建立空的数据文件,由键盘输入建立的数组元素对象,并写入文件,程序退出时,关闭文件;下一次运行由该文件构造对象,恢复前一次做过的工作。

介绍一个标准的面向对象的程序设计框架,也是对前面各章内容的总结。(注意:本例使用了多重的插入运算符重载)

9.6 文件与对象—例 9.13

Page 37: Unit  9  — I/O 流 类库 和 异 常处理

9.6 文件与对象—例 9.13class inventory{ string Description; //商品名称 string No; //货号 int Quantity; // 数量 double Cost; //价格 double Retail; //零售public: inventory (string ="#",string="#",int =0,double =0,double =0); friend ostream & operator<<(ostream&dist,inventory&iv); // 存对象 friend istream & operator>>(istream&sour,inventory&iv); //建对象 bool operator == (inventory &); //货号为关键字 bool operator <= (inventory &);};

inventory::inventory(string des,string no, int quan,double cost,double ret){

Description=des; No=no; Quantity=quan; Cost=cost; Retail=ret;}

Page 38: Unit  9  — I/O 流 类库 和 异 常处理

9.6 文件与对象—例 9.13

ostream & operator << (ostream &dist, inventory&iv){ dist<<left<<setw(20)<<iv.Description<<setw(10)<<iv.No; dist<<right<<setw(10)<<iv.Quantity<<setw(10)<<iv.Cost <<setw(10)<<iv.Retail<<endl;

return dist;}

Page 39: Unit  9  — I/O 流 类库 和 异 常处理

9.6 文件与对象—例 9.13istream &operator >> (istream &sour, inventory &iv){ if ( sour == cin ) { cout<<"请输入货物名称: "<<endl; sour>>iv.Description; cout<<"请输入货号: "<<endl; sour>>iv.No; cout<<"请输入货物数量: "<<endl; sour>>iv.Quantity; cout<<"请输入货物价格: "<<endl; sour>>iv.Cost; cout<<"请输入货物零售价格: "<<endl; sour>>iv.Retail; } else sour>>iv.Description>>iv.No>>iv.Quantity

>>iv.Cost>>iv.Retail; return sour;}

Page 40: Unit  9  — I/O 流 类库 和 异 常处理

bool inventory::operator == (inventory & inven) { return No == inven.No; }bool inventory::operator <= (inventory & inven) { return No <= inven.No; }template < typename T > class Array{ T *elements; int Subscript; //已用最大下标值 int maxSize; fstream datafile; // 文件流对象成员,聚合public: Array(int=2); //默认元素数为 2 ~Array(); bool IsFull() const { return Subscript == maxSize-1;} void renews(); //内存扩大一倍 void show(){ cout<<“已用最大下标值” <<Subscript<<‘\t’ <<“ 数组空间 "<<maxSize<<endl; } void ordinsert(T&); // 输入时以货号为关键字升序排序 friend ostream & operator << (ostream &dist, Array<T>&ar); //双重重载 };

9.6 文件与对象—例 9.13

Page 41: Unit  9  — I/O 流 类库 和 异 常处理

9.6 文件与对象—例 9.13template <typename T> Array<T>::Array ( int maxs ){ maxSize=maxs; Subscript=-1; //私有数据初始化 T temp; elements = new T[maxSize]; datafile.open(“mydatafile.txt”,ios::in); // 在当前目录下打开文件 if ( !datafile==0 ) {//打开文件成功,等价于 datafile!=0,datafile while ( !datafile.eof() ){

datafile>>temp; // 读文件,初始化对象if (datafile.eof()==0) { // 读到文件结束 , 读入不成

功 ,eofbit 为 1 ordinsert(temp); // 将 temp 对象升序插入动态数组

中}

} datafile.close(); // 关闭打开的文件, eofbit 会清零 } else datafile.clear(0); //若文件打开失败,流无法恢复}

P337 说法有误!注: open打开文件,若不存在则创建,但若子目录(路径)不存在,则创建失败。当前目录始终存在,不存在的文件可以创建!

P338 说法不准确!注:这个语句只针对文件打开失败的情况,若文件读到结束,则随着文件的关闭, eofbit 会清零,即文件指针返回文件头!

Page 42: Unit  9  — I/O 流 类库 和 异 常处理

template <typename T> Array<T>::~Array(){ int i; datafile.open("mydatafile.txt",ios::out); for(i=0;i<=Subscript;i++) datafile<<elements[i]; // 保存数据 datafile.close(); delete []elements; }

template <typename T> void Array<T>::renews(){ int i; T *temp=elements; maxSize*=2; elements=new T[maxSize]; // 指向新的动态数组 for(i=0;i<=Subscript;i++) elements[i]=temp[i]; delete[]temp; //释放原先的动态数组,小心内存泄露}

9.6 文件与对象—例 9.13

Page 43: Unit  9  — I/O 流 类库 和 异 常处理

template <typename T> void Array<T>::ordinsert(T & elem){ int i, j; // 输入时以货号为关键字排序 if(IsFull()) renews(); // 数组扩容 for(i=0;i<=Subscript;i++) if(elem<=elements[i]) break; // 定位 i if(!(elem==elements[i])){//elem 与 i 元素不重复 Subscript++; for(j=Subscript;j>=i;j--) elements[j+1]=elements[j]; }//依次后移 elements[i]=elem; // 插入 show();}

template <typename T>ostream & operator<< (ostream&dist, Array<T>&ar){ //双重重载的插入运算符 int i; for(i=0;i<=ar.Subscript;i++) cout<<ar.elements[i]; // 重载的 inventory 对象插入运算符 return dist; }

函数模板中以类模板作为参数, VC6.0 、 BCC5 、等都不支持,编译出错。解决办法:在 VC 2005及以上版本下运行;或将该插入符改为成员函数 void Array<T>::Output() ,由它调用inventory 的重载插入符 ;

9.6 文件与对象—例 9.13

Page 44: Unit  9  — I/O 流 类库 和 异 常处理

int main(){ Array<inventory> mylist; inventory temp; char ch; cout<<" 是否输入新商品? Y or N"<<endl; cin>>ch; while(ch=='Y'||ch=='y'){ cin.get(); //吸收回车 cin>>temp; mylist.ordinsert(temp); cout<<" 是否继续输入? Y or N"<<endl; cin>>ch;} cout<<mylist; //mylist.Output(); return 0; //mylist析构时,其数组元素保存到文件中}

9.6 文件与对象—例 9.13

Page 45: Unit  9  — I/O 流 类库 和 异 常处理

10.1 异常的概念

10.3 栈展开与 异常捕获

10.2 异常处理的机制 10.5 异常和继承

10.7 C++ 标准库异常类 层次结构 (选读)

10.6 异常规范(选读)

10.4 异常的重新抛出 和 catch_all子句(选读)

Unit 9 — I/O 流类库和异常处理

第十章 异常处理

Page 46: Unit  9  — I/O 流 类库 和 异 常处理

10.1 异常的概念

异常概念:异常( exception )是程序中可能检测到的,运行时的不正常情况,如存储空间耗尽、数组越界、被 0除等等。三点说明:1. 异常是可以预见的,即可以预测在什么地方,发什么类型的异常,尽管无法确知怎样发生和何时发生。2. 在可能发生错误的地方,添加相关代码,一旦错误发生,则停止发生错误的操作,回到调用操作的起点,对它进行处理,程序不用整个停止。3. 大型软件开发必须进行异常处理。

Page 47: Unit  9  — I/O 流 类库 和 异 常处理

10.1 异常的概念

如何处理异常?首先, C++ 提供了一些内置的语言来产生 (raise) 或抛出 (throw)异常,用以通知“异常已经发生”;然后,由预先安排的程序段来捕获 (catch)异常,并对它进行处理。

Page 48: Unit  9  — I/O 流 类库 和 异 常处理

10.2 异常处理的机制

template <typename T>void Stack<T>::Push(const T&data){ assert(!IsFull()) ; //假定未满,若栈满则退出程序 elements[++top]=data; }

template<typename T>T Stack<T>::Pop(){ assert(!IsEmpty()); // 出栈时栈空则退出程序 return elements[top--]; }

assert()语句测到栈满或栈空就退出程序

回忆【例 7.6 】栈类,其压栈和出栈成员函数 :

Page 49: Unit  9  — I/O 流 类库 和 异 常处理

10.2 异常处理的机制

测到栈满或栈空就抛出异常,而不是退出程序。template <typename T>void Stack<T>::Push(const T&data){ if(IsFull()) throw pushOnFull<T>(data);

// 注意加了括号 ,构造一无名对象 elements[++top]=data; }

template<typename T>T Stack<T>::Pop(){ if(IsEmpty()) throw popOnEmpty<T>(); return elements[top--]; }

异常与异常抛出:C++ 中异常以类来实现,例如 :

template <typename T>class popOnEmpty{...}; //栈空异常template <typename T>class pushOnFull{...}; //栈满异常

Page 50: Unit  9  — I/O 流 类库 和 异 常处理

10.2 异常处理的机制

1. C++ 要求抛出的必须是对象,是由调用异常类pushOnFull 类和 popOnEmpty 类的构造函数所建立的无名对象。2. throw 表达式抛出异常为异常处理的第一步。接着先捕获( catch )异常,并处理。在堆栈的压栈和出栈操作中发生错误而抛出的异常,理所当然地应由调用了压栈出栈操作的程序来处理。3. 异常并非总是类对象, throw 表达式也可以抛出任何类型的对象,如枚举、整数等等。但最常用的是类对象。

说明:

Page 51: Unit  9  — I/O 流 类库 和 异 常处理

10.2 异常处理的机制

C++ 中异常处理的程序设计方法:Step 1. 设计异常类,包含错误提示信息及一些处理;Step 2. 在可能出错的地方,设置 try块,它包含了能够抛出异常的语句或该语句的调用语句。Step 3. 设置 catch子句来处理对应 try块所抛出的异常。

异常处理机制:throwcatch 处理

Page 52: Unit  9  — I/O 流 类库 和 异 常处理

10.2 异常处理的机制

int main(){ int a[9]={1,2,3,4,5,6,7,8,9},b[9]={0},i; stack<int>istack(8); try{ for(i=0;i<9;i++) istack.Push(a[i]); istack.PrintStack(); } catch(pushOnFull<int>){cerr<<”栈满”<<endl;} try{ for(i=0;i<9;i++){b[i]=istack.Pop();} } catch(popOnEmpty<int>){cerr<<”栈空” <<endl;} for(i=0;i<9;i++) cout<<b[i]<<’\t’; cout<<endl; return 0; }

异常处理实例

Page 53: Unit  9  — I/O 流 类库 和 异 常处理

10.2 异常处理的机制

2. 与 catch 语句分别匹配的是在压栈和出栈成员函数模板中的 throw 语句,一个抛出 pushOnFull 类的无名对象,另一个抛出 popOnEmpty 类的无名对象。

3. 编程惯例:把正常执行的程序与异常处理两部分分隔开来,这样使代码更易于跟踪和维护。例如可以把两个 try块合成一个,而把两个 catch子句都放在函数最后。

说明:1. 这里有两个 try块,分别对应压栈与出栈;也有两个 catch子句( catch clause ),分别处理压栈时的栈满和出栈时的栈空。

Page 54: Unit  9  — I/O 流 类库 和 异 常处理

10.2 异常处理的机制

1 .如果没有异常发生,继续执行 try 块中的代码,与try 块相 关 联 的 catch 子句被忽略, 程 序正常执行, main() 返回 0 。2 .当某条语句抛出异常时,跟在该语句后面的语句将被跳过。程序执行权交给处理异常的 catch子句,如果没有catch子句能够处理异常,则交给 C++标准库中定义的函数 terminate() 。(1) 当第一个 try块在 for 循环中抛出异常,则退出该 for循环,也退出 try块,去执行可处理 pushOnFull异常的catch子句。 istack.PrintStack() 不再执行,被忽略。(2) 如果第二个 try块调用 Pop()抛出异常,则退出 for和 try块,去执行可处理 popOnEmpty异常的 catch子句。

流程控制规则:

Page 55: Unit  9  — I/O 流 类库 和 异 常处理

10.2 异常处理的机制

int main(){ int a[9]={1,2,3,4,5,6,7,8,9},b[9]={0},i; stack<int>istack(8); try{ for(i=0;i<12;i++) istack.Push(a[i]); istack.PrintStack(); } catch(pushOnFull<int>){cerr<<”栈满”<<endl;} try{ for(i=0;i<9;i++){b[i]=istack.Pop();} } catch(popOnEmpty<int>){cerr<<”栈空” <<endl;} for(i=0;i<9;i++) cout<<b[i]<<’\t’; cout<<endl; return 0; }

异常处理实例

Page 56: Unit  9  — I/O 流 类库 和 异 常处理

10.3 栈展开与异常捕获

catch子句的组成:关键字 catch 、圆括号中的异常声明以及复合语句 {} 中的一组语句。• catch子句不是函数,所以圆括号中不是形参,而是

一个异常类型声明,可以是类型也可以是对象。• catch子句的使用:没有定义和调用之分,使用时由

系统按规则自动在 catch子句列表中匹配。

catch子句可以包含返回语句( return ),也可不包含返回语句。包含返回语句,则整个程序结束。而不包含返回语句,则执行 catch 列表之后的下一条语句。

catch子句说明:当 try块中的语句抛出异常时,系统通过查看随后的catch子句列表,来查找可处理该异常的 catch子句。

Page 57: Unit  9  — I/O 流 类库 和 异 常处理

10.3 栈展开与异常捕获

对应在 throw 表达式中,构造抛出对象也要有实参:throw pushOnFull(data); //data 即Push(const &data) 中 data

template <typename T>class pushOnFull{ T _value;public: pushOnFull(T i):_value(i){} // 或pushOnFull(T i){_value=i;} T value(){return _value;} };

私有数据成员 _value 保存不能被压入栈中的值,该值用作调用构造函数时传入的实参。

catch子句中异常声明:异常声明中可以是一个对象。以栈为例,当栈满时,要求在异常对象中保存不能被压入栈的值, pushOnFull 类可定义如下:

Page 58: Unit  9  — I/O 流 类库 和 异 常处理

10.3 栈展开与异常捕获

在 catch子句中,要取得 _value ,须调用pushOnFull 中的成员函数 value() ,必须要引入一个具体的对象名。catch(pushOnFull<T> eObj){ cerr<<” 栈满” <<eObj.value()<<” 未压入栈” <<endl; return 1;}

异常对象是在抛出点被创建的无名对象,在 catch子句中只是为了调用异常类的成员函数才给它一个对象名 eObj 。

Page 59: Unit  9  — I/O 流 类库 和 异 常处理

catch子句的异常声明可以声明对象本身,也可以声明为对象的引用,这与函数参数声明类似。为减少不必要的复制,其异常最好也应被声明为引用,如:catch(pushOnFull<T> & eObj){ cerr<<”栈满” <<eObj.value()<<” 未压栈” <<endl; return 1; }使用引用类型的异常声明,异常对象的本身可以被catch子句修改。异常对象处理完后,生命期结束,这与一般类对象有差异。

【例10.1】包含栈满或空异常的完整的程序。

10.3 栈展开与异常捕获

Page 60: Unit  9  — I/O 流 类库 和 异 常处理

例 10.1 堆栈异常处理

template<typename T>class pushOnFull{ //栈满异常声明T _value;

public:pushOnFull(T i){_value=i;}T value(){return _value;}void print(){cerr<<"栈满, "<<value()<<" 未压入栈 "<<endl;}

};

template<typename T>class popOnEmpty{//栈空异常声明

public:void print(){cerr<<"栈已空,无法出栈 "<<endl;}

};

Page 61: Unit  9  — I/O 流 类库 和 异 常处理

template<typename T>class Stack{int top; //栈顶指针(下标)T *elements; //动态建立的数值int maxSize; //栈最大允纳的元素个数

public:Stack(int=20); //栈如不指定大小,设为 20 元素~Stack(){delete[] elements;}void Push(const T &data) ; //压栈T Pop(); //弹出, top--T GetElem(int i){return elements[i];} // 返回指定元素void MakeEmpty(){top= -1;} // 清空栈bool IsEmpty() const{return top== -1;} // 判栈空bool IsFull() const{return top==maxSize-1;} // 判栈满void PrintStack(); // 输出栈内所有数据

};

例 10.1 堆栈异常处理

Page 62: Unit  9  — I/O 流 类库 和 异 常处理

template<typename T> void Stack<T>::Push(const T &data){if(IsFull()) throw pushOnFull<T>(data); //栈满则抛出异常elements[++top]=data;

//栈顶指针先加 1 ,元素再进栈, top 是指向栈顶元素

}template<typename T>T Stack<T>::Pop() {

if(IsEmpty()) throw popOnEmpty<T>(); //栈已空则不能退栈,抛出异常

return elements[top--];// 返回栈顶元素,同时栈顶指针退 1

}

例 10.1 堆栈异常处理

Page 63: Unit  9  — I/O 流 类库 和 异 常处理

int main(){int a[9]={1,2,3,4,5,6,7,8,9}, b[9]={0},i;Stack<int>istack(8);try{

for(i=0;i<9;i++) istack.Push(a[i]); // 到 a[8] 时栈满 , 异常

istack.PrintStack();}catch(pushOnFull<int>&eObj){eObj.print();}try{for(i=0;i<9;i++) b[i]=istack.Pop();}catch(popOnEmpty<int>&eObj){ eOb-j.print();}for(i=0;i<9;i++) cout<<b[i]<<'\t';cout<<endl;return 0;

}

例 10.1 堆栈异常处理

Page 64: Unit  9  — I/O 流 类库 和 异 常处理

main()

f1()

f2()

fm()

fm+1()

fn()

……

调用函数f1()

调用函数f2()

调用函数fm()

调用函数f3()

调用函数fm+1()

调用函数fm+2()

调用函数fn()

发生异常

catch列表

调用异常处理

catch列表

catch列表

catch列表

catch列表

由terminate()处理

最终在程序中未找到匹配的异常

未找到匹配子句,向上寻找匹配

找到匹配子句,处理异常后结束

未找到匹配子句,向上寻找匹配

仍未找到匹配子句,向上寻找匹配

找到匹配子句,处理异常后结束

找到匹配子句,处理异常后结束

找到匹配子句,处理异常后结束

找到匹配子句,处理异常后结束寻找匹配的 catch子

句: 如果 throw 表达式位于 try块中,则检查与 try块相关联的catch子句列表,看是否有匹配的子句能够处理该异常;如无匹配的 catch子句,则在主调函数中继续查找。 如果函数调用在退出时带有一个被抛出的异常未能处理,而且该调用位于一个 try块中,则检查与该 try块相关联的 catch子句列表,看是否有匹配的子句能处理该异常; 没有,则查找过程逆着嵌套的函数调用链向上继续,直到找到能处理该异常的 catch子句。 只要遇到第一个匹配的catch子句,就会进入该catch子句进行处理,查找结束。 如最终未找到匹配的字句,则由 terminate() 处理。

Page 65: Unit  9  — I/O 流 类库 和 异 常处理

栈展开: 因发生异常而逐步退出复合语句和函数定义的过程,被称为栈展开 (stack unwinding)— 异常处理的核心技术。

10.3 栈展开与异常捕获

int main(){int a[9]={1,2,3,4,5,6,7,8,9}, b[9]={0},i;Stack<int>istack(8);try{ for(i=0;i<9;i++) istack.Push(a[i]); // 到 a[8] 时栈满 , 异常

istack.PrintStack();}catch(pushOnFull<int>&eObj){eObj.print();}try{for(i=0;i<9;i++) b[i]=istack.Pop();}catch(popOnEmpty<int>&eObj){ eOb-j.print();}for(i=0;i<9;i++) cout<<b[i]<<'\t';cout<<endl;return 0;

}

Page 66: Unit  9  — I/O 流 类库 和 异 常处理

  在栈异常处理的例子中,对 popOnEmpty ,首先应在 istack 的成员函数 Pop() 中找,因为 Pop() 中没有 try块,不存在 catch子句,所以 Pop() 带着一个异常退出。下一步是检查调用 Pop() 的函数,这里是main() ,在 main() 中对 Pop() 的调用位于一个 try块中,则可用与该 try块关联的 catch子句列表中的某一个来处理,找到第一个 popOnEmpty 类型异常声明的catch子句,并进入该子句进行异常处理。

异常对程序的影响通常不仅是在发生异常的那个局部范围中,而且可能逆调用链而上,甚至整个任务。因此,异常处理应该在其对程序影响的终结处进行,甚至是在调用该任务的菜单处进行。

10.3 栈展开与异常捕获

Page 67: Unit  9  — I/O 流 类库 和 异 常处理

  在栈展开期间,在退出的域中有某个局部量是类对象,栈展开过程将自动调用该对象的析构函数,完成资源的释放,这里资源包括动态分配的资源和打开的文件。所以 C++ 异常处理过程本质上反映的是“资源获取是由构造函数实现,而资源释放是由析构函数完成” 。

栈展开时资源的释放 :

异常处理用于面向对象的程序设计。对非面向对象的程序设计如果函数动态获得过资源,因异常,这些资源的释放语句可能被忽略,则这些资源将永远不会被自动释放。

10.3 栈展开与异常捕获

Page 68: Unit  9  — I/O 流 类库 和 异 常处理

10.3 栈展开与异常捕获

异常表明一个程序不能够继续正常执行,不能够永远保持在未被处理的状态。因此,如果没有找到处理代码,程序就调用 C++ 标准库中定义的函数terminate() 实现非正常退出。

异常对象的生命周期:异常对象是在 throw 表达式中建立并抛出: throw表达式通过调用异常类的构造函数创建一个临时对象,然后把这个临时对象复制到一个被称为异常对象( exception object )的存贮区中,它保证会持续到异常被处理完。

Page 69: Unit  9  — I/O 流 类库 和 异 常处理

函数调用和异常处理的区别:1. 建立函数调用所需要的全部信息在编译时已经获得,而异常处理机制要求运行时的支持。 2. 对于普通重载函数调用,通过函数重载解析过程,编译器知道在调用点上哪个函数会真正被调用。但对于异常处理,编译器不知道哪个 throw 表达式抛出异常以及不能确定由哪个 catch子句捕捉处理,在处理完之后执行权被转移到何处。异常是随机发生的,异常处理的 catch子句是逆调用链进行查找,这与运行时的多态 ——虚函数也是不一样的。 3. 当一个异常无处理代码时,系统无法通知用户,所以要有 terminate() 函数,由它结束程序并通知用户。

10.3 栈展开与异常捕获

Page 70: Unit  9  — I/O 流 类库 和 异 常处理

完谢谢!