C++:数据流和缓冲区
阅读原文时间:2023年07月11日阅读:1

原文链接:http://hi.baidu.com/nicker2010/item/d0c4cd2a7caf27c4ddf69aeb

input stream用三个指针维护一个read缓冲区,这些指针可以透过eback(),gptr(),egptr()获得
**1.eback()(意思是 end back)是缓冲区的起始位置,或者回退区的尾端,如果不采取特殊措施,

      字符最多只能被回退到这个位置

    2.gptr()(意思是 get pointer)是当前的读取位置

    3.egptr()(意思是 end get pointer)是input buffer的尾端**

函数sungetc()(由input stream的unget()调用)或者

    sputbackc()(由input  stream的putback()调用)

    可以用来存储stream缓冲区最后一次读取前的状态

函数sgetc()sbumpc()可以读取一个字符。二者的差别在于:后者会让读取指针前进,而前者不会

如果缓冲区读取完毕(gptr() == egptr()),就不再有可用字符了,此时对缓冲区就必须重新获得补给

    这项工作可以由虚函数 underflow()完成,它会负责读取数据

如果没有字符,函数sbumpc()会调用虚函数uflow(),而uflow()的缺省行为就是调用underflow(),

    并移动(前进)读取指针。基类basic_streambuf中对underflow()的缺省行为是返回EOF

函数sgetn()用于一次读取多个字符,这个函数把任务交给xsgetn(),

    后者的缺省做法是多美歌字符调用sbumpc().我们可以改进xsgetn()来优化对多个字符的读取

**对input缓冲区,要么就必须建立一个缓冲区,要么就至少实现underflow()和uflow()

    任何一个具备字符读取功能的stream缓冲区,都必须实现underflow()**

成员函数setg()可以建立一个read缓冲区,它有三个参数:

    1.一个指针,指向缓冲区头部(eback())

    2.一个指针,指向当前读取位置(gptr())

    3.一个指针,指向缓冲区尾部(egptr())

下面就实现一个我们自定义的input stream buffer:

extern "C"
{
int read(int fd,char* buf,int num);
}
class InBuffer : public std::streambuf
{
protected:
static const int bufferSize = 10; ///缓冲区大小
char buffer[bufferSize];
public:
/**建立read缓冲区,完全清空*/
InBuffer()
{
setg(buffer+4,buffer+4,buffer+4);
}
protected:
virtual int_type underflow()
{
if(gptr() < egptr()) return *gptr(); int numPutback = gptr()-eback(); if(numPutback > 4) numPutback=4;
/**把缓冲区最后几个字符移到头部,便于回退*/
std::memcpy(buffer+(4-numPutback), gptr()-numPutback, numPutback);
int num = read(0,buffer+4,bufferSize-4); ///读取数据
if(num <= 0) return EOF;
setg(buffer+(4-numPutback),buffer+4,buffer+4+num);
return *gptr();
}
};

void testMyInputBuffer()
{
InBuffer buf;
std::istream in(&buf);

char c;  
for(int i=0;i<20;++i)  
{  
    in.get(c);  
    cout<<c<<flush;  
    if(i == 10)  
    {  
        in.unget();  ///放回2个字符,即第9,10个字符被打印两次  
        in.unget();  
    }  
}  
cout<<endl;  

}

(2):C++对缓冲区的理解

**摘自:http://www.vckbase.com/document/viewdoc/?id=1897
**

什么是缓冲区

缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。

缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。

为什么要引入缓冲区

我们为什么要引入缓冲区呢?

比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。

又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。

现在您基本明白了吧,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。

缓冲区的类型

缓冲区 分为三种类型:全缓冲、行缓冲和不带缓冲。

1、全缓冲

在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是对磁盘文件的读写。

2、行缓冲

在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的I/O操作。典型代表是键盘输入数据。

3、不带缓冲

也就是不进行缓冲,标准出错情况stderr是典型代表,这使得出错信息可以直接尽快地显示出来。

缓冲区的刷新

下列情况会引发缓冲区的刷新:

1、缓冲区满时;

2、执行flush语句;

3、执行endl语句;

4、关闭文件。

可见,缓冲区满或关闭文件时都会刷新缓冲区,进行真正的I/O操作。另外,在C++中,我们可以使用flush函数来刷新缓冲区(执行I/O操作并清空缓冲区),如:

cout<<flush; //将显存的内容立即输出到显示器上进行显示

endl控制符的作用是将光标移动到输出设备中下一行开头处,并且清空缓冲区。

cout<<endl;

相当于

cout<<”\n” <<flush;

通过实例演示说明

1、文件操作演示全缓冲

创建一个控制台工程,输入如下代码:

#include using namespace std;int main(){ //创建文件test.txt并打开ofstream outfile("test.txt"); //向test.txt文件中写入4096个字符’a’for(int n=0;n<4096;n++){outfile<<'a';} //暂停,按任意键继续system("PAUSE"); //继续向test.txt文件中写入字符’b’,也就是说,第4097个字符是’b’outfile<<'b'; //暂停,按任意键继续system("PAUSE");return 0;}

