Skip to content

Latest commit

 

History

History
322 lines (192 loc) · 18.8 KB

chapter17.md

File metadata and controls

322 lines (192 loc) · 18.8 KB

第十七章 输入、输出和文件

本章内容包括:

  • C++ 角度的输入和输出;
  • iostream 类系列;
  • 重定向;
  • ostream;
  • 格式化输出;
  • istream类方法;
  • 流状态;
  • 文件I/O;
  • 使用 ifstream 类从文件输入;
  • 使用 ofstream 类输出到文件;
  • 使用 fstream 类进行文件输入和输出;
  • 命令行处理;
  • 二进制文件;
  • 随机文件访问;
  • 内核格式化。

17.1 C++输入和输出概述

C++依赖于C++的I/O解决方案,而不是C语言的I/O解决方 案,前者是在头文件iostream(以前为iostream.h)和fstream(以前为 fstream.h)中定义一组类。

17.1.1 流和缓冲区

C++程序把输入和输出看作字节流。输入时,程序从输入流中抽取字节;输出时,程序将字节插入到输出流中。

输入流中的字节可能来自键盘,也可能来自存储设备(如硬 盘)或其他程序。同样,输出流中的字节可以流向屏幕、打印机、存储 设备或其他程序。流充当了程序和流源或流目标之间的桥梁。

C++程序只是检查字节流,而不需要知道字节来自何方。同理,通过使 用流,C++程序处理输出的方式将独立于其去向。因此管理输入包含两 步:

  • 将流与输入去向的程序关联起来;
  • 将流与文件连接起来。

换句话说,输入流需要两个连接,每端各一个。文件端部连接提供了流的来源,程序端连接将流的流出部分转储到程序中(文件端连接可以是文件,也可以是设备,如键盘)。同样,对输出的管理包括将输出流连接到程序以及将输出目标与流关联起来。这就像将字节(而不是水)引入到水管中(参见图17.1)。

image-20210816095844394

通常,通过使用缓冲区可以更高效地处理输入和输出。缓冲区是用作中介的内存块,它是将信息从设备传输到程序或从程序传输给设备的临时存储工具。

image-20210816100336241

C++程序通常在用户按下回车 键时刷新输入缓冲区。对于屏幕输出,C++程序通 常在用户发送换行符时刷新输出缓冲区。程序也可能会在其他情况下刷新输入,例如输入即将到来时,这取决于实现。

17.1.2 流、缓冲区和iostream文件

image-20210816100640816

  • cin对象对应于标准输入流;
  • cout对象与标准输出流相对应;
  • cerr对象与标准错误流相对应,可用于显示错误消息。这个流没有 被缓冲,这意味着信息将被直接发送给屏幕,而不会等到缓冲区填 满或新的换行符;
  • clog对象也对应着标准错误流。在默认情况下,这个流被关联到标 准输出设备(通常为显示器),这个流被缓冲;

17.1.3 重定向

标准输入和输出流通常连接着键盘和屏幕。重定向这个方法可以改变标准输入和标准输出。通过输入重定向(<)和输出重定向(>)可以将输入和输出重置为文件。在 UNIX和Linux中,运算符 2> 重定向标准错误。

17.2 使用cout进行输出

17.2.2 其他ostream方法

除了各种 operator<<() 函数外,ostream 类还提供了 put() 方法和write() 方法,前者用于显示字符,后者用于显示字符串。

在程序清单17.3的输出中各列并没有对齐,这是因为数字的字段宽度不相同。可以使用width成员函数将长度不同的数字放到宽度相同的字段中。

width() 方法只影响将显示的下一个项目,然后字段宽度将恢复为默认值:

cout << "#";
cout.width(12);

cout << 12 << "#" << 24 << "#\n";

输出:

image-20210816104442007

12被放到宽度为12个字符的字段的最右边,这被称为右对齐。然后,字段宽度恢复为默认值,并将两个#符号以及24放在宽度与它们的长度相等的字段中。

  1. 设置填充字符

在默认情况下,cout用空格填充字段中未被使用的部分,可以用 fill( )成员函数来改变填充字符。例如,下面的函数调用将填充字符改为星号:

cout.fill('*');
  1. 设置浮点数的显示精度

