C++I/O标准库详解

十三、C++输入输出详解:C++标准输入输出流、文件流、字符串流

13.1 C++输入和输出的概念

我们经常用到的输入和输出,都是以终端为对象的,即从键盘输入数据,运行结果输出到显示器屏幕上。从操作系统的角度看,每一个与主机相连的输入输出设备都被看作一个文件。除了以终端为对象进行输入和输出外,还经常用磁盘(光盘)作为输入输出对象,磁盘文件既可以作为输入文件,也可以作为输出文件。

程序的输入指的是从输入文件将数据传送给程序,程序的输出指的是从程序将数据传送给输出文件。

C++输入输出包含以下三个方面的内容:
  1. 对系统指定的标准设备的输入和输出。即从键盘输入数据,输出到显示器屏幕。这种输入输出称为标准的输入输出,简称标准I/O。

  2. 以外存磁盘文件为对象进行输入和输出,即从磁盘文件输入数据,数据输出到磁盘文件。以外存文件为对象的输入输出称为文件的输入输出,简称文件I/O。

  3. 对内存中指定的空间进行输入和输出。通常指定一个字符数组作为存储空间(实际上可以利用该空间存储任何信息)。这种输入和输出称为字符串输入输出,简称串I/O。


C++采取不同的方法来实现以上种输入输出。为了实现数据的有效流动,C++系统提供了庞大的I/O类库,调用不同的类去实现不同的功能。

在C语言中,用printf和scanf进行输入输出,往往不能保证所输入输出的数据是可靠的安全的。 在C++的输入输出中,编译系统对数据类型进行严格的检查,凡是类型不正确的数据都不可能通过编译。因此C++的I/O操作是类型安全(type safe)的。C++的I/O操作是可扩展的,不仅可以用来输入输出标准类型的数据,也可以用于用户自定义类型的数据。C++对标准类型的数据和对用户声明类型数据的输入输出,采用同样的方法处理。C++通过I/O类库来实现丰富的I/O功能。C++的输入输出优于C语言中的printf和scanf,但是比较复杂,要掌握许多细节。

C++的I/O对C的发展--类型安全和可扩展性

在C语言中,用primf和scanf进行输人输出,往往不能保证所输入输出的数据是可靠的、安全的。学过C语言的读者可以分析下面的用法,想用格式符%d输出一个整数, 但不小心用它输出了单精度变量和字符串,会出现什么情况?假定所用的系统int型占两个字节。
   printf("%d", i);  // i为整型变量,正确,输出i的值
   printf("%d", f);  // f为单精度变量,输出f变量中前两个字节的内容
   printf("%d", "C++"); //输出宇符串"C++"的地址

编译系统认为以上语句都是合法的,而不对数据类型的合法性进行检查,显然所得到的结果不是人们所期望的,在用scanf 输入时,有时出现的问题是很隐蔽的。如:
   scanf("%d", &i);  //正确,输入一个整数,赋给整型变量i
   scanf("%d", i);  //漏写&

假如已有声明语句“int i = 1; ”,定义i为整型变量,其初值为1。编译系统不认为上面的scanf语句出错,而是将输人的值存放到地址为000001的内存单元中,这个错误可能产生严重的后果。

C++为了与C兼容,保留了用printf和scanf进行输出和输人的方法,以便使过去所编写的大量的C程序仍然可以在C ++的环境下运行,但是希望读者在编写新的C ++程 序时不要用C的输入输出机制,而要用C++自己特有的输人输出方法。在C++的输入输出中,编译系统对数据类型进行严格的检查,凡是类型不正确的数据都不可能通过编译。因此C++的I/O操作是类型安全(type safe)的。

此外,用printf和scanf可以输出和输入标准类型的数据(如int、float、double、char), 但无法输出用户自己声明的类型(如数组、结构体、类)的数据。在C++中,会经常遇到对类对象的输人输出,显然无法使用printf和scanf来处理。C++的I/O操作是可扩展 的,不仅可以用来输人输出标准类型的数据,也可以用于用户自定义类型的数据。C++对标准类型的数据和对用户声明类型数据的输人输出,采用同样的方法处理。显然,在用户声明了一个新类后,是无法用printf和scanf 函数直接输出和输人这个类的对象的。

可扩展性是C++输人输出的重要特点之一,它能提高软件的重用性,加快软件的开 发过程。

C++通过I/O类库来实现丰富的I/O功能。这样使C++的输人输出明显地优于C 语言中的printf和scanf,但是也为之付出了代价,C++的I/O系统变得比较复杂,要掌握许多细节。在本章中只能介绍其基本的概念和基本的操作,有些具体的细节可在日后实际深入应用时再进一步掌握。

13.2 与C++输入输出有关的类和对象

输入和输出是数据传送的过程,数据如流水一样从一处流向另一处。C++形象地将此过程称为流(Stream)。C++的输入输出流是指由若干字节组成的宇节序列,这些宇节中的数据按顺序从一个对象传送到另一对象。流表示了信息从源到目的端的流动。在输入操作时,字节流从输入设备(如键盘、磁盘)流向内存,在输出操作时,字节流从内存流向输出设备(如屏幕、打印机、磁盘等)。流中的内容可以是ASCII字符、二进制形式的数据、图形图像、数字音频视频或其他形式的信息。

实际上,在内存中为每一个数据流开辟一个内存缓冲区,用来存放流中的数据。当用cout和插入运算符“<<”向显示器输出数据时,先将这些数据送到程序中的输出缓冲区保存,直到缓冲区满了或遇到endl,就将缓冲区中的全部数据送到显示器显示出来。在输入时,从键盘输入的数据先放在键盘的缓冲区中,当按回车键时,键盘缓冲区中的数据输入到程序中的输入缓冲区,形成cin流,然后用提取运算符“ >>”从输入缓冲区中提取数据送给程序中的有关变量。总之,流是与内存缓冲区相对应的,或者说,缓冲区中的数据就是流。

在C++中,输入输出流被定义为类。C++的I/O库中的类称为流类(stream class)。 用流类定义的对象称为流对象。

其实,cout和cin并不是C++语言中提供的语句,它们是iostream类的对象,在不了解类和对象时,在不致引起误解的前提下,为叙述方便,把它们称为cout语句和cin语句。正如C++并未提供赋值语句,只提供赋值表达式,在赋值表达式后面加分号就成了C++的语句,为方便起见,我们习惯称之为赋值语句。又如,在C语言中常用 printf和scanf进行输出和输入,printf和scanf是C语言库函数中的输入输出函数,一般也习惯地将由printf和scanf函数构成的语句称为printf语句和scanf语句。在使用它们时,对其本来的概念应该有准确的理解。

了解了类和对象后,我们对C++的输入输出应当有更深刻的认识。

C++编译系统提供了用于输入输出的iostream类库。iostream这个单词是由3个部 分组成的,即i-o-stream,意为输入输出流。在iostream类库中包含许多用于输入输出的 类。常用的见表13.1。


图 13.1

表13.1 I/O类库中的常用流类
类名 作用 在哪个头文件中声明
ios 抽象基类 iostream
istream
ostream
iostream
通用输入流和其他输入流的基类
通用输出流和其他输出流的基类
通用输入输出流和其他输入输出流的基类
iostream
iostream
iostream
ifstream
ofstream
fstream
输入文件流类
输出文件流类
输入输出文件流类
fstream
fstream
fstream
istrstream
ostrstream
strstream
输入字符串流类
输出字符串流类
输入输出字符串流类
strstream
strstream
strstream

ios是抽象基类,由它派生出istream类和ostream类,两个类名中第1个字母i和o分别代表输入(input)和输出(output)。istream类支持输入操作,ostream类支持输出操作, iostream类支持输入输出操作。iostream类是从istream类和ostream类通过多重继承而派生的类。其继承层次见图13.1表示。

C++对文件的输入输出需要用ifstrcam和ofstream类,两个类名中第1个字母i和o分别代表输入和输出,第2个字母f代表文件(file)。ifstream支持对文件的输入操作, ofstream支持对文件的输出操作。类ifstream继承了类istream,类ofstream继承了类ostream,类fstream继承了类iostream。见图 13.2。


图 13.2

I/O类库中还有其他一些类,但是对于一般用户来说,以上这些已能满足需要了。如果想深入了解类库的内容和使用,可参阅所用的C++系统的类库手册。

与iostream类库有关的头文件

iostream类库中不同的类的声明被放在不同的头文件中,用户在自己的程序中用#include命令包含了有关的头文件就相当于在本程序中声明了所需要用到的类。可以换 —种说法:头文件是程序与类库的接口,iostream类库的接口分别由不同的头文件来实现。常用的有
  • iostream  包含了对输入输出流进行操作所需的基本信息。

  • fstream  用于用户管理的文件的I/O操作。

  • strstream  用于字符串流I/O。

  • stdiostream  用于混合使用C和C + +的I/O机制时,例如想将C程序转变为C++程序。

  • iomanip  在使用格式化I/O时应包含此头文件。

