深入VC流
  “流”是VC的一大特色。依靠强大的流类库可以方便简洁的实现数据的输入输出,相信熟悉VC的用户一定深有感触。
  “流”是一是十分抽象的概念,尽管很多资料都对其有定义,但都没有把这个概念讲透,用抽象的语言来描述抽象的概念结果必定是模糊的。而且不难发现这么一个现象,无论

是基础的VC教程还是高层程序开发的资料,一旦涉及“流”都是从如何在实践编程中应用的角度来描述,我个人觉得有些不妥。本文的重点是对流系统的两个基类的探讨,旨在于

让读者在脑中形成关于流的具体思维模型,而不是对概念的探讨。
  本文分为将从三个部分来讲解。第一部分是原理篇,介绍VC流类库中与本文相关的重要部分,着重于两个基类,从整体建立对流的印象。第二部分是实验篇,通过二个简

单的程序采用边调试边解释原理的方式,从实践的角度来加深对流的认识。
下面就进入主题。
 

一.原理篇
  VC流类库的两个平行的基类streambuf类和ios类,所有其它的流类都是从它们直接或间接地派生出来的。Streambuf类是面向内存区域的隐含操作,而ios类是面向用户的

具体操作,两者内外结合共同构成VC流系统。
1.Streambuf类的基本框架如下图所示。 

 

   streambuf对象在配置缓冲区处理的时候划分一块固定(0x200bytes)的内存区域--保留区域,可以动态的划分一部分为输入缓冲区(程序中的输入是从这里取得数据),一部分为输

出缓冲区(程序中的输出是把数据存储在这里).不同对象的固定内存区域可以是交迭也可以是不交迭的.使用streambuf的保护成员函数,可以访问和操作输入指针和输出指针来实现

字符的取得与存储.缓冲区和指针的具体行为是依靠它的派生类来执行的.
   上面一段是本人对MSDN的译文,为了方便对比下面给出原文。
All streambuf objects, when configured for buffered processing, maintain a fixed memory buffer, called a reserve area, that can be dynamically partitioned 

into a get area for input, and a put area for output. These areas may or may not overlap. With the protected member functions, you can access and manipulate 

a get pointer for character retrieval and a put pointer for character storage. The exact behavior of the buffers and pointers depends on the implementation 

of the derived class.
   显然对流的操作过程中streambuf是处于基层与内存区域打交道(如设置缓冲区,对缓冲区指针的操作,对缓冲区的存储或取得字符等),因而常被称为提供物理设备的接口。
   对“流”的操作最终要化为对内存区域的操作,因而不可避免的调用streambuf类成为其它类的必然选择。由此可见streambuf的重要性,这就要求有健全的结构来保证。

2.保留区的结构

Streambuf对象对内存区域的操作不是任意的,而是指定的保留区域,当streambuf对象建立时会自动的分配保留区域并把该区域绑定到streambuf对象(当缓冲标志_fUnbuf=0时)


为了保证对保留区的操作,streambuf类提供了最基本的8个指针:

1).(char *)_base:保留区域首指针
2). (char *)_ebuf:保留区域尾指针
3). (char *)_pbase:输出缓冲区首指针
4). (char *)_pptr:输出缓冲区当前指针
5). (char *)_epptr:输出缓冲区尾指针
6). (char *)_eback:输入缓冲区首指针
7). (char *)_gptr:输入缓冲区当前指针
8). (char *)_egptr:输入缓冲区尾指针
 
显然,通过这些指针可以方便的实现对流指针的任意操作。
可以用图形的方式来表示: 

 

对示图的说明:
1)._base与_ebuf之间为内存保留区域,作为一个整体;
  _pbase与_epptr之间为动态分配的输出缓冲区,作为一个整体;
  _eback与_egptr之间为动态分配的输入缓冲区,作为一个整体;
2).输入缓冲区与输出缓冲区没有先后次序的关系,它们可以同时存在于同一个保留区中,也可以单独出现。例如定义
ifstream input;
input>>a;
此时保留区中只有输入缓冲区而不存在输出缓冲区(也可以看作存在,但大小为0)。这里只给出一个简单例子,到实验篇再做详细讨论。
3).保留区域是作为内存变量和终端之间相互通信的介质而存在的,这是非常值得注意的一点,在实验篇会非常清晰的显示出来。

3.ios类的基本框架如下图所示(只包含最重要的部分):

 

  ios类是所有流类的基类,它枚举了大量标志,如I/0状态标志,指针定位标志,格式状态标志等;定义了大量调整字符格式的函数,实现输入输出时格式化和非格式化之间的转

化。下面具体介绍ios类中的关键成员变量:

1).Static const long basefield:进制标识。初始时通常为(ios::oct||ios::dec||ios::hex )根据格式状态标志字,易得常量0X70;
2). Static const long adjustfield:对齐标识。初始时通常为(ios::left||ios::right||ios::internal)为常量0XE;
3). Static const long floatfield:浮点格式标识。初始时通常为(ios::scientific||ios::fixed)为常量0X1800;
4).streambuf* bp:指向streambuf对象。在具有缓冲性质的流中,其实所指的就是保留区域。
5).int state:用来标识I/O状态。(四种情况good\eof\fail\bad)
6).int ispecial\ospecial\isfx_special\osfx_special这四个是保留未使用。
7).int x_delbuf:用来标识当调用ios类的析构函数时是否自动删除该流对象的保留区域。非0,删除;0,不删除。
8).long x_tie:标识与当前流绑定的输出流。默认时cout是与cin绑定在一起的。
9).long x_flag:输入输出的状态标志字,用于格式输入输出。
10).int x_precision: 小数位数标识。
11).char x_fill:用于填充的字符。
12).int x_width:输出时设置域宽。
13).static long x_maxbit:未知。
14 ).static int x_curindex:未知。
15).x_statebuf:这是一个条件编译产生的变量,可能是指针也可能是数组,当为用户添加的自定义状态标志字分配索引的时候起作用。
  
   很明显ios类对输入输出时的格式转化作了大量工作,为其它类的实现提供了保证。