C++的默认精度为6位(但末尾的0将不显 示)。precision() 成员函数使得能够选择其他值。

  1. 在谈 setf()

image-20210816105318255

17.3 使用 cin 进行输入

17.3.3 其他istream类方法

get( )getline( ) 方法:

  • 函数 get(char&)get(void) 提供不跳过空白的单字符输入功能;
  • 函数 get(char*, int, char)getline(char*, int, char) 在默认情况下读取整行而不是一个单词。

image-20210816110518509

这里的重点是,通过使用 get(ch),代码读取、显示并考虑空格和可打印字符

假设程序使用的是 >>,那么代码将跳过空格,因此最后的输出就压缩成了:

IC++clearly.

并且程序由于>>运算符跳过了最后的换行符,所以 while 循环不会结束。

到达文件尾后(不管是真正的文件尾还是模拟的文件尾), cin.get(void) 都将返回值 EOF——头文件 iostream 提供的一个符号常量。

image-20210816113232207

这里应将 ch 的类型声明为 int,而不是 char,因为值 EOF 可能无法使 用 char 类型来表示。

image-20210816113531629

2.采用哪种单字符输入形式

假设可以选择 >>get(char &)get(void),应使用哪一个呢? 首先,应确定是否希望跳过空白。如果跳过空白更方便,则使用抽取运算符 >>

如果希望程序检查每个字符,则使用 get() 方法,例如,计算字数的程序可以使用空格来判断单词何时结束。

get() 方法中,get(char &)的接口更佳get(void)的主要优点是,它与标准C语言中的 getchar() 函数极其类似,这意味着可以通过包含iostream(而不是 stdio.h),并用 cin.get() 替换所有的 getchar(),用 cout.put(ch) 替换所有的 putchar(ch),来将C程序转换为C++程序。

3.字符串输入:getline()get()ignore()

getline( )成员函 数和get( )的字符串读取版本都读取字符串,它们的函数特征标相同(这 是从更为通用的模板声明简化而来的):

image-20210816114047447

第一个参数是用于放置输入字符串的内存单元的地址。第二个参数比要读取的最大字符数大1(额外的一个字符用于存储结尾的空字符, 以便将输入存储为一个字符串)。第三个参数指定用作分界符的字符, 只有两个参数的版本将换行符用作分界符。上述函数都在读取最大数目的字符或遇到换行符后为止。

get()getline() 之间的主要区别在于,get() 将换行符留在输入流中,这样接下来的输入操作首先看到是将是换行符, 而 gerline() 抽取并丢弃输入流中的换行符

第三个参数用于指定分界符,遇到分界字符后, 输入将停止,即使还未读取最大数目的字符。在默认情况下,如果在读取指定数目的字符之前到达行尾,这两种方法都将停止读取输 入。并且,get() 将分界字符留在输入队列中,而getline() 不保留。

ignore() 成员函数,该函数接受两个参数:一个是数字,指定要读取的最大字符数;另一个是字符,用作输入分界符。下面的函数调用读取并丢弃接下来的255个字符或直到到达第一个换行符

cin.ignore(255, '\n');

原型为两个参数提供的默认值为1和 EOF, 该函数的返回类型为 istream &

  1. 意外字符串输入

get(char *, int)getline( ) 的某些输入形式将影响流状态。与其他输 入函数一样,这两个函数在遇到文件尾时将设置 eofbit遇到流被破坏 (如设备故障)时将设置 badbit。另外两种特殊情况是无输入以及输入到达或超过函数调用指定的最大字符数。

假设输入队列中的字符数等于或超过了输入方法指定的最大字 符数。首先,来看getline() 和下面的代码:

char temp[30];
while (cin.getline(temp, 30))

getline( )方法将从输入队列中读取字符,将它们放到temp数组的元 素中,直到(按测试顺序)到达文件尾、将要读取的字符是换行符或存 储了29个字符为止。如果遇到文件尾,则设置eofbit;如果将要读取的 字符是换行符,则该字符将被读取并丢弃;如果读取了29个字符,并且下一个字符不是换行符,则设置failbit。因此,包含30个或更多字符的输入行将终止输入。