在iostream头文件中定义的流对象

在 iostream 头文件中定义的类有 ios,istream,ostream,iostream,istream _withassign, ostream_withassign,iostream_withassign 等。

iostream.h包含了对输入输出流进行操作所需的基本信息。因此大多数C++程序都包括iostream.h。在iostream.h头文件中不仅定义了有关的类,还定义了4种流对象, 见表13.2。

表13.2 文件中定义的4种流对象
对象 含义 对应设备 对应的类 c语言中相应的标准文件
cin 标准输入流 键盘 istream_withassign stdin
cout 标准输出流 屏幕 ostream_withassign stdout
cerr 标准错误流 屏幕 ostream_withassign stderr
clog 标准错误流 屏幕 ostream_withassign stderr

在iostream头文件中定义以上4个流对象用以下的形式(以cout为例):
   ostream cout ( stdout);
在定义cout为ostream流类对象时,把标准输出设备stdout作为参数,这样它就与标准输出设备(显示器)联系起来,如果有
   cout <<3;
就会在显示器的屏幕上输出3。

在iostream头文件中重载运算符

“<<”和“>>”本来在C++中是被定义为左位移运算符和右位移运算符的,由于在iostream头文件中对它们进行了重载,使它们能用作标准类型数据的输入和输出运算符。所以,在用它们的程序中必须用#include命令把iostream包含到程序中。
   #include

在istream和ostream类(这两个类都是在iostream中声明的)中分别有一组成员函数对位移运算符“<<”和“>>”进行重载,以便能用它输入或输出各种标准数据类型的数据。对于不同的标准数据类型要分别进行重载,如:
   ostream operator << (im );  //用于向输出流插入一个int数据
   ostream operator << (float );  //用于向输出流插入一个float数据
   ostream operator << (char); //用于向输出流插入一个char数据
   ostream operator << (char * ); //用于向输出流插入一个字符串数据
等。如果在程序中有下面的表达式:
   cout<<"C++";
实际上相当于:
   cout.operator <<("C++")

"C ++"的值是其首字节地址,是字符型指针(char * )类型,因此选择调用上面最后一个运算符重载函数,通过重载函数的函数体,将字符串插入到cout流中,函数返回流对象cout。

在istream类中已将运算符“>> ”重载为对以下标准类型的提取运算符:char, signed char, unsigned char, short, unsigned short, int, unsigned int,long, unsigned long, float, double, long double, char * , signed char *, unsigned char * 等。

在ostream类中将“<<”重载为插入运算符,其适用类型除了以上的标准类型外,还增加了一个 void * 类型。

如果想将“<<”和“>>”用于自己声明的类型的数据,就不能简单地采用包含 iostream头文件来解决,必须自己对“<<”和“>>”进行重载。

怎样理解运算符“<<”和“>>”的作用呢?有一个简单而形象的方法:它们指出了数据移动的方向,例如
   >>a
箭头方向表示把数据放人a中。而
   < 箭头方向表示从a中拿出数据。

13.3 C++标准输出流详解

标准输出流是流向标准输出设备(显示器)的数据。ostream类定义了个输出流对象,即cout、cerr、clog,分述如下。

cout流对象

cont是console output的缩写,意为在控制台(终端显示器)的输出。前边已对cout作了一些介绍(详情请查看: 与C++输入输出有关的类和对象),在此再强调几点。

1) cout不是C++预定义的关键字,它是ostream流类的对象,在iostream中定义。 顾名思义,流是流动的数据,cout流是流向显示器的数据。cout流中的数据是用流插入运算符“<<”顺序加入的。如果有
   cout<<"I "<<"study C++ "<<"very hard.";
按顺序将字符串"I ", "study C++ ", "very hard."插人到cout流中,cout就将它们送到显示器,在显示器上输出字符串"I study C++ very hard."。cout流是容纳数据的载体,它并不是一个运算符。人们关心的是cout流中的内容,也就是向显示器输出什么。

2)用“ccmt<<”输出基本类型的数据时,可以不必考虑数据是什么类型,系统会判断数据的类型,并根据其类型选择调用与之匹配的运算符重载函数。这个过程都是自动的,用户不必干预。如果在C语言中用prinf函数输出不同类型的数据,必须分别指定相应的输出格式符,十分麻烦,而且容易出错。C++的I/O机制对用户来说,显然是方便而安全的。

3) cout流在内存中对应开辟了一个缓冲区,用来存放流中的数据,当向cout流插 人一个endl时,不论缓冲区是否已满,都立即输出流中所有数据,然后插入一个换行符, 并刷新流(清空缓冲区)。注意如果插人一个换行符”\n“(如cout<
4) 在iostream中只对"<<"和">>"运算符用于标准类型数据的输入输出进行了重载,但未对用户声明的类型数据的输入输出进行重载。如果用户声明了新的类型,并希望用"<<"和">>"运算符对其进行输入输出,应该按照前面介绍的方法(详情请查看: C++运算符重载),对"<<"和">>"运算符另作重载。

cerr流对象

cerr流对象是标准错误流,cerr流已被指定为与显示器关联。cerr的作用是向标准错误设备(standard error device)输出有关出错信息。cerr与标准输出流cout的作用和用法差不多。但有一点不同:cout流通常是传送到显示器输出,但也可以被重定向输出到磁盘文件,而cerr流中的信息只能在显示器输出。当调试程序时,往往不希望程序运行时的出错信息被送到其他文件,而要求在显示器上及时输出,这时应该用cerr。cerr流中的信息是用户根据需要指定的。

[例13.1] 有一元二次方程ax 2+bx+c=0,其一般解为

但若a=0,或b 2-4ac<0时,用此公式出错。

编程序,从键盘输入a、b、c的值,求x 1和x 2。如果a=0或b 2-4ac<0,输出出错信息。
#include  #include  using namespace std; int main( ) { float a,b,c,disc; cout<<"please input a,b,c:"; cin>>a>>b>>c; if (a==0) cerr<<"a is equal to zero,error!"<
运行情况如下:
①please input a,b,c: 0 2 3↙
a is equal to zero,error!

②please input a,b,c: 5 2 3↙
sc=b*b-4*a*c<0

③please input a,b,c: 1  2.5   1.5↙
x1=-1
x2=-1.5

clog流对象

clog流对象也是标准错误流,它是console log的缩写。它的作用和cerr相同,都是在终端显示器上显示出错信息。区别:cerr是不经过缓冲区,直接向显示器上输出有关信息,而clog中的信息存放在缓冲区中,缓冲区满后或遇endl时向显示器输出。

13.4 C++格式化输出,C++输出格式控制

在输出数据时,为简便起见,往往不指定输出的格式,由系统根据数据的类型采取默认的格式,但有时希望数据按指定的格式输出,如要求以十六进制或八进制形式输出一个 整数,对输出的小数只保留两位小数等。有两种方法可以达到此目的。一种是我们已经介绍过的使用控制符的方法(详情请查看:C++输入cout与输出cin);第2种是使用流对象的有关成员函数。分别叙述如下。

使用控制符控制输出格式

控制格式的使用方法这里不再赘述,仅举例说明,详情请查看:C++输入cout与输出cin。

[例13.2] 用控制符控制输出格式。

#include  #include //不要忘记包含此头文件 using namespace std; int main() { int a; cout<<"input a:"; cin>>a; cout<<"dec:"<

运行结果如下:
input a:34↙(输入a的值)
dec:34                   (十进制形式)
hex:22                   (十六进制形式)
oct:42                   (八进制形式)
        China               (域宽为)
*****China               (域宽为,空白处以'*'填充)
pi=3.14285714e+00         (指数形式输出,8位小数)
pi=3.1429e+00             (指数形式输出,4位小数)
pi=3.143                 (小数形式输出,精度仍为)

用流对象的成员函数控制输出格式

除了可以用控制符来控制输出格式外,还可以通过调用流对象cout中用于控制输出格式的成员函数来控制输出格式。用于控制输出格式的常用的成员函数见表13.4。

表13.4 用于控输出格式的流成员函数
流成员函数 与之作用相同的控制符 作用
precision(n) setprecision(n) 设置实数的精度为n位
width(n) setw(n) 设置字段宽度为n位
fill(c) setfill(c) 设置填充宇符c
setf() setiosflags() 设置输出格式状态,括号中应给出格式状态,内容与控制符setiosflags括号中的内容相同,如表13.5所示
unsetf() resetioflags() 终止已设置的输出格式状态,在括号中应指定内容


流成员函数setf和控制符setiosflags括号中的参数表示格式状态,它是通过格式标志来指定的。格式标志在类ios中被定义为枚举值。因此在引用这些格式标志时要在前面加上类名ios和域运算符“::”。格式标志见表13.5。