上面这段代码很容易理解,已经在代码内部作了注释。

编写这段小代码的目的是验证WindowsXP下全缓冲的大小是4096个字节,并验证缓冲区满后会刷新缓冲区,执行真正的I/O操作。

编译并执行,运行结果如下:

此时打开工程所在文件夹下的test.txt文件,您会发现该文件是空的,这说明4096个字符“a”还在缓冲区,并没有真正执行I/O操作。敲一下回车键,窗口变为如下:

此时再打开test.txt文件,您就会发下该文件中已经有了4096个字符“a”。这说明全缓冲区的大小是4K(4096),缓冲区满后执行了I/O操作,而字符“b”还在缓冲区。

再次敲一下回车键,窗口变为如下:

此时再打开test.txt文件,您就会发现字符“b”也在其中了。这一步验证了文件关闭时刷新了缓冲区。

2、键盘操作演示行缓冲

先介绍getchar()函数。

函数原型:int getchar(void);

说明:当程序调用getchar()函数时,程序就等着用户按键,用户输入的字符被存放在键盘缓冲区中,直到用户按回车为止(回车字符也放在缓冲区中)。当用户键入回车之后,getchar()函数才开始从键盘缓冲区中每次读入一个字符。也就是说,后续的getchar()函数调用不会等待用户按键,而直接读取缓冲区中的字符,直到缓冲区中的字符读完后,才重新等待用户按键。

不知道您明白了没有,再通俗一点讲,当程序调用getchar()函数时,程序就等着用户按键,并等用户按下回车键返回。期间按下的字符存放在缓冲区,第一个字符作为函数返回值。继续调用getchar()函数,将不再等用户按键,而是返回您刚才输入的第2个字符;继续调用,返回第3个字符,直到缓冲区中的字符读完后,才等待用户按键。

如果您还没有明白,只能怨我表达能力有限,您可以结合以下实例体会。

创建一个控制台工程,输入如下代码:

#include using namespace std;int main(){char c;//第一次调用getchar()函数//程序执行时,您可以输入一串字符并按下回车键,按下回车键后该函数才返回c=getchar(); //显示getchar()函数的返回值cout<<c<<endl; //暂停system("PAUSE"); //循环多次调用getchar()函数//将每次调用getchar()函数的返回值显示出来//直到遇到回车符才结束while((c=getchar())!='\n'){printf("%c",c);} //暂停system("PAUSE");return 0;}

这段小代码也很简单,同样在代码内部都有注释。

getchar()函数的执行就是采用了行缓冲。第一次调用getchar()函数,会让程序使用者(用户)输入一行字符并直至按下回车键 函数才返回。此时用户输入的字符和回车符都存放在行缓冲区。

再次调用getchar()函数,会逐步输出行缓冲区的内容。

好了,本人表达能力有限,还是编译运行程序,通过运行结果自己领会吧。

编译运行程序,会提示您输入字符,您可以交替按下一些字符,如下:
 

您一直按下去,您就会发现当您按到第4094个字符时,不允许您继续输入字符。这说明行缓冲区的大小也是4K。

此时您按下回车键,返回第一个字符’a’,如下图:

继续敲一下回车键,将缓冲区的其它的字符全部输出,如下图:

3、标准错误输出不带缓冲

如错误输出时使用:

cerr<<”错误,请检查输入的参数!”;

这条语句等效于:

fprintf(stderr, ”错误,请检查输入的参数!”);

好了,就说到这吧,祝您好运,希望能对您有所帮助。

(3):内存日志系统

原文链接:http://www.cnblogs.com/JefferyZhou/archive/2010/07/06/1771802.html

在第一篇中主要由讨论日志的需求以及接口设计,这里阐述一下各个部分的实现过程。在设计过程中我们把整个系统分为三个部分:模块buffer, 线程buffer, 和内存日志系统接口。

首先我们需要明确的是我们的写入是正对线程来说,不同线程需要在不同的内存段进行写入操作,而线程内的写入操作是针对模块而来的。所以正在的写入操作在模块buffer里面,这里我们直接实现 一个前面设计的模块buffer:

