C++ 输入与输出

C++ 输入与输出

输入和输出的概念

IO的分类
C++ 输入输出包含以下三个方面的内容:

  • 对系统指定的标准设备的输入和输出:即从键盘输入数据,输出到显示器屏幕;这种输入输出称为标准输入输出,简称标准I/O
  • 以外存磁盘文件为对象进行输入和输出,即从磁盘文件输入数据,数据输出到磁盘文件;以外存文件为对象的输入输出称为文件输入输出,简称文件I/O
  • 对内存中指定的空间进行输入和输出;通常指定一个字符数组作为存储空间(实际上可以利用该空间存储任何信息);这种输入和输出称为字符串输入输出,简称串I/O

类型安全和可扩展性
在 C 语言中,用 printf 和 scanf 进行输入输出,往往不能保证所输入输出的数据是可靠的、安全的;

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++ 对标准类型的数据和对用户声明类型数据的输入输出,采用同样的方法处理;
显然,在用户声明了一个新类后,是无法用 scanf 和 printf 函数直接输入和输出这个类的对象的;

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

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

相关的类及对象

输入和输出是数据传送的过程,数据如流水一样从一处流向另一处;C++ 形象地将此过程称为流(Stream)
C++ 的输入输出流是指由若干字节组成的宇节序列,这些宇节中的数据按顺序从一个对象传送到另一对象;

实际上,C++ 会在内存中为每一个数据流开辟一个内存缓冲区,用来存放流中的数据;

当用 cout 和插入运算符<<向显示器输出数据时,先将这些数据送到程序中的输出缓冲区保存,直到缓冲区满了或遇到endl,就将缓冲区中的全部数据送到显示器显示出来;
在输入时,从键盘输入的数据先放在键盘的缓冲区中,当按回车键时,键盘缓冲区中的数据输入到程序中的输入缓冲区,形成 cin 流,然后用提取运算符>>从输入缓冲区中提取数据送给程序中的有关变量;

总之,流是与内存缓冲区相对应的,或者说,缓冲区中的数据就是流;

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

cout 和 cin 并不是 C++ 语言中提供的语句,它们是 iostream 类的对象;

C++ 提供了用于输入输出的 iostream 类库;
iostream 这个单词是由3个部分组成的,即 i-o-stream,意为输入输出流;

在 iostream 类库中包含许多用于输入输出的类,如下表所示:

头文件 类名 说明
iostream ios 抽象基类
iostream istream 通用输入流和其他输入流的基类
iostream ostream 通用输出流和其他输出流的基类
iostream iostream 通用输入输出流和其他输入输出流的基类
fstream ifstream 输入文件流类
fstream ofstream 输出文件流类
fstream fstream 输入输出文件流类
strstream istrstream 输入字符串流类
strstream ostrstream 输出字符串流类
strstream strstream 输入输出字符串流类

ios 是抽象基类,由它派生出 istream 类和 ostream 类;
istream 类支持输入操作,ostream 类支持输出操作,iostream 类支持输入输出操作;
iostream 类是从 istream 类和 ostream 类通过多重继承而派生的类;

文件的输入输出需要用 ifstream 和 ofstream 类;
ifstream 支持对文件的输入操作,ofstream 支持对文件的输出操作;
类 ifstream 继承了类 istream ,类 ofstream 继承了类 ostream ,类 fstream 继承了类 iostream;

它们的继承关系如下图:
C++ IO类的继承关系图

与 IO 有关的头文件:

  • iostream:标准 IO,stdin、stdout、stderr;
  • fstream:文件 IO;
  • strstream:串 IO;
  • stdiostream:混合 C 和 C++ 的 IO;
  • iomanip:格式化 IO;

iostream头文件中定义的流对象
分别为:cin、cout、cerr、clog;

cin、cout 我们已经很熟悉了,这里简单说一下 cerr、clog:

  • cerr:标准错误流,默认输出至屏幕,无缓冲区;
  • clog:标准错误流,默认输出至屏幕,有缓冲区;

iostream头文件中重载的运算符
<<>>本来在 C++ 中是被定义为左位移运算符右位移运算符的;
由于在 iostream 头文件中对它们进行了重载,使它们能用作标准类型数据的输入和输出运算符;

在 istream 和 ostream 类中分别有一组成员函数对位移运算符<<>>进行重载,以便能用它输入或输出各种标准数据类型的数据;
对于不同的标准数据类型要分别进行重载,如:

如果在程序中有表达式cout << "C++";;实际上相当于cout.operator<<("C++");

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

标准输出流

使用控制符控制输出格式

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