表13.5 设置格式状态的格式标志
格式标志 作用
ios::left 输出数据在本域宽范围内向左对齐
ios::right 输出数据在本域宽范围内向右对齐
ios::internal 数值的符号位在域宽内左对齐,数值右对齐,中间由填充字符填充
ios::dec 设置整数的基数为10
ios::oct 设置整数的基数为8
ios::hex 设置整数的基数为16
ios::showbase 强制输出整数的基数(八进制数以0打头,十六进制数以0x打头)
ios::showpoint 强制输出浮点数的小点和尾数0
ios::uppercase 在以科学记数法格式E和以十六进制输出字母时以大写表示
ios::showpos 对正数显示“+”号
ios::scientific 浮点数以科学记数法格式输出
ios::fixed 浮点数以定点格式(小数形式)输出
ios::unitbuf 每次输出之后刷新所有的流
ios::stdio 每次输出之后清除stdout, stderr


[例13.3] 用流控制成员函数输出数据。

#include  using namespace std; int main( ) { int a=21 cout.setf(ios::showbase);//显示基数符号(0x或) cout<<"dec:"<

运行情况如下:
dec:21(十进制形式)
hex:0x15                 (十六进制形式,以x开头)
oct:025                  (八进制形式,以开头)
        China               (域宽为)
*****China               (域宽为,空白处以'*'填充)
pi=**3.142857e+00         (指数形式输出,域宽,默认位小数)
+***3.142857             (小数形式输出,精度为,最左侧输出数符“+”)

对程序的几点说明:
1) 成员函数width(n)和控制符setw(n)只对其后的第一个输出项有效。如:
   cout. width(6);
   cout <<20 <<3.14< 输出结果为 203.14

在输出第一个输出项20时,域宽为6,因此在20前面有4个空格,在输出3.14时,width (6)已不起作用,此时按系统默认的域宽输出(按数据实际长度输出)。如果要求在输出数据时都按指定的同一域宽n输出,不能只调用一次width(n),而必须在输出每一项前都调用一次width(n>,上面的程序中就是这样做的。

2) 在表13.5中的输出格式状态分为5组,每一组中同时只能选用一种(例如dec、hex和oct中只能选一,它们是互相排斥的)。在用成员函数setf和控制符setiosflags设置输出格式状态后,如果想改设置为同组的另一状态,应当调用成员函数unsetf(对应于成员函数self)或resetiosflags(对应于控制符setiosflags),先终止原来设置的状态。然后再设置其他状态,大家可以从本程序中看到这点。程序在开始虽然没有用成员函数self和控制符setiosflags设置用dec输出格式状态,但系统默认指定为dec,因此要改变为hex或oct,也应当先用unsetf 函数终止原来设置。如果删去程序中的第7行和第10行,虽然在第8行和第11行中用成员函数setf设置了hex和oct格式,由于未终止dec格式,因此hex和oct的设置均不起作用,系统依然以十进制形式输出。

同理,程序倒数第8行的unsetf 函数的调用也是不可缺少的。

3) 用setf 函数设置格式状态时,可以包含两个或多个格式标志,由于这些格式标志在ios类中被定义为枚举值,每一个格式标志以一个二进位代表,因此可以用位或运算符“|”组合多个格式标志。如倒数第5、第6行可以用下面一行代替:
   cout.setf(ios::internal I ios::showpos);  //包含两个状态标志,用"|"组合

4) 可以看到:对输出格式的控制,既可以用控制符(如例13.2),也可以用cout流的有关成员函数(如例13.3),二者的作用是相同的。控制符是在头文件iomanip中定义的,因此用控制符时,必须包含iomanip头文件。cout流的成员函数是在头文件iostream 中定义的,因此只需包含头文件iostream,不必包含iomanip。许多程序人员感到使用控制符方便简单,可以在一个cout输出语句中连续使用多种控制符。

13.5 用C++流成员函数put输出单个字符

在程序中一般用cout和插入运算符“<<”实现输出,cout流在内存中有相应的缓冲区。有时用户还有特殊的输出要求,例如只输出一个字符。ostream类除了提供上面介绍过的用于格式控制的成员函数外,还提供了专用于输出单个字符的成员函数put。如:
   cout.put('a');
调用该函数的结果是在屏幕上显示一个字符a。put函数的参数可以是字符或字符的ASCII代码(也可以是一个整型表达式)。如
   cout.put(65 + 32);
也显示字符a,因为97是字符a的ASCII代码。

可以在一个语句中连续调用put函数。如:
   cout.put(71).put(79).put(79). put(68).put('\n');
在屏幕上显示GOOD。

[例13.4] 有一个字符串"BASIC",要求把它们按相反的顺序输出。

#include  using namespace std; int main( ) { char *a="BASIC";//字符指针指向'B' for(int i=4;i>=0;i--) cout.put(*(a+i)); //从最后一个字符开始输出 cout.put('\n'); return 0; }

运行时在屏幕上输出:
CISAB

除了使用cout.put函数输出一个字符外,还可以用putchar函数输出一个字符。putchar函数是C语言中使用的,在stdio.h头文件中定义。C++保留了这个函数,在iostream头文件中定义。

[例13.4] 也可以改用putchar函数实现。

复制纯文本新窗口
#include  //也可以用#include ,同时不要下一行 using namespace std; int main( ) { char *a="BASIC"; for(int i=4;i>=0;i--) putchar(*(a+i)); putchar('\n'); }

运行结果与前相同。

成员函数put不仅可以用cout流对象来调用,而且也可以用ostream类的其他流对象调用。  

13.6 C++ cin输入流详解

标准输入流是从标准输入设备(键盘)流向程序的数据。在头文件iostream.h中定义了cin、cout、cerr、clog 4个流对象(详情请查看:与C++输入输出有关的类和对象),cin是输入流,cout、cerr、clog是输出流。

cin是istream类的对象,它从标准输入设备(键盘)获取数据,程序中的变量通过流提取符“>>”从流中提取数据。流提取符“>>”从流中提取数据时通常跳过输入流中的空格、tab键、换行符等空白字符。

注意:只有在输入完数据再按回车键后,该行数据才被送入键盘缓冲区,形成输入流,提取运算符“>>”才能从中提取数据。需要注意保证从流中读取数据能正常进行。

例如:
   int a,b;
   cin>>a>>b;
若从键盘上输入
   21 abc↙
变量a从输入流中提取整数21,提取操作成功,此时cin流处于正常状态。但在变量b准备提取一个整数时,遇到了字母a,显然提取操作失败了,此时,cin流被置为出错状态。只有在正常状态时,才能从输入流中提取数据。

当遇到无效字符或遇到文件结束符(不是换行符,是文件中的数据已读完)时,输入流cin就处于出错状态,即无法正常提取数据。此时对cin流的所有提取操作将终止。在 IBM PC及其兼容机中,以Ctrl + Z表示文件结束符。在UNIX和Macintosh系统中,以 Ctrl + D表示文件结束符。当输入流cin处于出错状态时,如果测试cin的值,可以发现它的值为false(假),即cin为0值。如果输入流在正常状态,cin的值为true(真),即cin为 一个非0值。可以通过测试cin的值,判断流对象是否处于正常状态和提取操作是否成功。如:
   if(!cn) //流cin处于出销状态,无法正常提取数据
       cout<<"error";

[例13.5] 通过测试cin的真值,判断流对象是否处于正常状态。

#include  using namespace std; int main( ) { float grade; cout<<"enter grade:"; while(cin>>grade)//能从cin流读取数据 { if(grade>=85) cout<

流提取符“>>”不断地从流中提取数据(每次提取一个浮点数),如果成功,就陚给 grade,此时cin为真,若不成功则cin为假。如果键入文件结束符,表示数据已完。

运行情况如下:
enter grade: 67↙
enter grade: 89↙
89 GOOD!
enter grade: 56↙
56 fail!
enter grade: 100↙
100 GOOD!
enter grade: ^Z↙//键入文件结束符
The end.

在遇到文件结束符时,程序结束。如果某次输入的数据为
   enter grade: 100/2↙
流提取符">>"提取100,赋给grade,进行if语句的处理。然后再遇到"/",认为是无效字符,cin返回0。循环结束,输出"The end."。

在不同的C++系统下运行此程序,在最后的处理上有些不同。以上是在GCC环境下运行程序的结果,如果在VC++环境(笔者使用win7系统)下运行此程序,在键入Ctrl + Z 时,需要敲击两次回车,程序才输出"The end."。

13.7 C++ get()函数读入一个字符

get()函数是cin输入流对象的成员函数,它有3种形式:无参数的,有一个参数的,有3个参数的。

关于cin输入流的更多信息请查看: 与C++输入输出有关的类和对象 和  C++ cin输入流详解

