快捷搜索:  汽车  科技

c语言编好程序后怎么封装(C输入输出类库的由来)

c语言编好程序后怎么封装(C输入输出类库的由来)这些问题清楚地表明I/O是标准C 类库最重要的内容之一。由于"hello world"几乎是每个程序员学习一门新语言时所编写的第1个程序,并且实际上每个程序都会用到I/O,所以C 中的1/O类库必须非常易于使用。更大的挑战在于I/O类库必须适用于任何新的类。如此一来,这个基础类库在设计时就不能再是简单的对C的I/O函数库的封装了。4) 对于C 来说,最关键的问题是printf()函数族不具备可扩展性。设计它们的目的仅仅是用来处理C语言中的基本数据类型(char、int、float、double、wchar_t、char*、wchar_t*和void*)以及这些数据类型的变体。程序员也许会认为每次添加一个新类时,可以重载函数printf()和scanf()(以及它们用于处理文件和字符串的变体),但是请记住,重载函数的参数列表中参数的类型必须不同,然而printf()函数

输入输出是任何一门编程语言很重要的部分。C考虑精简的需要(一些场合并不需要输入输出,如用于过程控制的单片机),使用I/O函数库。C 继承了此函数库库,但因为封装的考量及新类型引入的需要,重新定义了输入输出类。

1 为什么要引入输入输出流类库

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

为什么不把C库封装成新的类呢?有时这是 一种好的解决办法。例如,stdio中定义的FILE为指向文件的指针,假定现在需要安全地打开文件并且不依赖用户调用fclose()来关闭它,便可以封装一个CFile类来解决它,但使用可变参数列表的fprintf()和scanf()函数就不一样了。

可变参数列表列数(variable argument list function)使用运行时解释程序(runtime interpreter)。运行时解释程序是一段代码,它的作用是在运行时解析格式串(format string) 以及提取并解释从可变参数列表中得到的参数。其中存在的问题包括:

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函数库的封装了。

2 输入输出流

流是一个传送和格式化固定宽度(fixed width)字符的对象。读者可以获得一个输入流(通过istream类的子类)、一个输出流(使用ostream对象)或者同时实现两种功能的流(使用从iostream派生的对象)。输入输出流类库提供了下面几种不同的类:用干文件输入输出的ifstream、ofstream和fstream,用于标准C 中string类输入输出的istringstream、ostringstrearn和stringstream。所有的这些流类拥有几乎相同的接口,所以能够以统一的方式使用这些流类,不管操作对象是文件、标准1/O、内存区,还是string对象。这样单一的接口同样支持扩充和增加一些新定义的类。某些函数实现格式化命令,而某些函数以非格式化方式读写字符。 前面提到的流类实际上是模板的特化(templates pecialization),就像标准string类是basic_string模板的特化。下图描述了输入输出流类继承体系中的基本类:

c语言编好程序后怎么封装(C输入输出类库的由来)(1)

类ios_base声明了所有流类共有的内容,不依赖于流所处理的字符类型。这些声明大部分是常量以及处理这些常量的函数。其他类是以基础字符类型为参数的模板。例如类istream,定义如下:

typedef basic_istream<char> istream;

C 中也用wchar_t来替换char定义了所有的输入输出流类。模板basic_ios定义了输入和输出通用的函数,但是这依赖于基础字符类型(几乎不使用它们)。模板basic_istream定义了一般的输入函数,basic_ostream定义了一般的输出函数。后面介绍的文件流类和字符串流类增加了特殊的流处理功能。

在输入输出流类库中,重载了两种运算符以简化输入输出流的使用。运算符<<常用作输入输出流的插入符(inserter),运算符>>常用作提取符(extractor)。提取符按照目标对象的类型解析输入信息。举例说明,可以使用cin对象,它是输入流,相当地C中的stdin(键盘输入),即可重定向标准输入(redirectable standard input)。在代码中包含头文件时,就会预定义这个对象。

所有的内置数据类型都重载了operator<<。程序员自己也可以重载operator<<。为了显示不同变量中的内容,读者可以与插入符<<一起使用cout对象。