原理篇到此为止。总结一下,streambuf类和ios类是两个平行的基类,前者提供对保留区域的操作,后者提供输入输出时对格式的控制。只要抓住这一点,我想什么是“流”已经

不重要了,但如果只是这些还不够,浅显的影象还需要具体的实验来巩固。
(参考资料:MSDN)  

二.实验篇

调试环境:VC6.0
实验一
1)  .实验目的:熟悉调试环境中流的结构
2)  .实验过程及分析
1.#include <iostream.h>
2.int main()
3.{
4.  char a,b;
5.  cin>>a;
6.  cin>>b;
7.  cout<<a;
8.  cout<<b;
9.  return 0;
10.}
  
  在第4、7、9语句下短点,F5开始调试。因为实验的目的是观察流的状态,因而在watch窗口中添加cin以便观察。可以得到三种状态。
状态一如下:

 
 
上面是watch窗口cin下ios中的变量,对照着原理篇的介绍很容易理解。主要有两个参数值得注意一下,第一个是bp,指向streambuf对象,点击前面的“+”号可以进入子树后如图

2: 

 

显然此时保留区域的8个指针都为0是因为还没有初始化的原因,表明cin还不具有保留区域。第二个是x_tie(指向与这个流绑定的输出流,cout),进入其子树可以发现里面的结构

和图1是一样的,但需要注意的是两个bp指向的地址值。

状态二:
在状态一的基础上F5,随便输入两个字符,回车后到达状态二。此时ios的成员变量与图1一样。但bp的子树已经发生变化,如图3所示:
 
 

  对比图2与图3,很容易看出此时保留区域已经被分配,只存在输入缓冲区且从屏幕中取得用户的输入。此时_eback与_base指向的是同一个地址,表明保留区域在动态分配缓冲区

的时候是从首地址开始的;_base与_ebuf的差值是0X200,在原理篇中介绍streambuf保留区域的固定长度由此而得;_gptr与_eback的差值是3恰好是我刚才输入的长度(”m n”)

;_gptr与_egptr是差值为1,这也是缓冲区域保护的一种模型,防止_gptr的无限移动,一旦_gptr与_egptr指向同一地址就会返回EOF来表示输入缓冲区的流指针已经到达最底端。
  有时候最原始的观察所得到的结果能大大加深对问题本身的理解。
  状态一与状态二的区别就在于图2和图3的区别。接下来要作的事情是寻找变化的根源,这就要看源程序。差别只是”cin>>a;cin>>b;”,”cin”是在istream.h中定义的一个全局

的实例对象,”>>”是其中重载的操作符,这就足以让我们知道只有当调用cin即流对象的时候系统才会分配保留区域,由于cin是一个用于输入的实例对象,所以只是动态的分配

输入缓冲区而没有输出缓冲区,这也为原理篇中“调用streambuf类成为其它类的必然选择”提供一点事实的依据。
  此时x_tie子树与状态一相比并没有改变。

状态三:
  在状态二的基础上F5就来到状态三。
  此时ios的状态如图1,bp的状态如图2;此时有变化的是x_tie的子树,如图4所示:
 
 

  图4是x_tie->ios->bp所指向的streambuf的保留区域的结构。
  考虑到状态二与状态三的差别在于“cout<<a;cout<<b;”,因而需要观察cout流。在watch窗口添加cout,发现其bp所指向的streambuf对象的保留区域的指针数据完全一样。
  在ios类定义在X_tie指向一个与当前流绑定的ostream*类型的对象,显然与cin绑定的是cout.
  到这里应该基本达到这个实验的要求,但如果仔细一点就会发现当前屏幕上并没有如期望的输出,这也调试程序过程中经常遇到的问题,为了方便调试,此时通常会使用cerr。
  观察图4,可以发现_pbase(输出缓冲区头指针)与_base(保留区域头指针)都是指向应该输出的字符串,再回顾一下原理篇的保留区域结构图,答案显而易见,本应输出到屏

幕的字符在输出缓冲中滞留。
到此,保留区域的特性已经充分显露。调试会选择cerr是因为这个实例对象是无缓冲的,所以一旦用它来输出数据就不必到输出缓冲区而直接出现在屏幕。


实验二 

1).实验目的:有了实验一的基础且在实验一的最后部分输出缓冲区的缓冲特性已经明显,这里就来验证输入缓冲区的缓冲特性。
2).实验过程及分析。
1.#inlcude <iostream.h>
2.Int main()
3.{
4.  Char str1[20],str2[20];
5.  Cin>>str1;
6.  Cin>>str2;
7.  Return 0;
8.}
  

  在程序的第六行设断点,F5执行,在屏幕中输入“hello world!”,程序中断后保留区域的状态如图5所示。

 

  经过观察发现_base和_eback都指向输入的字符串(即保留区域的首地址)_egptr指向输入缓冲区的尾部,结合源程序再看_gptr的指向,可以知道字符串”hello”已经被送入到

内存变量,而” world”还只是处于输入缓冲区但没被读入,这样输入缓冲特性也就清晰了。