1) 不带参数的get函数
其调用形式为
   cin.get()
用来从指定的输入流中提取一个字符(包括空白字符),函数的返回值就是读入的字符。 若遇到输入流中的文件结束符,则函数值返回文件结束标志EOF(End Of File),一般以-1代表EOF,用-1而不用0或正值,是考虑到不与字符的ASCII代码混淆,但不同的C ++系统所用的EOF值有可能不同。

[例13.6] 用get函数读入字符。
#include  using namespace std; int main( ) { int c; cout<<"enter a sentence:"<
运行情况如下:
enter a sentence:
I study C++ very hard.↙(输入一行字符)
I study C++ very hard.               (输出该行字符)
^Z↙(程序结束)

C语言中的getchar函数与流成员函数cin.get( )的功能相同,C++保留了C的这种用法,可以用getchar(c)从键盘读入一个字符赋给c。

2) 有一个参数的get函数
其调用形式为
   cin.get(ch)
其作用是从输入流中读取一个字符,赋给字符变量ch。如果读取成功则函数返回true(真),如失败(遇文件结束符) 则函数返回false(假)。例13.6可以改写如下:
#include  using namespace std; int main( ) { char c; cout<<"enter a sentence:"<

3) 有3个参数的get函数
其调用形式为
   cin.get(字符数组, 字符个数n, 终止字符)

   cin.get(字符指针, 字符个数n, 终止字符)
其作用是从输入流中读取n-1个字符,赋给指定的字符数组(或字符指针指向的数组),如果在读取n-1个字符之前遇到指定的终止字符,则提前结束读取。如果读取成功则函数返回true(真),如失败(遇文件结束符) 则函数返回false(假)。再将例13.6改写如下:
#include  using namespace std; int main( ) { char ch[20]; cout<<"enter a sentence:"<
运行情况如下:
enter a sentence:
I study C++ very hard.↙
I study

在输入流中有22个字符,但由于在get函数中指定的n为10,读取n -1个(即9个)字符并赋给字符数组ch中前9个元素。有人可能要问:指定n-10,为什么只读取9个字符呢?因为存放的是一个字符串,因此在9个字符之后要加入一个字符串结束标志,实际上存放到数组中的是10个字符。请读者思考:如果不加入字符串结束标志,会 出现什么情况?结果是:在用“cout<
如果输入↙
   abcde
即未读完第9个字符就遇到终止字符、读取操作终止,前5个字符已存放到数组ch[0]到ch[4]中,ch[5]中存放'\0'。

如果在get函数中指定的n为20,而输入22个字符,则将输入流中前19个字符赋给字符数组ch中前19个元素,再加入一个'\0'。

get函数中第3个参数可以省写,此时默认为'\n'。下面两行等价:
   cin.get(ch,10,'\\n');
   cin.get(ch,10);
终止字符也可以用其他字符。如
   cin.get(ch,10,'x');
在遇到字符'x'时停止读取操作。

13.8 C++ getline()函数读入一行字符

getline函数的作用是从输入流中读取一行字符,其用法与带3个参数的get函数类似。即
   cin.getline(字符数组(或字符指针), 字符个数n, 终止标志字符)

[例13.7] 用getline函数读入一行字符。
复制纯文本新窗口
#include  using namespace std; int main( ) { char ch[20]; cout<<"enter a sentence:"<>ch; cout<<"The string read with cin is:"<
程序运行情况如下:
enter a sentence: I like C++./I study C++./I am happy.↙
The string read with cin is:I
The second part is: like C++.
The third part is:I study C++./I am h

请仔细分析运行结果。用“cin>>”从输入流提取数据,遇空格就终止。因此只读取 一个字符'I',存放在字符数组元素ch[0]中,然后在ch[1]中存放'\0'。因此用"cout<
有几点说明并请读者思考:
1) 如果第2个cin.getline函数也写成cin. getline(ch, 20, '/''),输出结果会如何? 此时最后一行的输出为:
   The third part is: I study C++.

2) 如果在用cin.getline(ch, 20, '/')从输入流读取数据时,遇到回车键("\n"),是否 结束读取?结论是此时"\n"不是结束标志"\n"被作为一个字符被读入。

3) 用getline函数从输入流读字符时,遇到终止标志字符时结束,指针移到该终止标志字符之后,下一个getline函数将从该终止标志的下一个字符开始接着读入,如本程序运行结果所示那样。如果用cin.get函数从输入流读字符时,遇终止标志字符时停止读取,指针不向后移动,仍然停留在原位置。下一次读取时仍从该终止标志字符开始。这是getline函数和get函数不同之处。假如把例13.7程序中的两个cin.line函数调用都改为以下函数调用:
   cin.getline(ch, 20, '/');
则运行结果为:
enter a sentence: I like C++./I study C++./I am happy.↙
The string read with cin is: I
The second part is: like C++.
The third part is:    (没有从输人流中读取有效字符)

第2个cin. getline(ch, 20, '/')从指针当前位置起读取字符,遇到的第1个字符就是终止标志字符读入结束,只把"\0"存放到ch[0]中,所以用“cout<
因此用get函数时要特别注意,必要时用其他方法跳过该终止标志字符(如用后面介绍的ignore函数,详情请查看: 一些与输入有关的istream类成员函数),但一般来说还是用getline函数更方便。

4) 请比较用“cin<<”和用成员函数cin.getline()读数据的区别。用“cin<<”读数据时以空白字符(包括空格、tab键、回车键)作为终止标志,而用cin.getline()读数据时连续读取一系列字符,可以包括空格。用“cin <<”可以读取C++的标准类型的各类型数据(如果经过重载,还可以用于输入自定义类型的数据),而用cin.getline()只用于输入字符型数据。

13.9 一些与输入有关的istream类成员函数

除了前面介绍的用于读取数据的成员函数外,istream类还有其他在输入数据时用得着的一些成员函数。

eof 函数

eof是end of file的缩写,表示“文件结束”。从输入流读取数据,如果到达文件末尾(遇文件结束符),eof函数值为非零值(真),否则为0(假)。

[例13.8] 逐个读入一行字符,将其中的非空格字符输出。
复制纯文本新窗口
#include  using namespace std; int main( ) { char c; while(!cin.eof( )) //eof( )为假表示未遇到文件结束符 if((c=cin.get( ))!=' ') //检查读入的字符是否为空格字符 cout.put(c); return 0; }
运行情况如下:
C++ is very interesting.↙
C++isveryinteresting.
^Z(结束)

peek函数

peek是“观察”的意思,peek函数的作用是观测下一个字符。其调用形式为:
   c=cin.peek( );
函数的返回值是指针指向的当前字符,但它只是观测,指针仍停留在当前位置,并不后移。如果要访问的字符是文件结束符,则函数值是EOF(-1)。

putback函数

其调用形式为
   cin.putback(ch);
其作用是将前面用get或getline函数从输入流中读取的字符ch返回到输入流,插入到当前指针位置,以供后面读取。

[例13.9] peek函数和putback函数的用法。
#include  using namespace std; int main( ) { char c[20]; int ch; cout<<"please enter a sentence:"<
运行情况如下:
please enter a sentence:
I am a boy./ am a student./↙
The first part is:I am a boy.
The next character(ASCII code) is:32(下一个字符是空格)
The second part is:I am a student

ignore函数

其调用形式为
   cin.ignore(n, 终止字符)
函数作用是跳过输入流中n个字符,或在遇到指定的终止字符时提前结束(此时跳过包括终止字符在内的若干字符)。如
   ighore(5, 'A')  //跳过输入流中个字符,遇'A'后就不再跳了
也可以不带参数或只带一个参数。如
   ignore( )  // n默认值为,终止字符默认为EOF
相当于
   ignore(1, EOF)

[例13.10] 用ignore函数跳过输入流中的字符。先看不用ignore函数的情况:
#include  using namespace std; int main( ) { char ch[20]; cin.get(ch,20,'/'); cout<<"The first part is:"<
运行结果如下:
I like C++./I study C++./I am happy.↙
The first part is:I like C++.
The second part is:(字符数组ch中没有从输入流中读取有效字符)

如果希望第二个cin.get函数能读取"I study C++.",就应该设法跳过输入流中第一个'/',可以用ignore函数来实现此目的,将程序改为:
复制纯文本新窗口
#include  using namespace std; int main( ) { char ch[20]; cin.get(ch,20,'/'); cout<<"The first part is:"<
运行结果如下:
I like C++./I study C++./I am happy.↙
The first part is:I like C++.
The second part is:I study C++.

以上介绍的各个成员函数,不仅可以用cin流对象来调用,而且也可以用istream类的其他流对象调用。 

13.10 C++文件的概念

