众所周知,C++标准库提供的iostream提供3种形式的缓冲:
§ 不带缓冲区,这样与fwrite等操作一致(理论上就不是iostream了);
§ 空间自动管理的缓冲区,下一个可写位置总是为末边界+1;
§ 外置的缓冲区,如定义局部字符串数组,并将此区域传递给iostream。
在带缓冲区的情况下,iostream本身却不具体负责其相关功能,而把此任务交给了basic_streambuf及其派生类来负责,常见的如basic_filebuf(用于文件操作的缓冲区)和basic_stringbuf(用于字符串操作的缓冲区)。如上所述,iostream和streambuf之间的关系如图所示(iostream是basic_iostream的具体实例,如cout, cin):
basic_iostream的两大分支basic_istream和basic_ostream都是basic_streambuf的友元类,而basic_iostream包含了一个basic_streambuf的派生类的实例,该实例可以通过basic_iostream的成员函数rdbuf()来获取,并通过构造函数传入;我们经常使用的流操作符<<实际上是通过调用basic_streambuf的相关操作实现的。例如输出流的<<操作,其调用过程如图所示:
1)调用输出流的_M_put_char(不同实现的名字不同,此为STLport的版本);
2)_M_put_char函数通过记录的basid_streambuf实例,调用其sputc的方法;
3) basid_streambuf的sputc函数会把字符作为参数调用overflow操作;
4)在overflow中会调用无换成的系统函数fwrite来写到文件。
Overflow是basic_streambuf类的一个保护虚成员函数,在写指针达到缓冲区末尾是会激活一次(即溢出),其基本功能是:
1)如果写指针可写,则把该字符放到缓冲区,并改变写指针;
2)如果缓冲区是自动维护的,则为该字符分配新的空间,并存储到缓冲区中去;
3)处理本地化相关工作(不是关注的重点)。
基类basic_streambuf的overflow函数只是简单的返回traits_type::eof()即可,具体功能交给了派生类;对于basic_filebuf而言,overflow最重要的就是调用fwrite,把字符写到文件中。
同时必须注意到VC6版本的overflow调用fwrite时,一次只写入一个字符。
综上所述,我们要控制标准输入输出流的缓冲区行为,我们必须生成自己的basic_streambuf派生类,并生成一个该类的对象,传递给标准准输入输出流;由于基类basic_streambuf很多功能没有具体实现,为了方便,我们需要从basic_filebuf或者basic_stringbuf派生。
这里举的例子是为了实现超过指定上限后自动覆盖缓冲区内容:如最大值为6时, 7、8、9将覆盖1、2、3,对于默认basic_filebuf则会不断增长以满足需求;这种缓冲区对于日志操作很有帮助。
具体处理步骤为:
1) 从basic_filebuf派生自己的流缓冲类:SimpleBuffer
template <class charT, class traits = std::char_traits<charT> >
class SimpleBuffer: public std::basic_filebuf<charT, traits>
{
public:
//方便模板内部使用的类型定义
typedef charT char_type;
typedef traits traits_type;
typedef typename traits_type::int_type int_type;
typedef typename traits_type::off_type off_type;
typedef typename traits_type::pos_type pos_type;
//必须提供最大长度的构造函数
SimpleBuffer(std::streamsize sz)
: max_size (sz), cur_size (0) { }
private:
std::streamsize max_size;
std::streamsize cur_size;
};
私有成员max_size和cur_size分别用于记录最大字符数和当前的字符数。
2) 重载overflow函数,改变其溢出控制逻辑
//重载basic_streambuf::overflow实现对缓冲区的控制
virtual int_type overflow(int_type c = traits::eof())
{
std::streamsize len = pptr () - pbase ();
std::streamsize rem = cur_size + len - max_size;
std::basic_string<charT, traits> saved;
//如果当前字符串长度超出了最大剩余空间
if (rem > 0)
{
//计算剩余超出部分
//把真正先后面移动,实际上是一个输出缓冲区的大小
this->pbump(-rem);
//记录超出部分
saved.assign (pptr (), rem);
if (!traits_type::eq_int_type (c, traits_type::eof ()))
{
saved += c;
}
len = max_size - cur_size;
//由于已经记录了当前溢出的字符,所以调用基类的overflow时不需要把溢出字符传入
//则把溢出字符换成eof()
c = traits_type::eof ();
}
//调用基类的overflow,实现对输出缓冲区的写操作
const int_type ret = basic_filebuf<charT, traits>::overflow(c);
//cur_size += len;
cur_size ++;
if (cur_size == max_size)
{
cur_size = max_size + 1;
//既然超出了最大长度,则把指针移到首位
pubseekoff(0, std::ios_base::beg);
//复位计数器
cur_size = 0;
}
if (saved.size ())
{
//把保存的内容再放回到输出缓冲区中
xsputn(saved.data (), saved.size ());
}
return ret;
}
对于缓冲区自动管理的类型(默认情况),由于pptr和pbase为空,cur_size不能通过len来改变,而是默认cur_size ++处理;同时这种情况下,rem永远不会大于0,则上述代码中,控制逻辑主要体现在cur_size==max_size后的情况,即达到上限的情况,这时需要把写指针放到缓冲区的首位,调用basic_streambuf的pubseekoff函数实现,并把计数器复位。
3) 给出自己的输出流类,该类包含SimpleBuffer的缓冲区对象
template <class charT, class traits = std::char_traits<charT> >
class SimpleStream: public std::basic_iostream<charT, traits>
{
SimpleBuffer<charT, traits> buf;
public:
typedef charT char_type;
typedef traits traits_type;
typedef typename traits_type::int_type int_type;
typedef typename traits_type::off_type off_type;
typedef typename traits_type::pos_type pos_type;
SimpleStream(std::streamsize sz, const char* file)
: std::basic_iostream<charT, traits>(&buf), buf (sz)
{
this->init(&buf);
if (!buf.open (file, std::ios_base::out))
{
this->setstate (std::ios_base::failbit);
}
};
SimpleBuffer<charT, traits> *rdbuf () const
{
return (SimpleBuffer<charT, traits>*)&buf;
}
};
在指定文件名的构造函数中,完成了输出流的初始化,并打开文件。
这样就能实现刚才设计的功能了。
最后要说一下外置缓冲区的情况,如
char buf [33];
test.rdbuf ()->pubsetbuf (buf, sizeof buf);
通过basic_filebuf的成员函数pubsetbuf,理论上就完成了缓冲区设置,但是在VC6下,pptr和pbase取到的指针都是空的;要解决这个问题,可以通过定义setp的public方法来完成:
void setp(charT* pbeg, charT* pend)
{
basic_filebuf<charT, traits>::setp(pbeg, pend);
}
它实际上是调用basic_filebuf的保护成员setp实现的,把缓冲区的首和末指针赋值,并设定缓冲区不自动增长,这时候overflow函数会每写sizeof(buf)个字符激活一次,这样rem的if语句将起作用,并需要将cur_size的计算改为cur_size+= len了。
上述代码中,往文件中写的功能都是通过调用basic_filebuf的overflow来实现的;如果改成外置缓冲后,除最后一个溢出字符外,在VC6情况下,其它字符basic_filebuf不负责往文件中写(fwrite),所以需要特别处理;而STLport中的overflow实现,包含了写整个缓冲区内容,不必单独处理。
小结:编写自己流缓冲区情况一般适用于需要进行字符转换(如把小写转换为大写等等)和缓冲区大小的控制(对缓冲区空间严格要求),可以说是特殊场合使用;一般不需要进行如此操作。写自己的流缓冲区时,需要知道哪些函数需要重载(经常是overflow);应该从那个类派生(如basic_filebuf)。
注:例子在 VC6 下编译调试通过。