稚久|为什么不是简单封装而是重新定义,C++|输入输出类库的由来

输入输出是任何一门编程语言很重要的部分 。 C考虑精简的需要(一些场合并不需要输入输出 , 如用于过程控制的单片机) , 使用I/O函数库 。 C++继承了此函数库库 , 但因为封装的考量及新类型引入的需要 , 重新定义了输入输出类 。
如果能把所有平常的“容器(receptacle)"(标准I/O函数、文件以及内存块)看做相同的对象 , 都使用相同的接口进行操作 , 这不是很好吗?这种思想是建立在输入输出流之上的 。 与C语言stdio(标准输入/输出)库中各式各样的函数相比 , 输人输出流使用起来更容易、更安全 , 有时甚至更高效 。
为什么不把C库封装成新的类呢?有时这是一种好的解决办法 。 例如 , stdio中定义的FILE为指向文件的指针 , 假定现在需要安全地打开文件并且不依赖用户调用fclose()来关闭它 , 便可以封装一个CFile类来解决它 , 但使用可变参数列表的fprintf()和scanf()函数就不一样了 。
可变参数列表列数(variableargumentlistfunction)使用运行时解释程序(runtimeinterpreter) 。 运行时解释程序是一段代码 , 它的作用是在运行时解析格式串(formatstring),以及提取并解释从可变参数列表中得到的参数 。 其中存在的问题包括:
1)解释程序需要全部加载 。 即使仅仅需要使用解释程序的一小部分功能 , 该解释程序的所有内容也都会被加载到可执行程序中 。 所以 , 如果在程序中仅仅使用printf("%c",'x');,那么程序包中所有的函数也都会被加载进来 , 包括打印浮点数和字符串的函数 。 没有标准选项可以减少程序使用的空间 。
2)因为解释是发生在运行时的 , 所以无法免除运行开销 。 这是很令人沮丧的 , 因为编译时所有的信息都存在格式串中 , 但是直到运行时刻才能对其进行求值 。 然而 , 如果能在编译时解析格式串中的变量 , 就可以产生直接的函数调用 , 速度比运行时解释程序更快(尽管printf()及同类函数已经很好地优化了) 。
3)因为格式串直到运行时才能求值 , 所以可以没有编译时错误检查 。 C++为尽早发现错误 , 就进行编译时错误检查做了许多工作 , 这使得代码的编写更加容易 。 把类型安全检查交给I/0库来完成似乎是欠妥的 , 尤其是进行大量I/O操作时 。
4)对于C++来说 , 最关键的问题是printf()函数族不具备可扩展性 。 设计它们的目的仅仅是用来处理C语言中的基本数据类型(char、int、float、double、wchar_t、char*、wchar_t*和void*)以及这些数据类型的变体 。 程序员也许会认为每次添加一个新类时 , 可以重载函数printf()和scanf()(以及它们用于处理文件和字符串的变体) , 但是请记住 , 重载函数的参数列表中参数的类型必须不同 , 然而printf()函数族把类型信息隐藏在可变参数列表和格式串中 。 对于一种语言如C++来说 , 如果设计它的目的是为了很容易地添加新的数据类型 , 那么这个限制是无法接受的 。
这些问题清楚地表明I/O是标准C++类库最重要的内容之一 。 由于"hello,world"几乎是每个程序员学习一门新语言时所编写的第1个程序 , 并且实际上每个程序都会用到I/O , 所以C++中的1/O类库必须非常易于使用 。 更大的挑战在于I/O类库必须适用于任何新的类 。 如此一来 , 这个基础类库在设计时就不能再是简单的对C的I/O函数库的封装了 。
流是一个传送和格式化固定宽度(fixedwidth)字符的对象 。 读者可以获得一个输入流(通过istream类的子类)、一个输出流(使用ostream对象)或者同时实现两种功能的流(使用从iostream派生的对象) 。 输入输出流类库提供了下面几种不同的类:用干文件输入输出的ifstream、ofstream和fstream , 用于标准C++中string类输入输出的istringstream、ostringstrearn和stringstream 。 所有的这些流类拥有几乎相同的接口 , 所以能够以统一的方式使用这些流类 , 不管操作对象是文件、标准1/O、内存区 , 还是string对象 。 这样单一的接口同样支持扩充和增加一些新定义的类 。 某些函数实现格式化命令 , 而某些函数以非格式化方式读写字符 。 前面提到的流类实际上是模板的特化(templatespecialization) , 就像标准string类是basic_string模板的特化 。 下图描述了输入输出流类继承体系中的基本类:


推荐阅读