迄今为止,我们讨论的输入输出是以系统指定的标准设备(输入设备为键盘,输出设备为显示器)为对象的。在实际应用中,常以磁盘文件作为对象。即从磁盘文件读取数据,将数据输出到磁盘文件。磁盘是计算机的外部存储器,它能够长期保留信息,能读能写,可以刷新重写,方便携带,因而得到广泛使用。

文件(file)是程序设计中一个重要的概念。所谓“文件”,一般指存储在外部介质上数据的集合。一批数据是以文件的形式存放在外部介质(如磁盘、光盘和U盘)上的。操 作系统是以文件为单位对数据进行管理的,也就是说,如果想找存在外部介质上的数据, 必须先按文件名找到所指定的文件,然后再从该文件中读取数据。要向外部介质上存储数据也必须先建立一个文件(以文件名标识),才能向它输出数据。

外存文件包括磁盘文件、光盘文件和U盘文件。目前使用最广泛的是磁盘文件,为叙述方便,教程中凡用到外存文件的地方均以磁盘文件来代表,在程序中对光盘文件和U盘文件的使用方法与磁盘文件相同。

对用户来说,常用到的文件有两大类,一类是程序文件(program file),如C++的源程序文件(.cpp)、目标文件(.obj)、可执行文件(.exe)等。一类是数据文件(data file), 在程序运行时,常常需要将一些数据(运行的最终结果或中间数据)输出到磁盘上存放起来,以后需要时再从磁盘中输入到计算机内存。这种磁盘文件就是数据文件。程序中的输入和输出的对象就是数据文件。

根据文件中数据的组织形式,可分为ASCII文件和二进制文件。ASCII文件又称文本(text)文件或字符文件,它的每一个字节放一个ASCII代码,代表一个字符。二进制文件又称内部格式文件或字节文件,是把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放。

对于字符信息,在内存中是以ASCII代码形式存放的,因此,无论用ASCII文件输出还是用二进制文件输出,其数据形式是一样的。但是对于数值数据,二者是不同的。例如有一个长整数100000,在内存中占4个字节,如果按内部格式直接输出,在磁盘文件中占 4个字节,如果将它转换为ASCII码形式输出,则要占6个字节。

用ASCII码形式输出的数据是与字符一一对应的,一个字节代表一个字符,可以直接在屏幕上显示或打印出来。这种方式使用方便,比较直观,便于阅读,便于对字符逐个进行输入输出。但一般占存储空间较多,而且要花费转换时间(二进制形式与ASCII码间的转换)。用内部格式(二进制形式)输出数值,可以节省外存空间,而且不需要转换时间,但一个字节并不对应一个字符,不能直接显示文件中的内容。 如果在程序运行过程中有些中间结果数据暂时保存在磁盘文件中,以后又需要输入到内存的,这时用二进制文件保存是最合适的。如果是为了能显示和打印以供阅读,则应按ASCII码形式输出。此时得到的是ASCII文件,它的内容可以直接在显示屏上观看。

C++提供了低级的I/O功能和高级的I/O功能。高级的I/O功能是把若干个字节组合为一个有意义的单位(如整数、单精度数、双精度数、字符串或用户自定义的类型的数据),然后以ASCII字符形式输入和输出。例如将数据从内存送到显示器输出,就属于高级I/O功能,先将内存中的数据转换为ASCII字符,然后分别按整数、单精度数、双精度数等形式输出。这种面向类型的输入输出在程序中用得很普遍,用户感到方便。但在传输大容量的文件时由于数据格式转换,速度较慢,效率不高。

所谓低级的I/O功能是以字节为单位输入和输出的,在输入和输出时不进行数据格式的转换。这种输入输出是以二进制形式进行的。 通常用来在内存和设备之间传输一批字节。这种输入输出速度快、效率高,一般大容量的文件传输用无格式转换的I/O。但使用时会感到不大方便。

13.11 C++文件流类与文件流对象

文件流是以外存文件为输入输出对象的数据流。输出文件流是从内存流向外存文件的数据,输入文件流是从外存文件流向内存的数据。每一个文件流都有一个内存缓冲区与之对应。

请区分文件流与文件的概念,不用误以为文件流是由若干个文件组成的流。文件流本身不是文件,而只是以文件为输入输出对象的流。若要对磁盘文件输入输出,就必须通过文件流来实现。

在C++的I/O类库中定义了几种文件类,专门用于对磁盘文件的输入输出操作。在 图13.2(详情请查看: 与C++输入输出有关的类和对象)中可以看到除了标准输入输出流类istream、ostream和iostream类外,还有3个用于文件操作的文件类:
  • ifstream类,它是从istream类派生的,用来支持从磁盘文件的输入。

  • ofstream类,它是从ostream类派生的,用来支持向磁盘文件的输出。

  • fstream类,它是从iostream类派生的,用来支持对磁盘文件的输入输出。


要以磁盘文件为对象进行输入输出,必须定义一个文件流类的对象,通过文件流对象将数据从内存输出到磁盘文件,或者通过文件流对象从磁盘文件将数据输入到内存。

其实在用标准设备为对象的输入输出中,也是要定义流对象的,如cin、cout就是流对象,C++是通过流对象进行输入输出的。由于cin、cout已在iostream.h中事先定义,所以用户不需自己定义。在用磁盘文件时,由于情况各异,无法事先统一定义,必须由用户自己定义。此外,对磁盘文件的操作是通过文件流对象(而不是cin和cout)实现的。文件流对象是用文件流类定义的,而不是用istream和ostream类来定义的。可以用下面的方法建立一个输出文件流对象:
   ofstream outfile;
如同在头文件iostream中定义了流对象cout —样,现在在程序中定义了outfile为 ofstream类(输出文件流类)的对象。但是有一个问埋还未解决:在定义 cout 时已将它和标准输出设备(显示器)建立关联,而现在虽然建立了一个输出文件流对象,但是还未指定它向哪一个磁盘文件输出,需要在使用时加以指定。下一节即将解答这个问题。

13.12 C++文件的打开与关闭

这里讲一下如何打开和关闭磁盘上的文件,其他外设(U盘、光盘等)上的文件与此相同。

打开文件

所谓打开(open)文件是一种形象的说法,如同打开房门就可以进入房间活动一样。 打开文件是指在文件读写之前做必要的准备工作,包括:

  • 为文件流对象和指定的磁盘文件建立关联,以便使文件流流向指定的磁盘文件。

  • 指定文件的工作方式,如,该文件是作为输入文件还是输出文件,是ASCII文件还是二进制文件等。


以上工作可以通过两种不同的方法实现。

1) 调用文件流的成员函数open。
   ofstream outfile; //定义ofstream类(输出文件流类)对象outfile
   outfile.open("f1.dat",ios::out);   //使文件流与f1.dat文件建立关联
第2行是调用输出文件流的成员函数open打开磁盘文件f1.dat,并指定它为输出文件, 文件流对象outfile将向磁盘文件f1.dat输出数据。ios::out是I/O模式的一种,表示以输出方式打开一个文件。或者简单地说,此时f1.dat是一个输出文件,接收从内存输出的数据。

调用成员函数open的一般形式为:
   文件流对象.open(磁盘文件名, 输入输出方式);
磁盘文件名可以包括路径,如"c:\new\\f1.dat",如缺省路径,则默认为当前目录下的文件。

2) 在定义文件流对象时指定参数
在声明文件流类时定义了带参数的构造函数,其中包含了打开磁盘文件的功能。因此,可以在定义文件流对象时指定参数,调用文件流类的构造函数来实现打开文件的功能。如
   ostream outfile("f1.dat",ios::out);
一般多用此形式,比较方便。作用与open函数相同。

输入输出方式是在ios类中定义的,它们是枚举常量,有多种选择,见表13.6。

表13.6 文件输入输出方式设置值
方 式 作用
ios::in 以输入方式打开文件
ios::out 以输出方式打开文件(这是默认方式),如果已有此名字的文件,则将其原有内容全部清除
ios::app 以输出方式打开文件,写入的数据添加在文件末尾
ios::ate 打开一个已有的文件,文件指针指向文件末尾
ios: :trunc 打开一个文件,如果文件已存在,则删除其中全部数据,如文件不存在,则建立新文件。如已指定了 ios::out 方式,而未指定ios: :app,ios::ate,ios: :in,则同时默认此方式
ios:: binary 以二进制方式打开一个文件,如不指定此方式则默认为ASCII方式
ios::nocreate 打开一个已有的文件,如文件不存在,则打开失败。nocrcate的意思是不建立新文件
ios:: noreplace 如果文件不存在则建立新文件,如果文件已存在则操作失败,replace 的意思是不更新原有文件
ios::in l ios::out 以输入和输出方式打开文件,文件可读可写
ios:: out | ios::binary 以二进制方式打开一个输出文件
ios::in l ios::binar 以二进制方式打开一个输入文件


