第九章 流类库和输入 /输出本章的目的就是把数据保存到磁盘文件中,C++同时解决了更多的问题 。
C++语言中并没有输入 /输出语句,而是在标准库里包含了一个 I/O流类库,它与标准模板库同为 C++标准库中最重要的组成部分。数据从一个对象到另一个对象的传送被抽象为“流”。数据的输入 /输出就是通过输入 /输出流来实现的。
流是一种抽象的概念,负责在数据的产生者和数据的使用者之间建立联系,并管理数据的流动。
第九章 流类库和输入 /输出
9.1 C++的基本流类体系
9.3 标准设备的输入 /输出
9.2 输入输出的格式控制
9.4 文件的输入与输出
9.6 MFC中的文件处理
9.5 字符串流(内存流)
9.1 C++的基本流类体系整个流类体系是一个派生类体系。整个流类体系在头文件
<iostream.h>中说明,它包含了 <ios.h>,<stream.h>、
<istream.h>和 <ostream.h>。而输入输出文件流部分在
<fstream.h>中说明,它包含 <iostream.h>。
图 9.1 输入 /输出流类派生体系
ios streambuf
istream ostream
istream_withassign iostream ostream_withassign
iostream_withassign
指针
9.1 C++的基本流类体系
ios
istream
ifstrstream
istream_withassign
ifstream
streambuf
filebuf
strstreambuf
stdiobuf
iostream_init
iostream
fstream
strstream
stdiostreamostream
ofstream
ostream_withassign
ostrstream
9.1 C++的基本流类体系在流类库中,最重要的两部分功能为标准输入 /输出( standard input/output)和文件处理。在 C++的流类库中定义了四个全局流对象:
cin,cout,cerr和 clog。使用这四个流对象,
可以完成人机交互的功能。要使用这四个功能,
必须包含 <iostream.h>文件。
文件处理完成永久保存的功能。在 VC++的
MFC编程中采用了序列化( Serialization)。
9.2 输入输出的格式控制
C++在类 ios中提供格式化输入输出 。 格式控制符定义为公有的无名的枚举类型:
enum{ skipws=0x0001 //跳过输入中的空格字符
left=0x0002 //输出左对齐
right=0x0004 //输出右对齐
internal=0x0008 //在输出符号或数制字符后填充
dec=0x0010 //在输入输出时将数据按十进制处理
oct=0x0020 //在输入输出时将数据按八进制处理
hex=0x0040 //在输入输出时将数据按十六进制处理
showbase=0x0080 //在输出时带有表示数制基的字符
showpoint=0x0100 //输出符点数时,必定带小数点
uppercase=0x0200 //输出十六进制,用大写
showpos=0x0400 //输出正数时,加,+”号
scientific=0x0800 //科学数方式输出浮点数
fixed=0x1000 //定点数方式输出实数
unitbuf=0x2000 //插入后,立即刷新流
stdio=0x4000 //插入后,立即刷新 stdout和 stderr }
9.2 输入输出的格式控制采用格式控制,只要把对应位置 1即可,取多种控制时,用或,|”运算符来合成。
【 例 9.1】 整型数输出 。
#include<iostream.h>
void main(void){
int inum=255;
cout<<”十进制方式,<<inum<<’\t’;
cout.flags(ios::oct|ios::showbase);
cout<<”八进制方式,<<inum<<’\t’;
cout.setf(ios::hex);
cout<<”十六进制方式,<<inum<<endl;
}
程序执行后输出:十进制方式 255 八进制方式 0377 十六进制方式 0xff
9.2 输入输出的格式控制
【 例 9.2】 浮点数输出 。
#include<iostream.h>
void main(){
double fnum=31.415926535;
cout<<”缺省域宽为,”<<cout.width()<<”位,<<’\n’;
cout<<”缺省精度为,”<<cout.precision()<<”位,<<’\n’;
cout<<”缺省表达方式,”<<funm<<’\n’;
cout.setf(ios::scientific,ios::floatfield);
cout<<”科学数表达方式,”<<fnum<<’\n’;
cout.setf(ios::fixed,ios::floatfield);
cout<<”定点表达方式,”<<fnum<<’\n’;
cout.precision(9);
cout.setf(ios::scientific,ios::floatfield);
cout<<”定点 9位科学数表达方式,”<<fnum<<endl; }
9.2 输入输出的格式控制其中 floatfield,是为了避免浮点数互相冲突的双重规定,所以是清除原浮点数第 13和第 14两位,再用新的一位去代替 。 程序执行后输出:
缺省域宽为,0位缺省精度为,6位缺省表达方式,31.4159
科学数表达方式,3.141593e+001
定点表达方式,31.415927
定点 9位科学数表达方式,3.141592654e+001
9.2 输入输出的格式控制
cin,cout和 clog都是缓冲流。输出而言,
仅当输出缓冲区满才将缓冲区中的信息输出,
对输入而言,仅当输入一行结束,才开始从缓冲区中取数据,当希望把缓冲区中的信息立即输出,可用 flush,
9.3 标准设备的输入 /输出标准设备输入不安全,健壮性差,错误的输入类型导致错误的结果而且是无法预知的错误结果 。 注意以下几点 。
1,cin为缓冲流 。 不可能用刷新来清除缓冲区,不能 输错,也不能多输 !
2,输入数据类型必须与对应要提取的数据类型一致,
否则出错。在编程中加入对状态字 state的判断,
可以提高输入的健壮性。
3,空格和回车都可以作为数据之间的分格符,但如果是字符型和字符串,则空格 。 回车符无法读入 。
9.3 标准设备的输入 /输出
enum ios_state{
goodbit=0x00; //流正常
eofbit=0x01; //输入流结束,忽略后继提取操作;或文件结束,已无数据可取
failbit=0x02; //最近的 I/O操作失败,流可恢复
badbit=0x04; //最近的 I/O操作非法,流可恢复
hardfail=0x08; //I/O出现致命错误,流不可恢复,VC++6.0不支持
}
9.3 标准设备的输入 /输出读取状态有关操作如下:
inline int ios::rdstate() const {return state;} //读取状态字
inline int ios:operator!() const{return state&(badbit|failbit);}
//可用操作符 !()代替 fail()
inline int ios::bad(){ return state & badbit;} //返回非法操作位
inline void ios::clear(int _i){ lock();state=_i;unlock();}
//人工设置状态,可用来清状态
inline int ios::eof() const {return state&eofbit;} //返回流 (文件 )结束位
inline int ios::fail() const{return state&(badbit|failbit);}
//返回操作失败,流可恢复这两位
inline int ios::good() const{return state==0;} //正常返回 1,否则返回 0
9.3 标准设备的输入 /输出
【 例 9.3】 提高输入的健壮性 。 输入时需要故意输错,以测试健壮性 。
程序:E x9_3.cpp
9.3 标准设备的输入 /输出输入流成员函数声明:
istream&istream::get(char &);//提取一个字符,放在字符型变量中
istream&istream::get(unsigned char &);//同上一函数
istream&istream::get(signed char &);//同上一函数
istream&istream::get(char *,int,char=’\n’);//提取字符串,到串结束符或指定长度为止
istream&istream::get(unsigned char *,int,char=’\n’); //同上
istream&istream::get(signed char *,int,char=’\n’); //同上
istream&istream::getline(char *,int,char=’\n’);//同上,但包括分隔符
istream&istream::getline(unsigned char *,int,char=’\n’);//同上
istream&istream::getline(signed char *,int,char=’\n’);//同上
9.3 标准设备的输入 /输出
【 例 9.4】 ignore()和 gcount()函数使用。
(运行两种情况下的输出 )
类 ostream串中输出字符的成员函数有:
ostream&ostream::put(char);//输出参数字符
ostream&ostream::put(unsigned char);
ostream&ostream::put(signed char);
ostream&ostream::flush();//刷新一个输出流,用于 count和 clog
9.4 文件的输入与输出本节中文件指的是磁盘文件 。 C++根据文件 ( file) 内容的数据格式,可分为两类:二进制文件和文本文件 。 文本文件由字符序列组成,也称 ASCII码文件,在文本文件中存取的最小信息单位为字符 ( character),而二进制文件中存取的最小信息单位为字节 ( Byte) 。
使用文件的方法是固定的,首先打开一个文件,使磁盘文件和文件流对象建立联系,然后将某种格式的数据写入一个文件,
以后可按同样格式从这个文件读出数据。最后当不再使用文件时,
要关闭文件,这时文件才能从缓冲区中完全写回磁盘。类
ifstream支持从输入文件中提取数据的操作。而类 ofstream完成数据写入文件中的各种操作。
9.4 文件的输入与输出
9,4,1 文件的打开与关闭
9,4,2 文件的读写
9.4.1 文件的打开与关闭文件的使用步骤如下:
1,说明一个文件流对象 。 如果只输入用 ifstream的对象,只输出用
ofstream的对象,既输入又输出用 iofstream的对象 。
ifstream ifile;
ofstream ofile;
fstream iofile;
这又被称为内部文件 。
9.4.1 文件的打开与关闭
2,使用文件流对象的成员函数打开一个磁盘文件 。 这样在文件流对象和磁盘文件名之间建立联系 。 文件流中说明了三个打开文件的成员函数 。
void ifstream::open(const char*,int =ios::in,int=filebuf::openprot);
void ofstream::open(const char *,int=ios::out,int=filebuf::opernprot);
void fstream::open(const char*,int,int=filebuf::openprot);
9.4.1 文件的打开与关闭三个文件流类都重载了一个带缺省参数的构造函数,与 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,int=filebuf::operprot);
所以 1,2两步可合成:
fstream iofile(,myfile.txt”,ios::in|ios::out) ;
如同动态分配内存一样,打开文件也应该判断是否成功,若打开成功,文件流对象值为非零值,不成功为 0( NULL),文件流对象值物理上就是指它的地址。
因此打开一个文件完整的程序为:
fstream iofile(,myfile.txt”,ios::in|ios::out) ;
if(!iofile){
cout<<”不能打开文件,”<<”myfile,txt”<<endl;
exit(1);//失败退回操作系统
}
9.4.1 文件的打开与关闭
3,使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写,这在下一节中讨论 。
4.关闭文件。
void ifstream::close();
void ofstream::close();
void fstream::close();
关闭文件时,系统把该文件相关联的文件缓冲区中的数据写到文件中,保证文件的完整,收回与该文件相关的内存空间,可供再分配,把磁盘文件名与文件流对象之间的关联断开,可防止误操作修改了磁盘文件。如又要对文件操作必须重新打开。
9.4.1 文件的打开与关闭文件流对象在程序结束时,或它的生命期结束时,由析构函数撤消 。 它同时释放内部分配的预留缓冲区 。
文件打开方式是在 ios类中定义的,公有枚举成员:
enum open_mode{
in=0x01//打开文件用于输入操作 (从文件读取 ),文件指针在文件头
out=0x02 //打开文件用于输出操作 (写入文件 )。 如文件不存在,则建立,但指定目录
//必须存在,否则建立文件失败 。 如文件存在,未同时设 app,ate,in则文件清空
ate=0x04 //打开文件用于输入 /输出,文件指针在文件尾,但新数据可写到任何位置
app=0x08//打开文件用于输出,但从尾部添加,新数据只能添加在尾部
trunce=0x10//打开文件,并清空它,以建立新文件
nocreate=0x20//如文件存在则打开,不存在并不创建新文件
noreplace=0x40//如文件不存在则创建,如文件存在则只能设为 ate及 app方式
binary=0x80//以二进制方式打开文件
};
9.4.2 文件的读写文件的读写可以是随机的也可以是顺序的,所谓随机指可以从文件任何位置进行读写,顺序是从文件头开始,或从尾部续上 。 读写可用 C++的提取运算符 ( >>)
和插入运算符 ( <<) 进行 。
【 例 9.7】 复制文件。
程序,Ex9_7.cpp
9.4.2 文件的读写
【 例 9.8】 按行进行复制文本文件。
程序,Ex9_8.cpp
9.4.2 文件的读写
【 例 9.9】 文本式数据文件的创建与读取数据。
程序,Ex9_9.cpp
本例是典型的 C++数据存入文件和由文件获得数据的方法,按面向对象的说法为把 对象存入文件和由文件重构对象。
对二进制文件的读写,C++也提供了成员函数来实现:
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);
读函数并不能知道文件是否结束,可用状态函数 int ios::eof()来判断。
9.4.2 文件的读写
【 例 9.10】 创建二进制数据文件,以及数据文件的读取。
按 C++程序设计惯例,将该操作设计为成员函数。为与
【 例 9.9】 对比,采用同名的类,并有同样的数据成员。
程序,Ex9_10.cpp
使用二进制文件,可以控制字节长度,读写数据不会出现二义性,可靠性高。同时不知格式是无法读取的,保密性好。
9.4.2 文件的读写
C++把每一个文件都看成一个有序的字节流,见图 9.3.每一个文件或者以文件结束符( end of file
marker)结束,或者在特定的字节号处结束。
0 1 2 43 65 7 8 … n-1
… 文件结束符图 9-3 C++把文件看作有序的 n个字节的流
9.4.2 文件的读写当打开一个文件时,该文件就和某个流关联起来了 。
对文件进行读写实际上受到一个文件定位指针 ( file
position pointer) 的控制,输入流的指针也称为读指针,
每一次提取操作将从读指针当前所指位置开始,每次提取操作自动将读指针向文件尾移动 。 输出流指针也称写指针,
每一次插入操作将从写指针当前位置开始,每次插入操作自动将写指针向文件尾移动 。
在 C++中可以由程序移动文件指针,从而实现文件的随机访问,即可读写流中任意一段内容。一般文本文件很难准确定位,所以随机访问多用于二进制文件。
9.4.2 文件的读写在 ios类中说明了一个公有枚举类型:
enum seek_dir{
beg=0;//文件开头
cur=1;//文件指针的当前位置
end=2;//文件结尾
};
istream类中提供了三个成员函数
istream&istream::seekg(streampos);//指针直接定位
istream&istream::seekg(streamoff,ios::seek_dir);//指针相对定位
long istream::tellg();//返回当前指针位置流的指针位置类型 streampos和流的指针偏移类型 streamoff定义为长整型,
9.4.2 文件的读写
datafile.seekg(-20L,ios::cur);
表示将文件定位指针从当前位置向文件头部方向移 20个字节 。
datafile.seekg(20L,ios::beg);
表示将文件定位指针从文件头向文件尾方向移 20个字节 。
datafile.seekg(-20L,ios::end);
表示将文件定位指针从文件尾向文件头方向移 20个字节 。
tellg()和 seekg()往往配合使用,按 【 例 9.10】 中的定义和程序
long pos=sdatafile.tellg();
car2.Bdatafromfile(sdatafile);
sdatafile::seekg(pos);
motor2.Bdatafromfile(sdatafile);
这时 car2和 motor2中放的都是 car1的数据。
9.4.2 文件的读写
ostream类也提供了三个成员函数管理文件定位指针
ostream&ostream::seekp(streampos);
ostream&ostream::seekp(streamoff,ios::seek_dir);
long ostream::tellp();
9.4.2 文件的读写
【 例 9.11】 使用随机访问对 【 例 9.10】 进行改造 。 将入口 ( main()) 程序中的文件改为输入输出文件,写完后将文件定位指针拨回文件开始处 。 对应商品类中两成员函数:
Bdatatofile(fstream&dist);
Bdatafromfile(fstream&dist);
参数类型也已改为 fsream&。
9.5 字符串流(内存流)
可以把流的概念连接到字符串 ( string) 上 。 文件在 C++看来是字节流或字符流,串当然更可以看作字符流 。 可以用输入输出操作来完成串流的操作 。 其他的流是与设备相关,而串流是与内存相关,所以也称内存流 。 串流类包括 ostrstream,istrstream、
strstream。 参见图 9.2。 它们在 <strstream.h>中说明 。 串流类对象可以保存字符,也可以保存整数,浮点数 。 串流类对象采用文本方式,如同文本文件一样 。 其构造函数常用下面几个:
istrstream::istrstream(const char * str);
istrstream::istrstream(const char * str,int);
ostrstream::ostrstream(char *,int,int=ios::out);
9.5 字符串流(内存流)
以串流为信息源:
char str[36]=”This is a book.\n”;
char ch;
istrstream input(str);
input>>ch;//从输入设备 (串 )读入一个字符
cout<<ch<<endl;//输出 ’ T’
9.5 字符串流(内存流)
将整数和浮点数放入串中,并取出来
int inum1=93,inum2;
double fnum1=89.5,fnum2;
char *str=new char[30];
ostrstream output(str,30);
output<<”整数,”<<inum<<”符点数,”<<fnum1<<endl;
output.close();
cout<<strlen(str)<<endl;
istrstream input(str,0);//第二参数为 0表示串以串结束符终结
input>>inum2>>fnum2;
cout<<”整数,”<<inum2<<” 浮点数,”<<fnum2<<endl;//输出:整数,93
浮点数,89。 5
delete [] str;
9.6 MFC中的文件处理
MFC中的文件系统,是广义的文件系统。先讨论
MFC的文档 /视结构( Document/view),再讨论序列化( Serialization)与永久保存。
9.6.1 文档 /视结构
9.6.2 存档类序列化
9.6.1 文档 /视结构文档 ( document) 指的是数据组合的抽象,它通常对应磁盘文件,也可以对应 I/O端口 ( 包括打印机,键盘 ) 或一块内存空间
( 串流 ) 。
同样的数据,我们可以用不同的方式来表示。把纯的数据放在文件中或数据库中。而把显示方式分离出来,可以让一份数据有多种不同的显示方式,程序中的数据表现称为“视” 。这样就可以分别考虑数据的保存 —— 文档,和数据的表现 —— 视。
MFC提供了一种应用程序框架,其核心就是文档 /视结构 。 在三种最基本的 MFC程序框架中有两种:多文档结构和单文档结构 。
MFC采用两个既相互独立又相互紧密联系的类来实现这一结构:
1,文档类 CDocument,它主要用于存取文档;
2,视图类 CView,它主要用于显示和打印文档,并支持与用户的交互。
9.6.1 文档 /视结构图 9.4给出了从文件对象到视图窗口的相互关系 。
读数据 送显视图窗口数据保存在视图成员变量中视图对象文档对象数据保存在文档的成员变量中由文档实现数据的提取和插入存 档 对象由 视 图实 现 文档 数 据更新由 视 图实 现 数据更新图 9.4 文档 /视有关系统文件对象
9.6.1 文档 /视结构文档类的含义非常广泛。包括复合文档,在多文档结构( MDI)中一个子窗口所掌握的数据也称为一个文档。文档就是数据,CDocument就可以简单地理解为负责处理数据的类。在 MFC编程时,我们应该把所有要处理的从外部获得的数据全部放在文档类及其派生类的对象中。 CDocument类是一个预先编好的类,只是提供了一个外壳或者讲框架,程序员可以在 Document中建立实际要用的数据格式及其管理方法。最常用的方法当然是派生出自己的文档类( CMyDocument类)。
9.6.1 文档 /视结构与文档类相配合的视类( CView)文档和视是数据的两个方面,文档是体,视是表现。
View还负责程序与用户之间的交互。
文档 /视结构中,MFC类已经把一个应用程序所需的,数据处理与显示,的函数空壳都设计好了,这些函数都是虚函数,这样我们可在自己的派生类( CMyDocument和 CMyView)中重编它们(多态)。
9.6.2 存档类序列化存档类( CArchive类)不是 CObject的派生类,它提供运行时对象模式支持。 Serialize()函数的参数是存档类 CArchive对象的引用。 CArchive类对象是在永久性对象(磁盘文件,CFile类对象)
和非永久性对象(内存对象,如文档类对象)之间充当一个过渡角色。负责按一定的顺序和格式在两者之间进行存取。文件一般为二进制格式文件。而内存对象可以是简单数据变量,也可以是 C++对象或 CObject派生类对象。参见图 9.5。
CFile对象 CArchive对象 内存对象读写
>>
<<
图 9.5 对象永久保存的层次结构
9.6.2 存档类序列化
MFC的存储功能被称为序列化( Serialization),CArchive对象从字面上看是档案,实际上是一个内存缓冲区。对文件的操作如果改为对
CArichive对象的操作,速度要快的多,Serialize()操作的对象就是
CArchive类对象。
函数 CObject::Serialize是一个虚函数。改写时先调用它。要使
CObject派生类真正有可序列化( Serializable)的功能,必须具备下列条件:
1,由 CObject派生出来,以保证有运行时类型识别( RTTL,
Runtime Type Identification)、动态生成( Dynamic Creation)等机能。
2,改写 Serialize虚函数,使它能适当地把类成员变量写入档案 。
3,要加一个缺省的构造函数 ( 无参 ) 。 MFC往往必须先动态生成存档类对象,而且是在无任何参数下生成对象 。
4.类声明必须包含一些必要的宏。
9.6.2 存档类序列化通常在 MFC程序中,由 CWinApp::OnFileSaveAs()消息处理函数,打开一个对话框提示输入一个文件名,并创建一个该文件的 CFile对象,然后把 CFile对象绑定到一个
CArchive对象上,最后调用文档类的 Serialize()成员函数由参数引用 CArchive对象 。
CArichive对象与内存对象之间数据流的流动方向可用成员函数 IsStoring()或 IsLoading()来判断当前的数据流动方向。当打开文档时,IsStoring()返回 false,
IsLoading()返回 true。当保存文档时,返回值相反。这可避免在程序中出现按实际数据流动方向相反操作的异常。