2.6 输入/输出流
在C++中,没有专门的内部输入/输出语句。但为了方便用户灵活使用输入/输出功能,C++提供了两套输入/输出方法:一套是与C语言相兼容的输入/输出函数,如printf和scanf函数等;另一套是使用功能强大的输入/输出流库ios。
尽管printf和scanf函数可以使用格式字符串来输入/输出各种不同数据,但却不能操作类对象数据。而C++的输入/输出流库ios不仅可以实现printf和scanf函数的功能,而且可以通过对提取运算符“>>”和插入运算符“<<”进行重载,从而实现类对象数据流的操作,扩展流的输入/输出功能。
2.6.1 流类和流对象
在C++中,输入/输出是由“流”来处理的。所谓流,它是C++的一个核心概念,数据从一个位置到另一个位置的流动抽象为流。当数据从键盘或磁盘文件流入到程序中时,这样的流称为输入流,而当数据从程序中流向屏幕或磁盘文件时,这样的流称为输出流。当流被建立后就可以使用一些特定的操作从流中获取数据或者向流中添加数据。从流中获取数据的操作称为提取操作,向流中添加数据的操作称为插入操作。
C++针对流的特点,构造了功能强大的输入/输出流库,它具有面向对象的特性,其继承结构如图2.2所示。
图2.2中,ios类用来提供一些关于对流状态进行设置的功能,它是一个虚基类,其他类都是从这个类派生而来的,但streambuf不是ios类的派生类,在类ios中只有一个指针成员,指向streambuf类的一个对象。streambuf类是用来为ios类及其派生类提供对数据的缓冲支持。所谓缓冲,是指系统在主存中开辟一个专用的区域来临时存放输入/输出信息,这个区域称为缓冲区。有了缓冲区以后,输入/输出时所占用的CPU时间就大大减少了,提高了系统的效率。这是因为只有当缓冲区满时,或当前送入的数据为新的一行时,系统才对流中的数据进行处理,称为刷新。
图2.2 C++的输入/输出流库
itream和ostream类均是ios的公有派生类,前者提供了向流中插入数据的有关操作,后者则提供了从流中提取数据的有关操作。iostream类是itream和ostream类公有派生的,该类并没有提供新的操作,只是将itream和ostream类综合在一起,提供一种方便。
为了方便用户对基本输入/输出流进行操作,C++提供了4个预定义的标准流对象:cin、cout、cerr和clog。当在程序中包含了头文件“iostream.h”,编译器调用相应的构造函数,产生这4个标准流对象,在程序中就可以直接使用它们了。其中,cin是istream类的对象,用处理标准输入,即键盘输入。cout是ostream类的对象,用处理标准输出,即屏幕输出。cerr和clog都是ostream类的对象,用来处理标准出错信息,并将信息显示在屏幕上。在这4个标准流对象中,除了cerr不支持缓冲外,其余3个都带有缓冲区。
标准流通常使用插入运算符“<<”和提取运算符“>>”来进行输入/输出操作,而且系统还会自动地完成数据类型的转换。由于以前已讨论过cin和cout的基本用法,对于cerr和clog也可同样使用,因此这里不再重复。
2.6.2 流的格式控制和错误处理
C++标准的输入/输出流提供了两种格式的控制方式,一种是使用ios类中的相关成员函数,如width、precision和fill等,另一种是可以直接使用的格式操作算子,如oct、hex和dec等。下面分别来讨论它们的使用方法及流的错误处理。
1.使用格式控制成员函数
在ios类中控制输入/输出的成员函数有:
int ios::width(); // 返回当前的宽度设置 int ios::width(int); // 设置宽度并返回上一次的设置 int ios::precision(); // 返回当前的精度设置 int ios::precision(int); // 设置精度并返回上一次的设置 char ios::fill(); // 返回当前空位填充的字符 char ios::fill(char); // 设置空位填充的字符并返回上一次的设置 long ios::setf(long); // 设置状态标志并返回上一次的状态标志 long ios::unsetf(long); // 消除状态标志并返回上一次的状态标志 long ios::flags(); // 返回当前的状态标志 long ios::flags(long); // 设置状态标志并返回上一次的状态标志
在这些成员函数中,前面6个成员函数在以前曾经讨论过,这里不再重复。后面的4个成员函数都跟状态标志有关,各种状态值之间都是通过“|”(或运算)组合而成的,在ios类中是一个公共的枚举类型,各个标志代表的含义如下:
ios::skipws 跳过输入中的空白符 ios::left 输出数据按输出域左对齐 ios::right 输出数据按输出域右对齐(默认) ios::internal 数据的符号左对齐,数据本身右对齐,符号和数据之间为填充字符 ios::dec 转换为十进制形式 ios::oct 转换为八进制形式 ios::hex 转换为十六进制形式 ios::showbase 输出的数值前面带有基数符号(0或0x) ios::showpoint 显示浮点数的小数点和后面的0 ios::uppercase 用大写字母(A~F)输出十六进制数 ios::showpos 显示正数前面的“+”号 ios::scientific 按科学记数法显示浮点数 ios::fixed 用定点格式显示浮点数 ios::unitbuf 输入操作完成后立即刷新缓冲区 ios::stdio 每次输入操作完成后刷新stdout和stderr
下面举例说明标志位的用法。
【例Ex_FormatFunc】 使用格式控制成员函数
#include <iostream.h> int main() { int nNum = 12345; double dNum = 12345.6789; char *str[] = {"This", "is", "a Test!"}; cout.setf(ios::oct|ios::showbase|ios::showpos); // 设置标志:八进制,显示基和正号 cout<<nNum<<"\t"<<dNum<<endl; cout.setf(ios::hex|ios::scientific|ios::uppercase); // 设置十六进制,科学记数法和大写标志 cout<<nNum<<"\t"<<dNum<<endl; cout.fill('*'); // 设置填充符号为* for (int i=0; i<3; i++) { cout.width(12); cout<<str[i]<<" "; } cout<<endl; cout.setf(ios::left); // 设置标志:左对齐 for (i=0; i<3; i++) { cout.width(12); cout<<str[i]<<" "; } cout<<endl; return 0; }
程序运行结果如下:
030071 +12345.7
0X3039 +1.234568E+004
********This **********is *****a Test!
This******** is********** a Test!*****
2.使用格式算子
前面介绍的使用成员函数进行格式控制的方法中,每次都要使用一条语句,这样操作起来比较烦琐。为此,C++提供了一些格式算子来简化上述操作。格式算子是一个对象,可以直接用插入符或提取符来操作。C++提供的预定义格式算子如表2.2所示。
表2.2 C++预定义的格式算子
需要说明的是,若使用从resetiosflags一直到后面的格式算子,则还需在程序中包含头文件iomanip.h。下面的例子与【例Ex_FormatFunc】的运行结果完全相同,只不过是使用了格式算子。
【例Ex_Formator】 使用格式算子
#include <iostream.h> #include <iomanip.h> int main() { int nNum=12345; double dNum = 12345.6789; char *str[] = {"This", "is", "a Test!"}; cout<<setiosflags(ios::oct|ios::showbase|ios::showpos); // 设置八进制,显示基和正号 cout<<nNum<<"\t"<<dNum<<endl; cout<<setiosflags(ios::hex|ios::scientific|ios::uppercase); // 设置十六进制,科学记数法和大写标志 cout<<nNum<<"\t"<<dNum<<endl; cout<<setfill('*'); // 设置填充符号为* for (int i=0; i<3; i++) cout<<setw(12)<<str[i]<<" "; cout<<endl; cout<<setiosflags(ios::left); // 设置标志:左对齐 for (i=0; i<3; i++) cout<<setw(12)<<str[i]<<" "; cout<<endl; return 0; }
3.流的错误处理
在输入/输出过程中,一旦发现操作错误,C++流就会将发生的错误记录下来。用户可以使用C++提供的错误检测功能,检测和查明错误发生的原因和性质,然后调用clear函数清除错误状态,使流能够恢复处理。
在ios类中,定义了一个公有枚举成员io_state来记录各种错误的性质:
enum io_state { goodbit =0x00, // 正常 eofbit =0x01, // 已达到文件尾 failbit =0x02, // 操作失败 badbit =0x04 // 非法操作 };
在ios类中又定义了检测上述流状态的下列成员函数:
int ios::rdstate(); // 返回当前的流状态,它等于io_state中的枚举值 int ios::bad(); // 如果badbit位被置1,返回非0 void ios::clear(int); // 清除错误状态 int ios::eof(); // 返回非0表示提取操作已到文件尾 int ios::fail(); // 如果failbit位被置1,返回非0 int ios::good(); // 操作正常时,返回非0
可以利用上述函数来检测流是否错误,然后进行相关处理。
【例Ex_ManipError】 检测流的错误
#include <iostream.h> int main() { int i, s; char buf[80]; cout<<"输入一个整数:"; cin>>i; s = cin.rdstate(); cout<<"流状态为:"<<hex<<s<<endl; while (s) { cin.clear(); cin.getline(buf, 80); cout<<"非法输入,重新输入一个整数:"; cin>>i; s = cin.rdstate(); } return 0; }
程序运行结果如下:
输入一个整数:a↵
流状态为:2
非法输入,重新输入一个整数:abcd↵
非法输入,重新输入一个整数:12↵
该程序检测输入的数据是否为整数,若不是,则要求重新输入。需要说明的是,若输入一个浮点数,C++会自动进行类型转换,不会发生错误。只有输入字符或字符串时,才会产生输入错误,但由于cin有缓冲区,是一个缓冲流,输入的字符或字符串会暂时保存到它的缓冲区中,因此为了能继续提取用户的输入,必须先将缓冲区清空,语句“cin.getline(buf, 80);”就是起到这样的作用。如果没有这条语句,就必然会导致输入流不能正常工作,而产生死循环。
2.6.3 使用输入/输出成员函数
不同数据类型的多次输入/输出可以通过插入符“<<”和提取符“>>”来进行,但是如果想要更为细致地控制,例如希望把输入的空格作为一个字符,就需要使用istream和ostream类中的相关成员函数。
1.输入操作的成员函数
数据的输入/输出可以分为三大类:字符类、字符串和数据。
(1)使用get和getline函数
用于输入字符或字符串的成员函数get原型如下:
int get(); istream& get( char& rch ); istream& get( char* pch, int nCount, char delim = '\n' );
第1种形式是从输入流中提取一个字符,并转换成整型数值。第2种形式是从输入流中提取字符到rch中。第3种形式是从输入流中提取一个字符串并由pch返回,nCount用来指定提取字符的最多个数,delim用来指定结束字符,默认时是'\n'。
函数getline原型如下:
istream& getline( char* pch, int nCount, char delim = '\n' );
它是用来从输入流中提取一个输入行,并把提取的字符串由pch返回,nCount和delim的含义同上。这些函数可以从输入流中提取任何字符,包括空格等。
【例Ex_GetAndGetLine】 get和getline的使用
#include <iostream.h> int main() { char s1[80], s2[80], s3[80]; cout<<"请输入一个字符:"; cout<<cin.get()<<endl; cin.get(); // 提取换行符 cout<<"请输入一行字符串:"; for (int i=0; i<80; i++) { cin.get(s1[i]); if (s1[i] == '\n') { s1[i] = '\0'; break; // 退出for循环 } } cout<<s1<<endl; cout<<"请输入一行字符串:"; cin.get(s2,80); cout<<s2<<endl; cin.get(); // 提取换行符 cout<<"请输入一行字符串:"; cin.getline(s3,80); cout<<s3<<endl; return 0; }
程序运行结果如下:
请输入一个字符:A↵
65
请输入一行字符串:This is a test!↵
This is a test!
请输入一行字符串:Computer↵
Computer
请输入一行字符串:你今天过得好吗?↵
你今天过得好吗?
需要说明的是,在用get函数提取字符串时,如果遇到换行符就会结束提取,此时换行符仍保留在缓冲区中,当下次提取字符串时就会不正常,而getline在提取字符串时,换行符也会被提取,但不保存它。因此,当提取一行字符串时,最好能使用函数getline。
(2)使用read函数
read函数不仅可以读取字符或字符串(称为文本流),而且可以读取字节流。其原型如下:
istream& read( char* pch, int nCount ); istream& read( unsigned char* puch, int nCount ); istream& read( signed char* psch, int nCount );
read函数的这几种形式都是从输入流中读取由nCount指定数目的字节并将它们放在由pch或puch或psch指定的数组中。
【例Ex_Read】 read函数的使用
#include <iostream.h> int main() { char data[80]; cout<<"请输入:"<<endl; cin.read(data, 80); data[cin.gcount()] = '\0'; cout<<endl<<data<<endl; return 0; }
程序运行结果如下:
请输入:
12345↵
ABCDE↵
This is a test!↵
^Z↵
12345
ABCDE
This is a test!
其中,^Z表示用户按下【Ctrl+Z】组合键,“^Z+回车键”表示数据输入提前结束。gcount是istream类的另一个成员函数,用来返回上一次提取的字符个数。从这个例子可以看出,当用read函数读取数据时,不会因为换行符而结束读取,因此它可以读取多个行的字符串,这在许多场合下是很有用处的。
2.输出操作的成员函数
ostream类中用于输出单个字符或字节的成员函数是put和write,它们的原型如下:
ostream& put( char ch ); ostream& write( const char* pch, int nCount ); ostream& write( const unsigned char* puch, int nCount ); ostream& write( const signed char* psch, int nCount );
例如:
char data[80]; cout<<"请输入:"<<endl; cin.read(data, 80); cout.write(data,80); cout<<endl;
2.6.4 提取和插入运算符重载
C++中的一个最引人注目的特性是允许用户重载“>>”和“<<”运算符,以便用户利用标准的输入/输出流来输入/输出自己定义的数据类型(包括类),实现对象的输入/输出。
重载这两个运算符时,虽然可使用别的方法,但最好将重载声明为类的友元函数,以便能访问类中的私有成员。下面来看一个示例。
【例Ex_ExtractAndInsert】 提取和插入运算符的重载
#include <iostream.h> class CStudent { public: friend ostream&operator<<(ostream&os,CStudent&stu); friend istream&operator>>(istream&is,CStudent&stu); private: char strName[10]; // 姓名 char strID[10]; // 学号 float fScore[3]; // 三门成绩 }; ostream& operator<< ( ostream& os, CStudent& stu ) { os<<endl<<"学生信息如下:"<<endl; os<<"姓名:"<<stu.strName<<endl; os<<"学号:"<<stu.strID<<endl; os<<"成绩:"<<stu.fScore[0]<<",\t"<<stu.fScore[1]<<",\t"<<stu.fScore[2]<<endl; return os; } istream& operator>> ( istream& is, CStudent& stu ) { cout<<"请输入学生信息"<<endl; cout<<"姓名:"; is>>stu.strName; cout<<"学号:"; is>>stu.strID; cout<<"三门成绩:"; is>>stu.fScore[0]>>stu.fScore[1]>>stu.fScore[2]; return is; } int main() { CStudent one; cin>>one; cout<<one; return 0; }
程序运行结果如下:
请输入学生信息
姓名:LiMing↵
学号:20110212↵
三门成绩:80 90 75↵
学生信息如下:
姓名:LiMing
学号:20110212
三门成绩:80, 90, 75
经重载提取和插入运算符后,通过cin和cout实现了对象的直接输入和输出。
2.6.5 文件流及其处理
1.文件流概述
C++将文件看成由连续的字符(字节)的数据顺序组成的。根据文件中数据的组织方式,可分为文本文件(ASCII文件)和二进制文件。文本文件中每一个字节用以存放一个字符的ASCII码值,而二进制文件是将数据以二进制存放在文件中,它保持了数据在内存中存放的原有格式。
无论是文本文件还是二进制文件,都需要用“文件指针”来操纵。一个文件指针总是和一个文件所关联的,当文件每一次打开时,文件指针指向文件的开始,随着对文件的处理,文件指针不断地在文件中移动,并一直指向最新处理的字符(字节)位置。
文件处理有两种方式,一种称为文件的顺序处理,即从文件的第一个字符(字节)开始顺序处理到文件的最后一个字符(字节),文件指针也相应地从文件的开始位置到文件的结尾。另一种称为文件的随机处理,即在文件中通过C++相关的函数移动文件指针,并指向所要处理的字符(字节)位置。按这两种处理方式,可将文件相应地称为顺序文件和随机文件。
为方便用户对文件的操作,C++提供了文件操作的文件流库,它的体系结构如图2.3所示。其中,ifstream类是从istream类公有派生而来的,用来支持从输入文件中提取数据的各种操作。ofstream类是从ostream类公有派生而来的,用来实现把数据写入文件中的各种操作。fstream类是从iostream类公有派生而来的,提供从文件中提取数据或把数据写入文件的各种操作。filebuf类是从streambuf类派生而来的,用来管理磁盘文件的缓冲区,应用程序中一般不涉及该类。
图2.3 C++的文件流库
在使用上述类的成员函数进行文件操作时,需要在程序中包含头文件fstream.h。文件操作一般是按打开文件、读写文件、关闭文件这3个步骤进行的。
2.顺序文件操作
文件的顺序处理是文件操作中最简单的一种方式。在C++中打开或创建一个指定的文件需要下列两个步骤:
(1)声明一个ifstream、ofstream或fstream类对象。例如:
ifstream infile; // 声明一个输入(读)文件流对象 ofstream outfile; // 声明一个输出(写)文件流对象 fstream iofile; // 声明一个可读可写的文件流对象
(2)使用文件流类的成员函数打开或创建一个指定的文件,使得该文件与声明的文件流对象联系起来,这样对流对象的操作也就是对文件的操作。例如:
infile.open("file1.txt"); outfile.open("file2.txt"); iofile.open("file3.txt",ios::in | ios::out);
上述这两步操作也可合为一步进行,即在声明对象时指定文件名。例如:
ifstream infile("file1.txt"); ofstream outfile("file2.txt"); fstream iofile("file3.txt",ios::in | ios::out);
事实上,ifstream、ofstream或fstream类构造函数中总有一种原型和它的成员函数open功能相同。它们的函数原型如下:
ifstream( const char* szName, int nMode = ios::in, int nProt = filebuf::openprot ); void ifstream::open( const char* szName, int nMode = ios::in, int nProt = filebuf::openprot ); ofstream( const char* szName, int nMode = ios::out, int nProt = filebuf::openprot ); void ofstream::open( const char* szName, int nMode = ios::out, int nProt = filebuf::openprot ); fstream( const char* szName, int nMode, int nProt = filebuf::openprot ); void fstream::open( const char* szName, int nMode, int nProt = filebuf::openprot );
其中,参数szName用来指定要打开的文件名,包括路径和扩展名,Mode指定文件的访问方式,表2.3列出了open函数可以使用的访问方式。参数Prot用来指定文件的共享方式,默认时是filebuf::openprot,表示DOS兼容的方式。
表2.3 文件访问方式
需要说明的是,nMode指定文件的访问方式通过“|”(或)运算组合而成。其中,ios::trunc方式将消除文件原有内容,在使用时要特别小心,它通常与ios::out、ios::ate、ios::app和ios:in进行“|”组合,如ios::out| ios::trunc。
ios::binary是二进制文件方式,通常可以有这样的组合:
ios::in|ios::binary 表示打开一个只读的二进制文件 ios::out|ios::binary 表示打开一个可写的二进制文件 ios::in|ios::out|ios::binary 表示打开一个可读可写的二进制文件
在使用文件过程中,一定不要忘记:当文件使用结束后要及时调用close函数关闭,以防止文件再被“误用”。
当文件打开后,就可以对文件进行读写操作。若从一个文件中读出数据,可以使用get、getline、read函数及提取符“>>”;而向一个文件写入数据,可以使用put、write函数及插入符“<<”。下面举例来说明文件的操作过程和方法。
【例Ex_File】 将文件内容保存在另一个文件中,并将内容显示在屏幕上
#include <iostream.h> #include<fstream.h> // 文件操作所必需的头文件 int main() { fstream file1; // 定义一个fstream类的对象用于读 file1.open("Ex_DataFile.txt", ios::in); if (!file1) { cout<<"Ex_DataFile.txt不能打开!\n"; return; } fstream file2; // 定义一个fstream类的对象用于写 file2.open("Ex_DataFileBak.txt", ios::out | ios::trunc); if (!file2) { cout<<"Ex_DataFileBak.txt不能创建!\n"; file1.close(); return; } char ch; while (!file1.eof()) { file1.read(&ch,1); cout<<ch; file2.write(&ch,1); } file2.close(); // 不要忘记文件使用结束后要及时关闭 file1.close(); return 0; }
上述程序中,eof是ios类的成员函数,当到达文件的末尾时,它将返回true(真)。
3.随机文件操作
随机文件提供在文件中来回移动文件指针和非顺序地读写文件的能力,这样在读写磁盘文件某一数据以前无须读写其前面的数据,从而能快速地检索、修改和删除文件中的信息。
C++中顺序文件和随机文件间的差异不是物理的,这两种文件都是以顺序字符流的方式将信息写在磁盘等存储介质上,其区别仅在于文件的访问和更新的方法。在以随机的方式访问文件时,文件中的信息在逻辑上组织成定长的记录格式。所谓定长的记录格式是指文件中的数据被解释成C++的同一种类型的信息的集合,例如都是整型数或者都是用户所定义的某一种结构的数据等。这样就可以通过逻辑的方法,将文件指针直接移动到所读写数据的起始位置,来读取数据或者将数据直接写到文件的这个位置上。
在以随机的方式读写文件时,同样必须首先打开文件,且随机方式和顺序方式打开文件所用的函数也完全相同,但随机方式的文件流的打开模式必须同时有ios::in|ios::out。
在文件打开时,文件指针指向文件的第一个字符(字节)。当然,可根据具体的读写操作使用C++提供的seekg和seekp函数将文件指针移动到指定的位置。它们的原型如下:
istream&seekg(long pos); istream&seekg(long off, ios::seek_dir dir); ostream&seekp(long pos); ostream&seekp(long off, ios::seek_dir dir);
其中,pos用来指定文件指针的绝对位置。而off用来指定文件指针的相对偏移时,文件指针的最后位置还依靠dir的值。dir值可以是:
ios::beg 从文件流的头部开始 ios::cur 从当前的文件指针位置开始 ios::end 从文件流的尾部开始
【例Ex_FileSeek】 使用seekp指定文件指针的位置
#include <iostream.h> #include <iomanip.h> #include <fstream.h> #include <string.h> class CStudent { public: CStudent(char* name, char* id, float score = 0); void print(); friend ostream& operator<< ( ostream& os, CStudent& stu ); friend istream& operator>> ( istream& is, CStudent& stu ); private: char strName[10]; // 姓名 char strID[10]; // 学号 float fScore; // 成绩 }; CStudent::CStudent(char* name, char* id, float score) { strncpy(strName,name,10); strncpy(strID,id,10); fScore=score; } void CStudent::print() { cout<<endl<<"学生信息如下:"<<endl; cout<<"姓名:"<<strName<<endl; cout<<"学号:"<<strID<<endl; cout<<"成绩:"<<fScore<<endl; } ostream& operator<< ( ostream& os, CStudent& stu ) { os.write(stu.strName,10); os.write(stu.strID,10); os.write((char*)&stu.fScore,4); return os; } istream& operator>> ( istream& is, CStudent& stu ) { char name[10]; char id[10]; is.read(name,10); is.read(id,10); is.read((char*)&stu.fScore,4); strncpy(stu.strName, name, 10); strncpy(stu.strID, id, 10); return is; } int main() { CStudent stu1("MaWenTao","99001",88); CStudent stu2("LiMing","99002",92); CStudent stu3("WangFang","99003",89); CStudent stu4("YangYang","99004",90); CStudent stu5("DingNing","99005",80); fstream file1; file1.open("student.dat",ios::out|ios::in|ios::binary); file1<<stu1<<stu2<<stu3<<stu4<<stu5; CStudent* one = new CStudent("",""); const int size = sizeof(CStudent); file1.seekp(size*4); file1>>*one; one->print(); file1.seekp(size*1); file1>>*one; one->print(); file1.seekp(size*2, ios::cur); file1>>*one; one->print(); file1.close(); delete one; return 0; }
程序运行结果如下:
学生信息如下:
姓名:DingNing
学号:99005
成绩:80
学生信息如下:
姓名:LiMing
学号:99002
成绩:92
学生信息如下:
姓名:DingNing
学号:99005
成绩:80
程序先将五个学生记录保存到文件中,然后移动文件指针,读取相应的记录,最后将数据输出到屏幕上。需要说明的是,由于文件流file1既可以读(ios::in)也可以写(ios::out),因此用seekg代替程序中的seekp,其结果也是一样的。
以上是C++的面向对象、输入/输出和模板的相关内容,但实际上由于Windows操作系统机制的引入,标准C++远不能满足Windows程序设计的需要。为此,Visual C++ 6.0针对其操作系统,提供了许多高效、实用的方法和技术,从下一章起将着重讨论这方面的内容。