几点说明:
1) 新版本的I/O类库中不提供ios::nocreate和ios::noreplace。

2) 每一个打开的文件都有一个文件指针,该指针的初始位置由I/O方式指定,每次读写都从文件指针的当前位置开始。每读入一个字节,指针就后移一个字节。当文件指针移到最后,就会遇到文件结束EOF(文件结束符也占一个字节,其值为-1),此时流对象的成员函数eof的值为非0值(一般设为1),表示文件结束了。

3) 可以用“位或”运算符“|”对输入输出方式进行组合,如表13.6中最后3行所示那样。还可以举出下面一些例子:
   ios::in | ios:: noreplace  //打开一个输入文件,若文件不存在则返回打开失败的信息
   ios::app | ios::nocreate //打开一个输出文件,在文件尾接着写数据,若文件不存在,则返回打开失败的信息
   ios::out l ios::noreplace //打开一个新文件作为输出文件,如果文件已存在则返回打开失败的信息
   ios::in l ios::out I ios::binary //打开一个二进制文件,可读可写

但不能组合互相排斥的方式,如 ios::nocreate l ios::noreplace。

4) 如果打开操作失败,open函数的返回值为0(假),如果是用调用构造函数的方式打开文件的,则流对象的值为0。可以据此测试打开是否成功。如
   if(outfile.open("f1.bat", ios::app) ==0)
       cout <<"open error";

   if( !outfile.open("f1.bat", ios::app) )
       cout <<"open error";

关闭磁盘文件

在对已打开的磁盘文件的读写操作完成后,应关闭该文件。关闭文件用成员函数close。如
   outfile.close( );  //将输出文件流所关联的磁盘文件关闭
所谓关闭,实际上是解除该磁盘文件与文件流的关联,原来设置的工作方式也失效,这样,就不能再通过文件流对该文件进行输入或输出。此时可以将文件流与其他磁盘文件建立关联,通过文件流对新的文件进行输入或输出。如
   outfile.open("f2.dat",ios::app|ios::nocreate);
此时文件流outfile与f2.dat建立关联,并指定了f2.dat的工作方式。

13.13 C++对ASCII文件的读写操作

如果文件的每一个字节中均以ASCII代码形式存放数据,即一个字节存放一个字符,这个文件就是ASCII文件(或称字符文件)。程序可以从ASCII文件中读入若干个字符,也可以向它输出一些字符。

对ASCII文件的读写操作可以用以下两种方法:
1) 用流插入运算符“<<”和流提取运算符“>>”输入输出标准类型的数据。“<<”和“ >>”都巳在iostream中被重载为能用于ostream和istream类对象的标准类型的输入输出。由于ifstream和ofstream分别是ostream和istream类的派生类(详情请见: 与C++输入输出有关的类和对象),因此它们从ostream和istream类继承了公用的重载函数,所以在对磁盘文件的操作中,可以通过文件流对象和流插入运算符“<<”及流提取运算符“>>”实现对磁盘 文件的读写,如同用cin、cout和<<、>>对标准设备进行读写一样。

2) 用文件流的put、get、geiline等成员函数进行字符的输入输出,前面已介绍,请查看: 用C++流成员函数put输出单个字符、 C++ get()函数读入一个字符和 C++ getline()函数读入一行字符。

[例13.11] 有一个整型数组,含个元素,从键盘输入个整数给数组,将此数组送到磁盘文件中存放。
#include  using namespace std; int main( ) { int a[10]; ofstream outfile("f1.dat",ios::out);//定义文件流对象,打开磁盘文件"f1.dat" if(!outfile) //如果打开失败,outfile返回值 { cerr<<"open error!"<>a[i]; outfile<
运行情况如下:
enter 10 integer numbers:
1 3 5 2 4 6 10 8 7 9 ↙

对程序的几点说明:
1) 程序中用#indude命令包含了头文件fstream,这是由于在程序中用到文件流类 ofstream,而ofstream是在头文件fstream中定义的。有人可能会提出:程序中用到cout, 为什么没有包含iostream头文件?这是由于在头文件fstream中包含了头文件iostream, 因此,包含了头文件fstream就意味着已经包含了头文件iostream,不必重复(当然,多写 一行#include 也不出错)。

2) 参数 ios::out 可以省写。 如不写此项,则默认为ios::out。下面两种写法等价:
   ofstream outfile("f1.dat", ios::out);
   ofstream outfile("f1.dat");

(3) 系统函数exit用来结束程序运行。exit的参数为任意整数,可用0,1或其他整数。由于用了exit函数,某些老版本的C ++要求包含头文件stdlib.h,而在新版本的C++(如 GCC)则不要求此包含。

4) 在程序中用“cin>>”从键盘逐个读入10个整数,每读入一个就将该数向磁盘文件输出,输出的语句为:
   outfile< 可以看出,用法和向显示器输出是相似的,只是把标准输出流对象cout换成文件输出流对象outfile而已。由于是向磁盘文件输出,所以在屏幕上看不到输出结果。

请注意:在向磁盘文件输出一个数据后,要输出一个(或几个)空格或换行符,以作为数据间的分隔,否则以后从磁盘文件读数据时,10个整数的数字连成一片无法区分。

[例13.12] 从例13.11建立的数据文件f1.dat中读入个整数放在数组中,找出并输出个数中的最大者和它在数组中的序号。
#include  using namespace std; int main( ) { int a[10],max,i,order; //定义输入文件流对象,以输入方式打开磁盘文件f1.dat ifstream infile("f1.dat",ios::in|ios::nocreate); if(!infile) { cerr<<"open error!"<>a[i]; //从磁盘文件读入10个整数,顺序存放在a数组中 cout<max) { max=a[i]; //将当前最大值放在max中 order=i; //将当前最大值的元素序号放在order中 } cout<<"max="<
运行情况如下:
1 3 5 2 4 6 10 8 7 9    (在磁盘文件中存放的个数)
max=10   (最大值为)
order=6   (最大值是数组中序号为的元素)

可以看到:文件f1.dat在例13.11中作为输出文件,在例13.12中作为输入文件。 一个磁盘文件可以在一个程序中作为输入文件,而在另一个程序中作为输出文件,在不同 的程序中可以有不同的工作方式。甚至在同一个程序中先后以不同方式打开,如先以输出方式打开,接收从程序输出的数据,然后关闭它,再以输入方式打开,程序可以从中读取数据。

[例13.13] 从键盘读入一行字符,把其中的字母字符依次存放在磁盘文件f2.dat中。再把它从磁盘文件读入程序,将其中的小写字母改为大写字母,再存入磁盘文件f3.dat。
#include  using namespace std; // save_to_file函数从键盘读入一行字符,并将其中的字母存入磁盘文件 void save_to_file( ) { ofstream outfile("f2.dat"); //定义输出文件流对象outfile,以输出方式打开磁盘文件f2.dat if(!outfile) { cerr<<"open f2.dat error!"<=65 && c[i]<=90||c[i]>=97 && c[i]<=122) //如果是字母字符 { outfile.put(c[i]); //将字母字符存入磁盘文件f2.dat cout<=97 && ch<=122) //判断ch是否为小写字母 ch=ch-32; //将小写字母变为大写字母 outfile.put(ch); //将该大写字母存入磁盘文件f3.dat cout<
运行情况如下:
New Beijing, Great Olypic, 2008, China.↙
NewBeijingGreatOlypicChina(将字母写入磁盘文件f2.dat,同时在屏幕显示)
NEWBEIJINGGREATOLYPICCHINA           (改为大写字母)

本程序用了文件流的put、get、getline等成员函数实现输入和输出,用成员函数inline从键盘读入一行字符,调用函数的形式是cin.inline(c, 80) 在从磁盘文件读一个字符时用infile.get(ch)。可以看到二者的使用方法是一样的, cin和infile都是istream类派生类的对象,它们都可以使用istream类的成员函数。二者的区别只在于:对标准设备显示器输出时用cin,对磁盘文件输出时用文件流对象。

磁盘文件f3.dat的内容虽然是ASCII字符,但人们是不能直接看到的,如果想从显示器上观看磁盘上ASCII文件的内容,可以采用以下两个方法:
1) 在DOS环境下用TYPE命令,如
   D:\\C++>TYPE f3.dat↙(假设当前目录是D:\\C++ )
在显示屏上会输出
   NEWBEIJINGGREATOLYPICCHINA
如果用GCC编译环境,可选择File菜单中的DOS Shell菜单项,即可进入DOS环境。想从DOS返回GCC主窗口,从键盘输入exit即可。

2) 编一程序将磁盘文件内容读入内存,然后输出到显示器。可以编一个专用函数。
复制纯文本新窗口
#include  using namespace std; void display_file(char *filename) { ifstream infile(filename,ios::in|ios::nocreate); if(!infile) { cerr<<"open error!"<
运行时输出f3.dat中的字符:
NEWBEIJINGGREATOLYPICCHINA