//模块buffer
template
classIModuleBufferImp
{
public:
typedefunsigned int size_t;

staticconst unsigned int m\_snMaxBuffLen = nMaxBuffLen;

voidWriteLog( constchar\* szBufferCache)                                  //写日志  
{  
    WriteLog\_Imp(GetTimeSting().c\_str());  
    WriteLog\_Imp(szBufferCache);  
    WriteLog\_Imp("\\n");  
}

constchar\* GetMemLog()                                        //获取当前模块的日志信息  
{  
    m\_Buffer\[m\_snMaxBuffLen\] = 0;  
    returnm\_Buffer;  
}

constchar\* GetModuleName()const  
{  
    returnm\_szModuleName;  
}

staticIModuleBufferImp\* CreateModuleBuffer(constchar\* pszModuleName)  
{  
    IModuleBufferImp\* pModuleBuffer =new IModuleBufferImp(pszModuleName);  
    returnpModuleBuffer;  
}

staticvoid DestroyModuleBuffer(IModuleBufferImp\* pModuleBuffer)  
{  
    SAFE\_DELETE(pModuleBuffer);  
}

staticstd::string GetTimeSting()  
{  
    tm\* pTimeStruct = Time::GetLOCALTime();

    charszTempBuffer\[128\] = {0};

    sprintf\_s(szTempBuffer,"%.2d.%.2d.%.2d:%.2d:%.2d:%.10d  ",pTimeStruct->tm\_mon,pTimeStruct->tm\_mday,pTimeStruct->tm\_hour,pTimeStruct->tm\_min,pTimeStruct->tm\_sec, GetTickCount());

    returnstd::string(szTempBuffer);  
}  

protected:

explicitIModuleBufferImp(constchar\* pszModuleName)  
{  
    if(pszModuleName)  
    {  
        sprintf\_s(m\_szModuleName, MEMLOGSYS\_MAX\_MODULENAME\_LEN-1,"%s", pszModuleName);  
        m\_szModuleName\[MEMLOGSYS\_MAX\_MODULENAME\_LEN-1\] = 0;  
    }else  
    {  
        memset(m\_szModuleName, 0, MEMLOGSYS\_MAX\_MODULENAME\_LEN);  
        sprintf\_s(m\_szModuleName, MEMLOGSYS\_MAX\_MODULENAME\_LEN-1,"%s", MEMLOGSYS\_DEFAULT\_MODULENAME);  
    }

    m\_WriteOffSet = 0;  
    memset(m\_Buffer, 0, m\_snMaxBuffLen);  
}

~IModuleBufferImp()  
{  
    ;  
}

boolWriteLog\_Imp(constchar\* pszLog)  
{  
    if(pszLog)  
    {  
        intlen = strlen(pszLog);

        if(len >= m\_snMaxBuffLen)   //超过最大值了  
        {  
            returnfalse;  
        }  

        if(m\_WriteOffSet + len > m\_snMaxBuffLen)  
        {  
            memcpy(m\_Buffer+m\_WriteOffSet, pszLog, m\_snMaxBuffLen - m\_WriteOffSet);  
            memcpy(m\_Buffer,pszLog + (m\_snMaxBuffLen - m\_WriteOffSet), len-m\_snMaxBuffLen + m\_WriteOffSet);  
        }else  
        {  
            memcpy(m\_Buffer+m\_WriteOffSet, pszLog, len);  
        }

        m\_WriteOffSet += len;

        if(m\_WriteOffSet >= m\_snMaxBuffLen)  
        {  
            m\_WriteOffSet -= m\_snMaxBuffLen;  
        }

        returntrue;  
    }  
    returnfalse;  
}

private:
IModuleBufferImp(constIModuleBufferImp& rstIModuleBuffer){;}; //禁用
IModuleBufferImp& operator = (constIModuleBufferImp& rstIModuleBuffer) {;};//禁用

charm\_szModuleName\[MEMLOGSYS\_MAX\_MODULENAME\_LEN\];  
intm\_WriteOffSet;  
charm\_Buffer\[m\_snMaxBuffLen + 1\];  

};

上面我们提供了一个简单的工厂函数来创建模块buffer, 并置拷贝构造函数和赋值构造函数为私有防止恶意拷贝。

在上面的实现过程中,有几个小细节需要注意:

  1。 我们是需要一个线程安全的写入类,所以在类的实现中不能出现任何多线程共享的可写字段。

  2。 线程Buffer 是一个循环日志,必须保证最后记录的完整性,以及记录的循环可读性,

必须在写入记录的时候保证字符串结尾的0不屏蔽,数组中的有效内容。也就是在Write_Imp()里面用的是memcpy, 当然也可以使用 sprintf 系列,

(读者也可以自行改为使用sprintf系列,如果使用sprintf系列需要仔细阅读相关文档, 比如 sprintf_s 保证最后给你加一个 0, 超过缓冲区会报错获设置错误标志位 : 这里有一个小tips,sprintf_s ,不仅在你的缓冲区里面输入内容,而且在后面加一个0,不仅加一个0,还会对0后面的有效缓冲区拿来做其他用途)

3。 为了保证不破坏任何有效的完整记录,以及循环可读性,申请的缓冲区必须比可写缓冲区多一个字节用来存字符串结束符.

4。 把构造函数定义为保护,提供一个简单工厂函数,这里把缓冲区定义为一个数组,而不是一个动态申请的空间,然后,这个时候工厂函数全部是new出的模块buff,所有你定义任何大小的缓冲区,只要不操作当前系统最大可申请内存数量就不会出现错误。

Sign Clown 2010.7.6 0:59 HDPY

大概实现就是上面了。后面另外两个部分,线程buff,系统管理接口的具体实现,会陆续展示。

[本文原创,转载请注明出处,在文章末尾提供原文链接http://www.cnblogs.com/JefferyZhou/,否则一旦发现,将按字节每人民币收费,绝不论价]