get(char *, int) 方法首先测试字符数,然后测试是否为 文件尾以及下一个字符是否是换行符。如果它读取了最大数目的字符,则不设置 failbit 标记。然而,由此可以知道终止读取是否是由于输入字 符过多引起的。可以用 peek()(下一节)来查看下一个输入字符。 如果它是换行符,则说明 get() 已读取了整行;如果不是换行符,则说明 get() 是在到达行尾前停止的。这种技术对getline() 不适用,因为 getline() 读取并丢弃换行符,因此查看下一个字符无法知道任何情况。然而, 如果使用的是 get(),则可以知道是否读取了整个一行。

image-20210816120636318

getline()get() 不同的是,read() 不会在输入后加上空值字符, 因此不能将输入转换为字符串read() 方法不是专为键盘输入设计的, 它最常与 ostream write() 函数结合使用,来完成文件输入和输出。该方法的返回类型为 istream &

image-20210816145101989

17.4 文件输入和输出

重定向虽然可以提供一些文件 支持,但它比显式程序中的文件I/O的局限性更大。另外,重定向来自操作系统,而非C++,因此并非所有系统都有这样的功能。

要写入文件,需要创建一个ofstream对象,并使用 ostream 方法,如 << 插入运算符或 write()。要读取文件,需要创建一个 ifstream 对象,并使用 istream 方法,如 >> 抽取运算符或 get()

17.4.1 简单的文件 I/O

程序写文件需要包含头文件 fstream,并这样做:

  • 声明并创建一个 ofstream 对象来管理输出流;
  • 将该对象和特定文件关联起来,open 方法;
  • 以 cout 的方式使用该对象,区别是输出将进入文件,而不是屏幕。

ostreamofstream 类的基类,因此可以使用所有的 ostream 方法,包括各种插入运算符定义、格式化方法和控制符。ofstream 类使用被缓冲的输出,因此程序在创建像 fout 这样的 ofstream 对象时,将为输出缓冲区分配空间。如果创建了两个 ofstream 对象,程序将创建两个缓冲区,每个对象各一个。

程序读取文件的要求与写入文件相似:

  • 创建一个 ifstream 对象来管理输入流;
  • 将该对象与特定的文件关联起来,open()方法;
  • 以使用 cin 的方式使用该对象。

输入和输出一样,也是被缓冲的,因此创建ifstream对象与fin一 样,将创建一个由fin对象管理的输入缓冲区。与输出一样,通过缓冲, 传输数据的速度比逐字节传输要快得多。

当输入和输出流对象过期(如程序终止)时,到文件的连接将自动 关闭。另外,也可以使用 close() 方法来显式地关闭到文件的连接。

关闭这样的连接并不会删除流,而只是断开流到文件的连接,但流管理装置仍被保留。关闭文件将刷新缓冲区,从而确保文件被更新。

17.4.2 流状态检查和 is_open()

如果一切顺利,则流状态为零(没有消息就是好消息)。其他状态都是通过将特定位设置为1来记录的。试图打开一个不存在的文件进行 输入时,将设置failbit位,因此可以这样进行检查:

fin.open(argv[file]);
if (fin.fail()) {...}  // open attempt failed

由于ifstream对象和istream对象一样,被放在需要bool类型的地方 时,将被转换为bool值,因此您也可以这样做:

fin.open(argv[file]);
if (!fin) {...} 

较新的C++实现提供了一种更好的检查文件是否被打开的方 法——is_open()方法:

if (!fin.is_open()) {...}  // open attempt failed

这种方式之所以更好,是因为它能够检测出其他方式不能检测出的 微妙问题:

image-20210816152004401

17.4.3 打开多个文件

可能要依次处理一组文件。例如,可能要计算某个名称在10 个文件中出现的次数。在这种情况下,可以打开一个流,并将它依次关 联到各个文件。这在节省计算机资源方面,比为每个文件打开一个流的效率高。 使用这种方法,首先需要声明一个ifstream 对象(不对它进行初始化),然后使用 open() 方法将这个流与文件关联起来。例如,下面 是依次读取两个文件的代码:

image-20210816152403503

17.4.4 命令行处理技术

文件处理程序通常使用命令行参数来指定文件。命令行参数是用户 在输入命令时,在命令行中输入的参数。

C++有一种让在命令行环境中运行的程序能够访问命令行参数的机制,方法是使用下面的 main() 函数:

int main(int argc, char *argv[])

argc 为命令行中的参数个数,其中包括命令名本身argv 变量为一个指针,它指向一个指向 char 的指针。例如:argv[0] 是一个指针,指向存储第一个命令行参数的字符串的第一个字符,依此类推。也就是说, argv[0]是命令行中的第一个字符串,依此类推。例子:

wc report1 report2 report3

argc 为4,argv[0]wcargv[1]report1,依此类推。

17.4.5 文件模式

文件模式描述的是文件将被如何使用:读、写、追加等。将流与文件关联时(无论是使用文件名初始化文件流对象,还是使用 open() 方法),都可以提供指定文件模式的第二个参数,下表给出了第二个参数的具体信息。

image-20210816154243582

ofstream fout("text", ios_base::out);                // 新写入
ofstream fout("text", ios_base::out|ios_base::app);  // 追加

注意,C++ mode 是一个open mode值,如 ios_base::in;而 c mode是相 应的 C模式字符串,如“r”。

image-20210816163125203 image-20210816163153980

17.4.6 随机存取

随机存取指的是直接移 动(不是依次移动)到文件的任何位置。

使用临时文件

开发应用程序时,经常需要使用临时文件,这种文件的存在是短暂的,必须受程序控制。创建临时文件、复制另一个文件的内容并 删除文件其实都很简单。首先,需要为临时文件制定一个命名方案,但如何确保每个文件都 被指定了独一无二的文件名呢?cstdio 中声明的 tmpnam() 标准函数可以帮助您。

char * tmpnam(char* pszName);

tmpnam() 函数创建一个临时文件名,将它放在 pszName 指向的C-风格字符串中。常量 L_tmpnamTMP_MAX(二者都是在cstdio中定义的)限制了文件名包含的字符数以及在确保当前目录中不生成重复文件名的情况下 tmpnam() 可被调用的最多次数。下面是生成10个临时 文件名的代码:

#include <cstdio>
#include <iostream>

int main() {
    using namespace std;
    cout << "This system can generate up to " << TMP_MAX
         << " temporary names of up to " << L_tmpnam
         << " characters.\n";
    char pszName[L_tmpnam] = {'\0'};
    cout << "Here are ten names:\n";
    for (int i=0; i<10; i++) {
        tmpnam(pszName);
        cout << pszName << endl;
    }
    return 0;
}

更具体地说,使用tmpnam( )可以生成TMP_NAM个不同的文件名,其中每个文件名包含 的字符不超过L_tmpnam个。生成什么样的文件名取决于实现。

17.5 内核格式化

17.6 总结

流是进出程序的字节流。缓冲区是内存中的临时存储区域,是程序 与文件或其他I/O设备之间的桥梁。

istream类定义了多个版本的抽取 运算符(>>),用于识别所有基本的C++类型,并将字符输入转换为这 些类型。get( )方法族和getline( )方法为单字符输入和字符串输入提供了 进一步的支持。同样,ostream类定义了多个版本的插入运算符 (<<),用于识别所有的C++基本类型,并将它们转换为相应的字符输 出。put( )方法对单字符输出提供了进一步的支持。wistream和wostream 类对宽字符提供了类似的支持。

fstream文件提供了将iostream方法扩展到文件I/O的类定义。ifstream 类是从istream类派生而来的。通过将ifstream对象与文件关联起来,可 以使用所有的istream方法来读取文件。同样,通过将ofstream对象与文 件关联起来,可以使用ostream方法来写文件;通过将fstream对象与文件 关联起来,可以将输入和输出方法用于文件。

要将文件与流关联起来,可以在初始化文件流对象时提供文件名, 也可以先创建一个文件流对象,然后用open( )方法将这个流与文件关联 起来。close( )方法终止流与文件之间的连接。类构造函数和open( )方法 接受可选的第二个参数,该参数提供文件模式。

seekg()seekp() 函数提供对文件的随机存取。这些类方法使得能够将文件指针放置到相对于文件开头、文件尾和当前位置的某个位置。tellg()tellp() 方法报告当前的文件位置。