13.14 C++对二进制文件的读写操作

二进制文件不是以ASCII代码存放数据的,它将内存中数据存储形式不加转换地传送到磁盘文件,因此它又称为内存数据的映像文件。因为文件中的信息不是字符数据,而是字节中的二进制形式的信息,因此它又称为字节文件

对二进制文件的操作也需要先打开文件,用完后要关闭文件。在打开时要用ios::binary指定为以二进制形式传送和存储。二进制文件除了可以作为输入文件或输出文件外,还可以是既能输入又能输出的文件。这是和ASCII文件不同的地方。

用成员函数read和write读写二进制文件

对二进制文件的读写主要用istream类的成员函数read和write来实现。这两个成员函数的原型为
   istream& read(char *buffer,int len);
   ostream& write(const char * buffer,int len);
字符指针buffer指向内存中一段存储空间。len是读写的字节数。调用的方式为:
   a. write(p1,50);
   b. read(p2,30);
上面第一行中的a是输出文件流对象,write函数将字符指针p1所给出的地址开始的50个字节的内容不加转换地写到磁盘文件中。在第二行中,b是输入文件流对象,read 函数从b所关联的磁盘文件中,读入30个字节(或遇EOF结束),存放在字符指针p2所指的一段空间内。

[例13.14] 将一批数据以二进制形式存放在磁盘文件中。

#include  using namespace std; struct student { char name[20]; int num; int age; char sex; }; int main( ) { student stud[3]={"Li",1001,18,'f',"Fun",1002,19,'m',"Wang",1004,17,'f'}; ofstream outfile("stud.dat",ios::binary); if(!outfile) { cerr<<"open error!"<

用成员函数write向stud.dat输出数据,从前面给出的write函数的原型可以看出: 第1个形参是指向char型常变量的指针变量buffer,之所以用const声明,是因为不允许通过指针改变其指向数据的值。形参要求相应的实参是字符指针或字符串的首地址。现在要将结构体数组的一个元素(包含4个成员)一次输出到磁盘文件stud.dat。&tud[i] 是结构体数组第i个元素的首地址,但这是指向结构体的指针,与形参类型不匹配。因此 要用(char *)把它强制转换为字符指针。第2个参数是指定一次输出的字节数。sizeof (stud[i])的值是结构体数组的一个元素的字节数。调用一次write函数,就将从&tud[i]开始的结构体数组的一个元素输出到磁盘文件中,执行3次循环输出结构体数组的3个元素。

其实可以一次输出结构体数组的个元素,将for循环的两行改为以下一行:
  outfile.write((char*)&stud[0],sizeof(stud));
执行一次write函数即输出了结构体数组的全部数据。

abort函数的作用是退出程序,与exit作用相同。

可以看到,用这种方法一次可以输出一批数据,效率较高。在输出的数据之间不必加入空格,在一次输出之后也不必加回车换行符。在以后从该文件读入数据时不是靠空格作为数据的间隔,而是用字节数来控制。

[例13.15] 将刚才以二进制形式存放在磁盘文件中的数据读入内存并在显示器上显示。

#include  using namespace std; struct student { string name; int num; int age; char sex; }; int main( ) { student stud[3]; int i; ifstream infile("stud.dat",ios::binary); if(!infile) { cerr<<"open error!"<

运行时在显示器上显示:
NO.1
name: Li
num: 1001
age: 18
sex: f

NO.2
name: Fun
num: 1001
age: 19
sex: m

NO.3
name: Wang
num: 1004
age: 17
sex: f

请思考,能否一次读入文件中的全部数据,如:
   infile.read((char*)&stud[0],sizeof(stud));
答案是可以的,将指定数目的字节读入内存,依次存放在以地址&tud[0]开始的存储空间中。要注意读入的数据的格式要与存放它的空间的格式匹配。由于磁盘文件中的数据是从内存中结构体数组元素得来的,因此它仍然保留结构体元素的数据格式。现在再读入内存,存放在同样的结构体数组中,这必然是匹配的。如果把它放到一个整型数组中,就不匹配了,会出错。

与文件指针有关的流成员函数

在磁盘文件中有一个文件指针,用来指明当前应进行读写的位置。在输入时每读入 一个宇节,指针就向后移动一个字节。在输出时每向文件输出一个字节,指针就向后移动 一个字节,随着输出文件中字节不断增加,指针不断后移。对于二进制文件,允许对指针进行控制,使它按用户的意图移动到所需的位置,以便在该位置上进行读写。文件流提供 一些有关文件指针的成员函数。为了查阅方便,将它们归纳为表13.7,并作必要的说明。

表13.7 文件流与文件指针有关的成员函数
成员函数 作 用
gcount() 返回最后一次输入所读入的字节数
tellg() 返回输入文件指针的当前位置
seekg(文件中的位置) 将输入文件中指针移到指定的位置
seekg(位移量, 参照位置) 以参照位置为基础移动若干字节
tellp() 返回输出文件指针当前的位置
seekp(文件中的位置) 将输出文件中指针移到指定的位置
seekp(位移量, 参照位置) 以参照位置为基础移动若干字节


几点说明:
1) 这些函数名的第一个字母或最后一个字母不是g就是p。带 g的是用于输入的函数(g是get的第一个字母,以g作为输入的标识,容易理解和记忆), 带p的是用于输出的函数(P是put的第一个字母,以P作为输出的标识)。例如有两个 tell 函数,tellg用于输入文件,tellp用于输出文件。同样,seekg用于输入文件,seekp用于输出文件。以上函数见名知意,一看就明白,不必死记。

如果是既可输入又可输出的文件,则任意用seekg或seekp。

2) 函数参数中的“文件中的位置”和“位移量”已被指定为long型整数,以字节为单位。“参照位置”可以是下面三者之一:
   ios::beg 文件开头(beg是begin的缩写),这是默认值。
   ios::cur  指针当前的位置(cur是current的缩写)。
   ios::end  文件末尾。
它们是在ios类中定义的枚举常量。举例如下:
   infile.seekg(100); //输入文件中的指针向前移到字节位置
   infile.seekg(-50,ios::cur);  //输入文件中的指针从当前位置后移字节
   outfile.seekp(-75,ios::end);  //输出文件中的指针从文件尾后移字节

随机访问二进制数据文件

一般情况下读写是顺序进行的,即逐个字节进行读写。但是对于二进制数据文件来说,可以利用上面的成员函数移动指针,随机地访问文件中任一位置上的数据,还可以修改文件中的内容。

[例13.16] 有个学生的数据,要求:

  • 把它们存到磁盘文件中;

  • 将磁盘文件中的第,3,5个学生数据读入程序,并显示出来;

  • 将第个学生的数据修改后存回磁盘文件中的原有位置。

  • 从磁盘文件读入修改后的个学生的数据并显示出来。


要实现以上要求,需要解决个问题:

  • 由于同一磁盘文件在程序中需要频繁地进行输入和输出,因此可将文件的工作方式指定为输入输出文件,即ios::in|ios::out|ios::binary。

  • 正确计算好每次访问时指针的定位,即正确使用seekg或seekp函数。

  • 正确进行文件中数据的重写(更新)。


可写出以下程序:

#include  using namespace std; struct student { int num; char name[20]; float score; }; int main( ) { student stud[5]={1001,"Li",85,1002,"Fun",97.5,1004,"Wang",54,1006,"Tan",76.5,1010,"ling",96}; fstream iofile("stud.dat",ios::in|ios::out|ios::binary); //用fstream类定义输入输出二进制文件流对象iofile if(!iofile) { cerr<<"open error!"<

运行情况如下:
1001 Li 85(第个学生数据)
1004 Wang 54         (第个学生数据)
1010 ling 96        (第个学生数据)

1001 Li 85           (输出修改后个学生数据)
1002 Fun 97.5
1012 Wu 60          (已修改的第个学生数据)
1006 Tan 76.5
1010 ling 96

本程序也可以将磁盘文件stud.dat先后定义为输出文件和输入文件,在结束第一次的输出之后关闭该文件,然后再按输入方式打开它,输入完后再关闭它,然后再按输出方式打开,再关闭,再按输入方式打开它,输入完后再关闭。显然这是很烦琐和不方便的。 在程序中把它指定为输入输出型的二进制文件。这样,不仅可以向文件添加新的数据或读入数据,还可以修改(更新)数据。利用这些功能,可以实现比较复杂的输入输出任务。

请注意,不能用ifstream或ofstream类定义输入输出的二进制文件流对象,而应当用fstream类。

13.15 C++对字符串流的读写