(相当于标准输出(standard output),如输出到显示屏,同样地,cerr对象相当于标准错误输出(standard error))。

重载的插入符和提取符可以连续使用, 构成复杂的表达式, 使得写(和读)更容易。

为自己的类定义插入符和提取符, 就是重载相关的运算符以完成正确的操作, 即:

1) 第1个参数定义成流(输入为istream 输出为ostream)的非const引用。

2) 执行向/从流中插入/提取数据的操作(通过处理对象的组成元素)。

3) 返回流的引用。

输入输出流应该是非常量, 因为处理流数据将改变流的状态。通过返回流, 如前所述, 可

以将这些流操作链接成单一的语句。

举个例子, 考虑如何输出一个MM-DD-YYYY格式的Date类对象。下面的代码重载了插入符:

ostream& operator<< (ostream& os const Date& d) { char fillc = os.fill('0'); os<<setw (2)<<d.getMonth()<<'-' <<setw(2)<<d.getDay ()<< '-' <<setw{4)<<setfill(fillc)<<d.getYear (); return os; }

这个函数不能设为Date类的成员函数, 因为运算符<<左边的操作数必须是输出流。

ostream的成员函数fill()用于更换填充字符(padding character) 当输出域(field)的宽度大于输出数据长度时, 使用填充字符填充超出部分, 域宽由操纵算子(manipulator)setw()决定。使用"0" 作为前导填充字符, 所以显示10月之前的月份时, 如显示9月份为"09"。函数fill()返回原有的填充字符(默认为一个空格符), 以便在后面使用操纵算子setfill()恢复这个填充字符。

3 文件输入输出流

使用输入输出流类操纵文件比使用C语言中的stdio更容易、更安全。打开一个文件要做的全部工作就是创建一个对象(这是构造函数所做的工作)。不需要显式地关闭文件(尽管能使用成员函数close()来关闭文件),因为当对象超出作用域时析构函数会关闭文件。构造一个ifstream对象用于创建默认的输入文件。构造一个ofstream对象用于创建默认的输出文件。一个fstream对象既可以用于输入文件,也可以用地输出文件。

下图说明了适用于输入输出流类的文件流类:

c语言编好程序后怎么封装(C输入输出类库的由来)(2)

和以前一样, 这里实际使用的类都是由类型定义的模板的特化。例如, ifstream用来处

理char文件,定义如下:

typedef basic_ifstream<char> ifstream;4 输入输出流缓存

输入输出流类库为了与产生和消耗数据的输入输出流部分进行通信,并提供统一的接口并隐藏底层实现, 标准库把它抽象成一个类, 称为streambuf。每个输入输出流对象包含一个指向streambuf的指针。用户可以直接访问streambuf(对象类型依赖于被处理的内容是标准I/O、文件还是内存等。)。例如,可以把原始字节移入或移出streambuf,而不用通过封装它的输入输出流对其进行格式化而不用通过封装它的输入输出流对其进行格式化。这可以通过调用streambuf对象的成员函数来完成。

c语言编好程序后怎么封装(C输入输出类库的由来)(3)

在stream 的实现中,除了虚基类ios_base之外,所有的类内部都有一个streambuf streambuf 是一个虚基类(不能被实例化,因此所内部包含streambuf(这个虚基类而非其子类)的类也是虚基类),代表流对象内部的缓冲区,就是我们流操作中输入输出的内容在内存中的缓冲区。

c语言编好程序后怎么封装(C输入输出类库的由来)(4)

Streambuf有两个子类,分别是stringbuf 和 filebuf 这两个子类可以被实例化,我们常用的文件流和字符串流,内部的缓冲区就是这两个类。

我们平常使用到的流基本是标准输入输出流,文件流和字符串流。在每个流初始化的时候都会初始化相应的streambuf(其实是它的子类)用来缓冲数据。

当我们用文件或者字符串初始化流的时候,流内部会保存该文件和字符串的信息,而在内部实例化一个streambuf用来缓冲数据,些数据时,当缓冲区满的时候再将数据写到文件或者字符串,读数据时当缓冲区没有数据时从文件或字符串读数据到缓冲区。

