第八章输入输出流inputoutputstream的基本概念及原理.docx
《第八章输入输出流inputoutputstream的基本概念及原理.docx》由会员分享,可在线阅读,更多相关《第八章输入输出流inputoutputstream的基本概念及原理.docx(92页珍藏版)》请在冰豆网上搜索。
![第八章输入输出流inputoutputstream的基本概念及原理.docx](https://file1.bdocx.com/fileroot1/2022-12/16/ee4fcaf4-48f9-4992-9fd9-55965fe19ef9/ee4fcaf4-48f9-4992-9fd9-55965fe19ef91.gif)
第八章输入输出流inputoutputstream的基本概念及原理
第八章输入输出流(input/outputstream)的基本概念及原理
8.1预定义数据类型的输入/输出
8.1.1基本情况及其优点
C++输入输出流的优点:
(一)重载运算符“<<”和“>>”能以函数重载的形式极大地扩大用途,在输入输出流中充分体现多态性。
C语言的输入/输出系统本来就灵活性大、功能比较完善。
但它有一个较大缺点:
无法处理众多的用户自定义数据类型(主要是类及其对象)。
例如,有一个结构类型exampl如下:
structexampl{
intj;
charstr[80];
}str_ex;
如欲输出此结构对象str_ex的两个成员的内容,因而笼统地使用如下输出语句printf,
printf(“%exampl”,str_ex);
则将会出现编译错误。
而C++的输出/输入系统则能很好地解决这个问题。
(二)类型安全(type-safe)
[例1]C语言输出语句中的类型错误(第一章已看过)
#include
voidmain()
{
inti=3;
doubled=4.4;
printf(“%d\t%f\n”,i,d);
}
运行结果:
34.400000对!
但如写错为:
printf(“%d\t%d\n”,i,d);
则编译时不出错,但运行结果错,为:
3-26214
4
但在C++中只须写cout<
就能得出
34.4,对!
始终不会出错!
(三)通过缓存增加功能。
(四)附带优点是书写方便以及显示中没有冗余字符,能自动略去浮点数尾数中的零(但如用户希望显示多余的零,也可以做到)。
C++的输入/输出系统是对流的操作,也即操作数据使其流向对象,或从对象流出。
什么是流?
流是从源头到目的的数据流动。
当键入字符时,字符从键盘流入程序中;当将数据写入磁盘文件中时,数据自程序流动至磁盘上。
C++输入/输出流库是使用继承方法建立起来的一个输入/输出类库,它具有两个平行的基类,即streambuf类和ios类。
所有其它流类都是从它们直接或间接地派生出来的。
streambuf类用于提供物理设备的接口,它提供缓冲或处理流的通用方法。
它作为一个虚基类,具有类层次如下:
streambuf
conbuf
filebuf
strstreambuf
图8.1
ios类及其派生类用于为用户提供使用流类的接口。
它使用streambuf完成检查错误的格式化输入/输出操作,并支持对streambuf的缓冲区进行输入/输出时的格式化或非格式化转换。
ios类作为流库中的一个虚基类,派生出许多派生类,其主要层次如下:
ios
istream
fstreambase
ostream
fstream
istream-withassign
ostream-withassign
iostream
iostream-withassign
图8.2
8.1.2预定义流(标准流)的基本原理
预定义输出输入流涉及较多的头文件有四个:
ios.h,istream.h,ostream.h和iostream.h。
下面分别介绍。
8.1.2.1输出流的基本概念
流输出运算符“<<”是在头文件ostream.h的classostream中定义的。
从图8.2可以看出,classostream是从classios中派生出来的。
因此下面先看一下用于定义classios的头文件ios.h。
先看ios.h:
/***
*ios.h-definitions/declarationsfortheiosclass.
***/
classios{
public:
enumio_state{goodbit=0x00,
eofbit=0x01,
failbit=0x02,
badbit=0x04};
enumopen_mode{in=0x01,
out=0x02,
ate=0x04,
app=0x08,
trunc=0x10,
nocreate=0x20,
noreplace=0x40,
binary=0x80};
enumseek_dir{beg=0,cur=1,end=2};
enum{skipws=0x0001,
left=0x0002,
right=0x0004,
internal=0x0008,
dec=0x0010,
oct=0x0020,
hex=0x0040,
showbase=0x0080,
showpoint=0x0100,
uppercase=0x0200,
showpos=0x0400,
scientific=0x0800,
fixed=0x1000,
unitbuf=0x2000,
stdio=0x4000};
staticconstlongbasefield;//dec|oct|hex
staticconstlongadjustfield;//left|right|internal
staticconstlongfloatfield;//scientific|fixed
ios(streambuf*);//differsfromANSI
virtual~ios();
inlinelongflags()const;
inlinelongflags(long_l);
inlinelongsetf(long_f,long_m);
inlinelongsetf(long_l);
inlinelongunsetf(long_l);
inlineintwidth()const;
inlineintwidth(int_i);
inlineostream*tie(ostream*_os);
inlineostream*tie()const;
inlinecharfill()const;
inlinecharfill(char_c);
inlineintprecision(int_i);
inlineintprecision()const;
……
//inlineoperatorvoid*()const;
operatorvoid*()const{if(state&(badbit|failbit))return0;return(void*)this;}
inlineintoperator!
()const;
……
protected:
ios();
ios(constios&);//treatasprivate
……
intstate;
longx_flags;
intx_precision;
charx_fill;
intx_width;
};
以后讨论到有关classios的问题时,可参照以上内容。
再看ostream.h:
/***
ostream.h-definitions/declarationsfortheostreamclass
***/
classostream:
virtualpublicios{
public:
ostream(streambuf*);
virtual~ostream();
ostream&flush(……);
ostream&endl(……);
…………
inlineostream&operator<<(ostream&(__cdecl*_f)(ostream&));
inlineostream&operator<<(ios&(__cdecl*_f)(ios&));
ostream&operator<<(constchar*);
inlineostream&operator<<(constunsignedchar*);
inlineostream&operator<<(constsignedchar*);
inlineostream&operator<<(char);
ostream&operator<<(unsignedchar);
inlineostream&operator<<(signedchar);
ostream&operator<<(short);
ostream&operator<<(unsignedshort);
ostream&operator<<(int);
ostream&operator<<(unsignedint);
ostream&operator<<(long);
ostream&operator<<(unsignedlong);
inlineostream&operator<<(float);
ostream&operator<<(double);
ostream&operator<<(longdouble);
ostream&operator<<(constvoid*);
ostream&operator<<(streambuf*);
inlineostream&put(char);
ostream&put(unsignedchar);
inlineostream&put(signedchar);
ostream&write(constchar*,int);
inlineostream&write(constunsignedchar*,int);
inlineostream&write(constsignedchar*,int);
ostream&seekp(streampos);
ostream&seekp(streamoff,ios:
:
seek_dir);
streampostellp();
protected:
ostream();
ostream(constostream&);//treatasprivate
ostream&operator=(streambuf*);//treatasprivate
ostream&operator=(constostream&_os){returnoperator=(_os.rdbuf());}
intdo_opfx(int);//notused
voiddo_osfx();//notused
private:
ostream(ios&);
ostream&writepad(constchar*,constchar*);
intx_floatused;
};
附注:
在C++的老版本中,_Cdecl是预定内部宏,供编译系统使用。
当它有定义(例如等于1)时,标示只选择C++语言而不选择Pascal语言。
而另一个宏_Pascal有定义时,则标示同时选择Pascal语言。
classostream_withassign:
publicostream{
public:
ostream_withassign();
ostream_withassign(streambuf*_is);
~ostream_withassign();
ostream&operator=(constostream&_os){returnostream:
:
operator=(_os.rdbuf());}
ostream&operator=(streambuf*_sb){returnostream:
:
operator=(_sb);}
};
externostream_withassigncout;
externostream_withassigncerr;
externostream_withassignclog;
从以上文件看出,classostream中所定义的各个流输出运算符“<<”是相对于各个预定义数据类型进行重载的。
也即,他们可用于各种预定义数据类型。
试看以下我们很熟悉的例子:
[例1]用于三种最常用预定义数据类型的输出运算符
//out_1.cpp
//copiedfromp.341ofWang'sbook
#include
voidmain()
{
inti=10,j=45;
doublex=12.34,y=56.78;
char*str="Windows";
cout<<"i="<
cout<<"x="<cout<<"str="<}
/*Results:
i=10,j=45
x=12.34,y=56.78
str=Windows
*/
在以上程序中,输出流对象cout顺序地调用流输出运算符”<<”,将一条语句中的多个不同类型的数据依次输出。
输出流对象cout还可用于输出各类函数的运行结果,如下例:
[例2]输出各类函数的运行结果
//out_2.cpp
#include
intfun1()
{
intj=111;
returnj;
}
doublefun2()
{
doublej=13.57;
returnj;
}
voidmain()
{
cout<<"Thefirstfunhas"<<<"thesecondfunhas"<}
/*Results:
Thefirstfunhas111;thesecondfunhas13.57
*/
从图8.2和ostream.h中可看出,从classostream中派生出classostream-withassign,而ostream-withassign建立了三个对象:
cout、clog和cerr。
这就是标准库中定义的三个输出流对象。
其中cout是标准的输出流对象,而cerr和clog则与标准错误流有关。
其中,cout和clog是缓冲输出流(bufferedoutputstream)对象,发送给它们的数据暂存在缓存内,只当满足§8.1.2.3中所述条件之一时才将数据输出至标准设备;而cerr是非缓冲输出流对象,发送给它的任何数据在执行完一条语句后立即输出。
现在简要地看一下输出流对象cout、clog和cerr级联地运行的过程。
从以上图8.2和ostream.h中可看出:
cout、clog和cerr都是classostream-withassign的对象,而classostream-withassign是从classostream中派生出来的,因此classostream是这些输出流对象的基类。
以cout<<为例,这是对象cout调用函数operator<<(),也即cout.operator<<()。
按照支配规则,也即对象cout调用了基类的成员函数operator<<()。
函数operator<<()原型的返回值是classostream对象的引用,而实际上则返回classostream的派生类classostream-withassign的对象cout的引用。
此返回的对象可继续调用下一个函数operator<<()。
直至整条语句末尾。
如只从表面看,有些现象看不清楚,见以下两例:
[例1]变量值出错
//out_10.cpp
//trapofcout,clogandcerrstatements
#include
voidmain()
{
inti=10;
cout<<"firsti="<
}
/*Results:
firsti=11,secondi=10
*/
以上程序中,原来指望所显示的两个i都等于10,结果不然。
为何?
[例2]第七章§7.1“程序运行错误及其处理”中[例1]程序sqrt_negative_1.cpp。
该程序中主函数第二语句“cout<<"Sqrtof-4is"<[例3]有些输出语句并不立即输出数据。
//out_4.cpp
//Touseendl()functiontodisplayoutputcontents
#include
#include//forgetch()
voidmain()
{
char*c12="12345";
cout<getch();
cout<<"ABCDEFG"<}
/*Results:
只在击键一次之后,才能显示以下内容:
12345ABCD12345ABCDEFG
*/
以上程序中,主函数的第二语句运行完后,并无输出,也即屏幕上并无显示。
何故?
这些都要求、也只能从汇编语言的更深层次来解释。
整句cout、clog或cerr输出语句的运行都可分为三个阶段:
入栈阶段、缓存阶段和输出阶段。
详述如下。
第一阶段---入栈阶段
此阶段由主函数main()完成。
主函数按照逆顺序将整条输出语句中需要输出的各项数据或数据的存放地址压入栈区。
其中:
(a)如果数据是单个数据(无论预定义类型或是用户自定义类型),则直接将该项数据压入栈区。
其中整型数据或单个字符占4个字节(堆栈按照4字节边界原则运行,4-byteboundary),double型数据占8个字节,类的对象则可能占更多空间,等等。
(b)
(b)如果数据是数组,则系统将该数组放置于数据区内,并将此数组首址压入栈区。
(c)如果数据是一个函数运行后的返回值,则系统执行该函数,并根据该函数返回只的数值类型,按照上两项规定来决定将该数据或是数据地址压入栈区。
第二阶段---缓存阶段
此阶段由重载运算符函数<<完成。
它取出栈区内所存数据或按数据地址找到数据本身,并将该项完整的数据存入位于堆区内的缓存。
如缓存是空的,则该数据被放置于缓存的首址(例如§8.1.2.2“非缓冲输出流”);如缓存内已存有其它数据,则该数据被放置于缓存内已有数据之后(例如§8.1.2.3“缓冲输出流”)。
第三阶段---输出阶段
此阶段由另一些重载运算符函数<<调用flush函数来完成,它们最终调用硬件接口的函数__imp__WriteFile,直接将数据输出至标准设备(显示屏幕)。
由于函数__imp__WriteFile无法访问堆区,因此必须再将数据从堆区内再转存至栈区内,才能供函数__imp__WriteFile调用后输出。
flush函数顺序地从缓存(堆区)内取出所存全部数据,从新压入至栈区另外的地址,以备输出。
接着函数__imp__WriteFile直接将位于栈区新地址处的全部数据输出至标准设备(显示屏幕)。
下面将分别介绍缓冲输出流对象和非缓冲输出流对象,它们在执行输出语句时的差别在于:
(1)每当非缓冲输出流对象cerr语句运行时,在入栈阶段之后,它调用重载运算符函数<<,接连执行缓存阶段和输出阶段,直接将全部数据输出至标准设备(显示屏幕)。
(2)每当缓冲输出流对象cout或clog调用重载运算符函数<<时,一般情况下只执行入栈阶段和缓存阶段,只将数据存入缓存而不输出数据。
只当满足§8.1.2.3中所述条件之一时才执行输出阶段的操作,通过运算符函数<<,从堆区中的缓存内取出所存全部数据,调用用于控制硬件接口的函数__imp__WriteFile,直接将全部数据输出至标准设备(显示屏幕)。
8.1.2.2非缓冲输出流的运行机理
cerr是非缓冲输出流对象,发送给它的所有数据在整条输出语句(不是半条语句)运行后立即输出至标准设备。
[例1]非缓冲输出流对象cerr一次将整条语句的数据顺序地输出至标准设备。
//out_3.cpp
//"cerr"outputsthecontentsofonestatementimmediately
#include
intfun1()
{
intj=512;
returnj;
}
doublefun2()
{
doublej=12.34;
returnj;
}
voidmain()
{
cerr<<"Thefirstresultis"<<<"thesecondresultis"<}
/*Results:
Thefirstresultis512;thesecondresultis12.34
*/
以上程序out_3.cpp中只有一句cerr输出语句,整个语句的运行分为两大步:
第一大步就是入栈阶段;第二大步包括缓存阶段和输出阶段。
第一大步---入栈阶段
主函数按照逆顺序将整条语句中需要输出的各项数据或数据的存放地址压入栈区。
其顺序操作如下:
(a)系统执行函数fun2(),其运行结果为12.34。
此数据被压入栈区,占8个字节。
(b)
(b)第二项被压入栈区的数据是字符串"thesecondresultis"在数据区内的地址(0042a038)。
(c)第三项被压入栈区的数据是字符';'(其ASCII码值为3Bh),占4个字节。
(d)系统接着执行函数fun1(),其运行结果512被压入栈区,占4个字节。
(e)第五项也即最后一项被压入栈区的数据是字符串"thefirstresultis"在数据区内的地址(0042a01c)。
Table_out_1:
程序out_3.cpp入栈阶段后的栈区内容
0x0042a01c
512
3Bh
0x0042a038
12.34
低地址
高地址
请注意:
以上数据压入栈区的顺