文件流是以外存文件为输入输出对象的数据流,字符串流不是以外存文件为输入输出的对象,而以内存中用户定义的字符数组(字符串)为输入输出的对象,即将数据输出到内存中的字符数组,或者从字符数组(字符串)将数据读入。字符串流也称为内存流。

字符串流也有相应的缓冲区,开始时流缓冲区是空的。如果向字符数组存入数据,随着向流插入数据,流缓冲区中的数据不断增加,待缓冲区满了(或遇换行符),一起存入字符数组。如果是从字符数组读数据,先将字符数组中的数据送到流缓冲区,然后从缓冲区中提取数据赋给有关变量。

在字符数组中可以存放字符,也可以存放整数、浮点数以及其他类型的数据。在向字符数组存入数据之前,要先将数据从二进制形式转换为ASCII代码,然后存放在缓冲区, 再从缓冲区送到字符数组。从字符数组读数据时,先将字符数组中的数据送到缓冲区,在赋给变量前要先将ASCII代码转换为二进制形式。总之,流缓冲区中的数据格式与字符数组相同。这种情况与以标准设备(键盘和显示器)为对象的输入输出是类似的,键盘和显示器都是按字符形式输入输出的设备,内存中的数据在输出到显示器之前,先要转换为 ASCII码形式,并送到输出缓冲区中。从键盘输入的数据以ASCII码形式输入到输入缓冲区,在赋给变量前转换为相应变量类型的二进制形式,然后赋给变量。对于字符串流的输入输出的情况,如不清楚,可以从对标准设备的输入输出中得到启发。

文件流类有ifstream,ofstream和fstream,而字符串流类有istrstream,ostrstream和strstream。文件流类和字符串流类都是ostream,istream和iostream类的派生类,因此对它们的操作方法是基本相同的。向内存中的一个字符数组写数据就如同向文件写数据一样,但有3点不同:
  1. 输出时数据不是流向外存文件,而是流向内存中的一个存储空间。输入时从内存中的存储空间读取数据。在严格的意义上说,这不属于输入输出,称为读写比较合适。 因为输入输出一般指的是在计算机内存与计算机外的文件(外部设备也视为文件)之间 的数据传送。但由于C++的字符串流采用了 C++的流输入输出机制,因此往往也用输入和输出来表述读写操作。

  2. 字符串流对象关联的不是文件,而是内存中的一个字符数组,因此不需要打开和关闭文件。

  3. 每个文件的最后都有一个文件结束符,表示文件的结束。而字符串流所关联的字符数组中没有相应的结束标志,用户要指定一个特殊字符作为结束符,在向字符数组写入全部数据后要写入此字符。


字符串流类没有open成员函数,因此要在建立字符串流对象时通过给定参数来确立字符串流与字符数组的关联。即通过调用构造函数来解决此问题。建立字符串流对象的方法与含义如下。

建立输出字符串流对象

ostrstream类提供的构造函数的原型为:
  ostrstream::ostrstream(char *buffer,int n,int mode=ios::out);
buffer是指向字符数组首元素的指针,n为指定的流缓冲区的大小(一般选与字符数组的大小相同,也可以不同),第3个参数是可选的,默认为ios::out方式。可以用以下语句建立输出字符串流对象并与字符数组建立关联:
   ostrstream strout(ch1,20);
作用是建立输出字符串流对象strout,并使strout与字符数组ch1关联(通过字符串流将数据输出到字符数组ch1),流缓冲区大小为20。

建立输入字符串流对象

istrstream类提供了两个带参的构造函数,原型为:
   istrstream::istrstream(char *buffer);
   istrstream::istrstream(char *buffer,int n);
buffer是指向字符数组首元素的指针,用它来初始化流对象(使流对象与字符数组建立关联)。可以用以下语句建立输入字符串流对象:
   istrstream strin(ch2);
作用是建立输入字符串流对象strin,将字符数组ch2中的全部数据作为输入字符串流的内容。
   istrstream strin(ch2,20);
流缓冲区大小为20,因此只将字符数组ch2中的,20个字符作为输入字符串流的内容。

建立输入输出字符串流对象

strstream类提供的构造函数的原型为:
   strstream::strstream(char *buffer,int n,int mode);
可以用以下语句建立输入输出字符串流对象:
   strstream strio(ch3,sizeof(ch3),ios::in|ios::out);
作用是建立输入输出字符串流对象,以字符数组ch3为输入输出对象,流缓冲区大小与数组ch3相同。

以上个字符串流类是在头文件strstream中定义的,因此程序中在用到istrstream、ostrstream和strstream类时应包含头文件strstream(在GCC中,用头文件strstream)。

[例13.17] 将一组数据保存在字符数组中。
#include  using namespace std; struct student { int num; char name[20]; float score; }; int main( ) { student stud[3]={1001,"Li",78,1002,"Wang",89.5,1004,"Fun",90}; char c[50]; //用户定义的字符数组 ostrstream strout(c,30); //建立输出字符串流,与数组c建立关联,缓冲区长 for(int i=0;i<3;i++) //向字符数组c写个学生的数据 strout<
运行时在显示器上的输出如下:
array c:
1001Li781002Wang89.51004Fun90

以上就是字符数组c中的字符。可以看到:
1) 字符数组c中的数据全部是以ASCII代码形式存放的字符,而不是以二进制形式表示的数据。

2) 在建立字符串流strout时指定流缓冲区大小为30字节,与字符数组c的大小不同,这是允许的,这时字符串流最多可以传送30个字符给字符数组c。请思考:如果将流 缓冲区大小改为10字节,即:
   ostrstream.strout( c ,10);
运行情况会怎样?流缓冲区只能存放10个字符,将这10个字符写到字符数组c中。运行时显示的结果是:
   1001Li7810

字符数组c中只有10个有效字符。一般都把流缓冲区的大小指定与字符数组的大小 相同。

3) 字符数组c中的数据之间没有空格,连成一片,这是由输出的方式决定的。如果以后想将这些数据读回赋给程序中相应的变量,就会出现问题,因为无法分隔两个相邻的数据。为解决此问题,可在输出时人为地加入空格。如
   for(int i=0;i<3;i++)
       strout<<" "< 同时应修改流缓冲区的大小,以便能容纳全部内容,今改为字节。这样,运行时将输出:
   1001 Li 78 1002 Wang 89.5 1004 Fun 90
再读入时就能清楚地将数据分隔开。

[例13.18] 在一个字符数组c中存放了个整数,以空格相间隔,要求将它们放到整型数组中,再按大小排序,然后再存放回字符数组c中。
#include  using namespace std; int main( ) { char c[50]="12 34 65 -23 -32 33 61 99 321 32"; int a[10],i,j,t; cout<<"array c:"<>a[i]; //从字符数组c读入个整数赋给整型数组a cout<<"array a:"; for(i=0;i<10;i++) cout<a[j+1]) {t=a[j];a[j]=a[j+1];a[j+1]=t;} ostrstream strout(c,sizeof(c)); //建立输出串流对象strout并与字符数组c关联 for(i=0;i<10;i++) strout<
运行结果如下:
array c: 12 34 65 -23 -32 33 61 99 321 32(字符数组c原来的内容)
array a: 12 34 65 -23 -32 33 61 99 321 32     (整型数组a的内容)
array c: -32 –12 32 33 34 61 65 99 321     (字符数组c最后的内容)

对字符串流的几点说明:
1) 用字符串流时不需要打开和关闭文件。

2) 通过字符串流从字符数组读数据就如同从键盘读数据一样,可以从字符数组读入字符数据,也可以读入整数、浮点数或其他类型数据。如果不用字符串流,只能从字符数组逐个访问字符,而不能按其他类型的数据形式读取数据。这是用字符串流访问字符数组的优点,使用方便灵活。

3) 程序中先后建立了两个字符串流strin和strout,与字符数组c关联。strin从字符数组c中获取数据,strout将数据传送给字符数组。分别对同一字符数组进行操作。甚至可以对字符数组交叉进行读写,输入字符串流和输出字符串流分别有流指针指示当前位 置,互不干扰。

4) 用输出字符串流向字符数组c写数据时,是从数组的首地址开始的,因此更新了 数组的内容。

5) 字符串流关联的字符数组并不一定是专为字符串流而定义的数组,它与一般的字符数组无异,可以对该数组进行其他各种操作。

通过以上对字符串流的介绍,大家可以看到:与字符串流关联的字符数组相当于内存中的临时仓库,可以用来存放各种类型的数据(以ASCII形式存放),在需要时再从中读回来。它的用法相当于标准设备(显示器与键盘),但标准设备不能保存数据,而字符数组中的内容可以随时用ASCII字符输出。它比外存文件使用方便,不必建立文件(不 需打开与关闭),存取速度快。但它的生命周期与其所在的模块(如主函数)相同,该模块的生命周期结束后,字符数组也不存在了。因此只能作为临时的存储空间。

你可能感兴趣的:(C++编程)