在文件流这种情况下,streambuf 是为了避免大量的IO 操作

在字符串流的情况下,streambuf (其实是套在上面的流对象)是为了提供字符串的格式化读取和输出操作(想象字符串是你从键盘输入的数据)

因为streambuf 本身是个虚基类,不能实例化,所以要用streambuf 就需要自己继承streambuf 写一个新的类出来才能用,好在c 标准类库实现了两个子类stringbuf 和 filebuf 所以我们可以选stringbuf 来作为我们的数据缓冲对象(不选filebuf 是因为它的实现和文件紧耦合的,只适合文件流)

流对象有一个构造函数是通过streambuf 来构造:

stringbuf sb;istream is(&sb);

有了流对象我们就可以在流上进行各种输入输出操作,输入会从缓冲区读数据,输出会将数据写到缓冲区。

下面是缓冲区使用的情景:

考虑一个生产者,消费者的问题,线程A 生成的数据,线程B读取,可以解决的方案如下:

1) 设立全局变量,缓冲数据,A B都可以访问(在这种情况下,A 生产的时候要考虑缓冲区是否够用,B读取的时候要判断当前是否有有效数据可读,而且很难设计一个合理分配内存的缓冲区(想象A生产的数据有时很大,有时很小))

2) 网络通信(TCP UDP)

3) streambuf 登场,有了streambuf配合stream, A就像正常操作流一样往流对象里塞数据,而B 就像正常操作流一样从流里面读数据,不用关心其他问题,只要这两个流的sterambuf 是同一个对象。

代码:

#include <iostream> #include <streambuf> #include <sstream> #include <fstream> #include <string> #include <cstring> #include <memory> #include <thread> using namespace std; stringbuf buf; istream in(&buf); ostream out(&buf); bool flag = false; void threadb() { char data; while (true) { if (flag) { in >> data; cout << "thread B recv:" << data << endl; flag = false; } } } int main() { thread consumer(threadb); char data; while (true) { cin >> data; out << data; flag = true; } return 0; }

一般情况下,在C 中,iostream内的cin和cout是比scanf和printf慢的,这主要是为了同时兼容iostream和stdio,iostream与stdio的缓冲区被绑到了一起,以及cin和cout的stream是绑定在一起的,这使得cin和cout有额外的开销。

为了提高cin和cout的效率,我们可以取消iostream与stdio的同步,以及cin和cout的stream的绑定:

std::ios::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL); std::streambuf *fb = cin.rdbuf(); char ch = fb -> sbumpc();5 在输入输出流中定位

每种类型的输入输出流都有“下一个”字符从哪里来(如果是istream)或到哪里去(如果是ostream)的概念。在某些情况下,需要移动流的位置(通过设置“流指针”给流重新定位)。可以使用两种方式进行流定位:一种是使用称为streampos的流指针进行绝对流位置定位,另一种方式和标准C中用于处理文件的库函数fseek()相似,实现从文件头、文件尾或当前位置移动某个给定的字节数进行相对流定位。

用streampos进行绝对流位置定位,需要先调用一个“告知”函数,以便知逍流指针在流中的确切位置:对于ostream调用tellp(),对于istream调用tellg()("p"表示“写"的put,'"g"表示“读“折get)。这个函数返回一个streampos,当要问到流中定位流指针时要用到它,对ostream对象调用seekp(),对istream对象调用seekg()。第2种方法是相对定位,使用重载版本的seekp()和seekg()函数。函数的第1个参数是要移动的字符数目,这个数目可正可负,第2个参数是移动方向。

6 字符串输入输出流

字符串输入输出流类直接对内存而不是对文件和标准输出(设备)进行操作。它使用与cin及cout相同的读取和格式化函数来操纵内存中的数据。在早期的计算机中,内存储器是计算机的核心,所以这种功能的类型常常称为内核格式化(in-coreformatting)。

c语言编好程序后怎么封装(C输入输出类库的由来)(5)

ref:Bruce Eckel 《C 编程思想》

-End-

猜您喜欢: