第4章 输入输出流

处理一般的I/O问题,比仅仅使用标准I/O库函数并把它变成一个类需要做更多的工作。

如果能把所有平常的“容器(receptacle)”—标准I/O函数、文件以及内存块—看做相同的对象,都使用相同的接口进行操作,这不是很好吗?这种思想是建立在输入输出流之上的。与C语言stdio(标准输入/输出)库中各式各样的函数相比,输入输出流使用起来更容易、更安全,有时甚至更高效。

C++类库中的输入输出流类通常是C++初学者最先学习使用的部分。本章讨论输入输出流中比C语言中stdio更强大的功能,阐述了文件流、字符串流和标准控制台流。

4.1 为什么引入输入输出流

读者可能想知道以前的C库到底有什么不好。为什么不把C库封装成新的类呢?有时这是一种好的解决办法。例如,stdio中定义的FILE为指向文件的指针,假定现在需要安全地打开文件并且不依赖用户调用close()来关闭它,下面的程序可以实现这一目标:

第4章 输入输出流 - 图1

当在C语言中进行文件I/O时,是使用无保护的指向FILE struct的指针来完成有关操作,但这个类封装了文件结构指针,并且用构造函数和析构函数来确保指针被正确地初始化和清理。构造函数的第2个参数是文件打开模式,默认值为“r”即“只读模式”。

为了在文件I/O函数中使用这个指针的值,可以用存取访问函数(access function)fp()取得它。下面是这个成员函数的定义:

第4章 输入输出流 - 图2

第4章 输入输出流 - 图3

就像平常所做的一样,构造函数调用fopen(),而且要确保返回结果不为零,结果为零说明打开文件失败。如果文件不能正常打开,则抛出异常。

析构函数用来关闭文件,而存取访问函数fp()则返回指针f。下面是使用FileClass的一个简单例子:

第4章 输入输出流 - 图4

现在,创建一个FileClass对象并在普通的C文件I/O函数中通过调用fp()使用它。当用完这个对象之后就不需要再理会它了;当文件对象超出其作用域后,析构函数会关闭该文件。

虽然FILE指针是私有的,但它并不是特别安全,因为成员函数fp()可以检索它。既然惟一的作用似乎只是为了确保指针能被正确初始化和清除,那么为什么不把它设计成公有的或使用struct来代替呢?注意,当能够用函数fp()取得指针f的一个拷贝的时候,不能同时给f赋值—这项操作完全由类来控制。得到由fp()返回的指针后,客户程序员仍然能给结构元素赋值或对其进行进一步处理,所以从安全的角度对于FILE指针,与其确保其合法性还不如将其作为结构的固有成员。

如果需要得到完全的安全,就必须防止客户直接存取FILE指针。所有的常用文件I/O函数都必须作为成员函数封装在类中,使得借助于C语言能做到的每一件事,在C++类中均可做到:

第4章 输入输出流 - 图5

第4章 输入输出流 - 图6

这个类几乎包含了<cstdio>中所有的文件I/O函数。(不包含vfprintf(),它只是用来实现printf()成员函数。)

类File的构造函数和前面的例子相同,并且这个类还有一个默认的构造函数。如果想创建File对象数组,或把File对象作为另一个类的数据成员来使用,这时类的初始化操作不在构造函数中完成,而是发生在其所属的对象已经创建之后,在这些情况下默认构造函数是很重要的。

默认构造函数将私有FILE指针f设为0。但是在对f进行任何引用之前,必须对其进行检查以确保指针不为空。这项操作由成员函数F()完成,这个函数为私有成员函数,这样做的目的是只允许类中的其他成员函数调用它。(不想让用户直接访问类的FILE结构。)

无论如何这并不是一种糟糕的解决方法。这种方法能起很好的作用,甚至能设想为标准(控制台)I/O和内核格式化(in-core formatting)(读/写一个内存块,而不是文件或控制台)构造相似的类。

在这里遇到的绊脚石是用于可变参数列表函数(variable argument list function)的运行时解释程序(runtime interpreter)。运行时解释程序是一段代码,它的作用是在运行时解析格式串(format string),以及提取并解释从可变参数列表中得到的参数。产生这个问题有4个原因:

1)即使仅仅需要使用解释程序的一小部分功能,该解释程序的所有内容也都会被加载到可执行程序中。所以,如果在程序中仅仅使用printf("%c",'x');,那么程序包中所有的函数也都会被加载进来,包括打印浮点数和字符串的函数。没有标准选项可以减少程序使用的空间。

2)因为解释是发生在运行时的,所以无法免除运行开销。这是很令人沮丧的,因为编译时所有的信息都存在格式串中,但是直到运行时刻才能对其进行求值。然而,如果能在编译时解析格式串中的变量,就可以产生直接的函数调用,速度比运行时解释程序更快(尽管printf()及同类函数已经很好地优化了)。

3)因为格式串直到运行时才能求值,所以可以没有编译时错误检查。如果读者曾经为找出函数printf()中的错误而对其使用错误的数字或者参数类型进行测试,也许就对这个问题比较熟悉了。C++为尽早发现错误,就进行编译时错误检查做了许多工作,这使得代码的编写更加容易。把类型安全检查交给I/O库来完成似乎是欠妥的,尤其是进行大量I/O操作时。

4)对于C++来说,最关键的问题是printf()函数族不具备可扩展性。设计它们的目的仅仅是用来处理C语言中的基本数据类型(char、int、float、double、wchar_t、char、wchar_t和void*)以及这些数据类型的变体。读者也许会认为每次添加一个新类时,可以重载函数printf()和scanf()(以及它们的用于处理文件和字符串的变体),但是请记住,重载函数的参数列表中参数的类型必须不同,然而printf()函数族把类型信息隐藏在可变参数列表和格式串中。对于一种语言如C++来说,如果设计它的目的是为了很容易地添加新的数据类型,那么这个限制是无法接受的。