总结
对输出格式的控制,既可以用控制符,也可以用cout流的有关成员函数,二者的作用是相同的;

控制符是在头文件 iomanip 中定义的,因此用控制符时,必须包含 iomanip 头文件;
cout 流的成员函数是在头文件 iostream 中定义的,因此只需包含头文件 iostream,不必包含 iomanip;

许多程序人员感到使用控制符方便简单,可以在一个cout输出语句中连续使用多种控制符;

个人还是偏向使用 scanf/printf 风格的格式化输入输出;

流成员函数put输出单个字符
ostream 类除了提供上面介绍过的用于格式控制的成员函数外,还提供了专用于输出单个字符的成员函数 put();

如:cout.put('a');,调用该函数的结果是在屏幕上显示一个字符 a;

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

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

标准输入流

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

cin的返回值判断
当遇到无效字符或遇到文件结束符时,输入流 cin 就处于出错状态,即无法正常提取数据;此时对 cin 流的所有提取操作将终止;

当输入流 cin 处于出错状态时,如果测试 cin 的值,可以发现它的值为 false,即 cin 为 0 值;
如果输入流在正常状态,cin 的值为 true,即 cin 为一个非 0 值;

可以通过测试 cin 的值,判断流对象是否处于正常状态和提取操作是否成功;

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

1) 不带参数的get函数
其调用形式为:cin.get()
用来从指定的输入流中提取一个字符(包括空白字符),函数的返回值就是读入的字符;
若遇到输入流中的文件结束符,则函数值返回文件结束标志 EOF(End Of File),一般以 -1 代表 EOF;

C语言中的 getchar() 函数与流成员函数 cin.get() 的功能相同,C++ 保留了 C 的这种用法;

2) 有1个参数的get函数
其调用形式为:cin.get(ch)
其作用是从输入流中读取一个字符,赋给字符变量 ch;如果读取成功则函数返回 true,如失败(遇文件结束符)则函数返回 false;

3) 有3个参数的get函数
其调用形式为:cin.get(字符数组/指针, 数组/指针长度n, 终止字符)
其作用是从输入流中读取 n-1 个字符,赋给指定的字符数组(或字符指针指向的数组);
如果在读取 n-1 个字符之前遇到指定的终止字符,则提前结束读取;
如果读取成功则函数返回 true,如失败(遇文件结束符)则函数返回 false;

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

相关的istream类成员函数
eof() 函数:EOF 表示“文件结束”;从输入流读取数据,如果到达文件末尾(遇文件结束符),eof()函数值为真,否则为假;

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

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

文件流

C++ 提供了低级的I/O功能高级的I/O功能

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

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

文件流类与文件流对象
在 C++ 的I/O类库中定义了几种文件类,专门用于对磁盘文件的输入输出操作;
除了标准输入输出流类 istream、ostream 和 iostream 类外,还有 3 个用于文件操作的文件类:

  • ifstream类:它是从istream类派生的,用来支持从磁盘文件的输入
  • ofstream类:它是从ostream类派生的,用来支持向磁盘文件的输出
  • fstream类:它是从iostream类派生的,用来支持对磁盘文件的输入输出

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

创建文件流对象:
ifstream fin;:输入流对象;
ofstream fout;:输出流对象;

文件的打开与关闭

打开文件:
1) 调用文件流的成员函数open
ofstream outfile;:定义 ofstream 类(输出文件流类)对象 outfile;
outfile.open("f1.dat", ios::out);:使文件流与 f1.dat 文件建立关联;

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

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

方 式 作 用
ios::in 以输入方式打开文件
ios::out 以输出方式打开文件(这是默认方式),如果已有此名字的文件,则将其原有内容全部清除
ios::app 以输出方式打开文件,写入的数据添加在文件末尾
ios::ate 打开一个已有的文件,文件指针指向文件末尾
ios::trunc 打开一个文件,如果文件已存在,则删除其中全部数据,如文件不存在,则建立新文件
ios::binary 以二进制方式打开一个文件,如不指定此方式则默认为 ASCII 方式
ios::nocreate 打开一个已有的文件,如文件不存在,则打开失败
ios::noreplace 如果文件不存在则建立新文件,如果文件已存在则操作失败
ios::in │ ios::out 以输入和输出方式打开文件,文件可读可写
ios::in │ ios::binary 以二进制方式打开一个输入文件
ios::out │ ios::binary 以二进制方式打开一个输出文件

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

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

3) 可以用按位或运算符|对输入输出方式进行组合,比如下面一些例子:

  • ios::in | ios::noreplace:打开一个输入文件,若文件不存在则返回打开失败的信息
  • ios::app | ios::nocreate:打开一个输出文件,在文件尾接着写数据,若文件不存在,则返回打开失败的信息
  • ios::out | ios::noreplace:打开一个新文件作为输出文件,如果文件已存在则返回打开失败的信息
  • ios::in | ios::out | ios::binary:打开一个二进制文件,可读可写

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

4) 如果打开操作失败,open() 函数的返回值为 0,如果是用调用构造函数的方式打开文件的,则流对象的值为 0;

关闭磁盘文件
关闭文件用成员函数 close();如outfile.close();

ASCII文件的读写操作

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

2) 用文件流的 put()、get()、getline() 等成员函数进行字符的输入输出;

二进制文件的读写操作

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

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

用成员函数read和write读写二进制文件
istream & read(char *buffer, int len);
ostream & write(const char *buffer, int len);

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

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

几点说明:
1) 这些函数名的第一个字母或最后一个字母不是 g 就是 p;
带 g 的是用于输入的函数(g 是 get 的第一个字母,以 g 作为输入的标识);
带 p 的是用于输出的函数(p 是 put 的第一个字母,以 p 作为输出的标识);

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

2) 函数参数中的“文件中的位置”和“位移量”已被指定为 long 型整数,以字节为单位;“参照位置”可以是下面三者之一:

  • ios::beg:文件开头,这是默认值
  • ios::cur:指针当前的位置
  • ios::end:文件末尾

它们是在 ios 类中定义的枚举常量;举例如下:

  • infile.seekg(100);:输入文件中的指针向前移到第 100 字节位置
  • infile.seekg(50, ios::cur);:输入文件中的指针从当前位置后移 50 字节
  • outfile.seekp(-75, ios::end);:输出文件中的指针从文件末尾前移 75 字节

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

字符串流

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

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

在字符数组中可以存放字符,也可以存放整数、浮点数以及其他类型的数据;
在向字符数组存入数据之前,要先将数据从二进制形式转换为 ASCII 代码,然后存放在缓冲区,再从缓冲区送到字符数组;
从字符数组读数据时,先将字符数组中的数据送到缓冲区,在赋给变量前要先将 ASCII 代码转换为二进制形式;

总之,流缓冲区中的数据格式与字符数组相同;

这种情况与以标准设备(键盘和显示器)为对象的输入输出是类似的:
键盘和显示器都是按字符形式输入输出的设备,内存中的数据在输出到显示器之前,先要转换为 ASCII 码形式,并送到输出缓冲区中;
从键盘输入的数据以 ASCII 码形式输入到输入缓冲区,在赋给变量前转换为相应变量类型的二进制形式,然后赋给变量;

对于字符串流的输入输出的情况,如不清楚,可以从对标准设备的输入输出中得到启发;

文件流类有 ifstream、ofstream 和 fstream,而字符串流类有 istrstream、ostrstream 和 strstream;
文件流类和字符串流类都是 ostream、istream 和 iostream类的派生类;因此对它们的操作方法是基本相同的;

向内存中的一个字符数组写数据就如同向文件写数据一样,但有 3 点不同:

  • 输出时数据不是流向外存文件,而是流向内存中的一个存储空间;输入时从内存中的存储空间读取数据;
    在严格的意义上说,这不属于输入输出,称为读写比较合适;因为输入输出一般指的是在计算机内存与计算机外的文件(外部设备也视为文件)之间的数据传送;但由于 C++ 的字符串流采用了 C++ 的流输入输出机制,因此往往也用输入和输出来表述读写操作;
  • 字符串流对象关联的不是文件,而是内存中的一个字符数组,因此不需要打开和关闭文件;
  • 每个文件的最后都有一个文件结束符,表示文件的结束;而字符串流所关联的字符数组中没有相应的结束标志;用户要指定一个特殊字符作为结束符,在向字符数组写入全部数据后要写入此字符;

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

建立输出字符串流对象
ostrstream 类提供的构造函数的原型为:ostrstream::ostrstream(char *buffer, int n, int mode = ios::out);
buffer 是指向字符数组首元素的指针,n 为指定的流缓冲区的大小,第 3 个参数是可选的,默认为 ios::out 方式;

建立输入字符串流对象
istrstream 类提供了两个带参的构造函数,原型为:
istrstream::istrstream(char *buffer);
istrstream::istrstream(char *buffer, int n);
buffer 是指向字符数组首元素的指针,用它来初始化流对象(使流对象与字符数组建立关联);

建立输入输出字符串流对象
strstream 类提供的构造函数的原型为:
strstream::strstream(char *buffer, int n, int mode);

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