流操纵器如何工作?

时间:2011-01-08 12:33:00

标签: c++ iostream ostream

众所周知,用户可以像这样定义流操纵器:

ostream& tab(ostream & output)
{
    return output<< '\t';
} 

这可以在 main()中使用,如下所示:

cout<<'a'<<tab<<'b'<<'c'<<endl;

请解释一下这一切是如何运作的?如果运算符&lt;&lt; 将第二个参数作为指向函数的指针并返回 ostream&amp; ,那么请解释一下为什么有必要?如果函数没有接受并返回 ostream&amp; 但是它是 void 而不是 ostream&amp; 会出现什么问题?

另外有趣的是为什么“十进制”,“十六进制”操纵器会在我们之间不进行更改之前生效,但是应该始终使用用户定义的操纵器以便为每个流操作生效?

3 个答案:

答案 0 :(得分:22)

该标准在operator<<类模板中定义了以下basic_ostream重载:

basic_ostream<charT,traits>& operator<<(
    basic_ostream<charT,traits>& (*pf) (basic_ostream<charT,traits>&) );
  

效果:无。不像格式化的输出函数(如27.6.2.5.1中所述)。

     

返回:pf(*this)

该参数是一个指向函数的指针,该函数接受并返回对std::ostream的引用。

这意味着您可以将具有此签名的函数“流”化到ostream对象,并且它具有在流上调用该函数的效果。如果在表达式中使用函数的名称,则它(通常)转换为指向该函数的指针。

std::hex是一个std::ios_base操纵符,定义如下。

   ios_base& hex(ios_base& str);
  

效果:致电str.setf(ios_base::hex, ios_base::basefield)

     

返回:str。

这意味着将hex传输到ostream会将输出基本格式标记设置为以十六进制输出数字。操纵器本身不输出任何东西。

答案 1 :(得分:8)

它没有任何问题,除了没有重载&lt;&lt;运营商为它定义。 &lt;&lt;&lt;&lt;&lt;&lt;&lt;期待一个带有签名 ostream&amp;的操纵者(* FP)(ostream的&安培;)即可。

如果你给它一个类型为 ostream&amp;的操纵器(* fp)()你会得到编译器错误,因为运算符&lt;&lt;(ostream&amp;,ostream&amp;(* fp)()的定义)即可。如果您想要此功能,则必须重载&lt;&lt;操作员接受此类操纵者。

你必须为此写一个定义:
的ostream&安培; ostream :: operator&lt;&lt;(ostream&amp;(* m)())

请记住,这里没有任何神奇的事情发生。流库严重依赖于标准 C ++特性:运算符重载,类和引用。

既然您知道如何创建您描述的功能,那么我们就不会这样做了:

如果没有传递对我们试图操作的流的引用,我们就无法修改连接到最终设备的流(cin,out,err,fstream等)。函数(修饰符都只是具有花哨名称的函数)要么必须返回一个新的ostream,它与&lt;&lt;&lt;&lt;&lt;&lt;&lt;运算符,或通过一些非常丑陋的机制,找出它应该连接哪个ostream,否则修饰符右边的所有内容都不会进入最终设备,而是发送到函数/修饰符返回的任何ostream。

想想像这样的流

cout << "something here" << tab << "something else"<< endl;

真的意味着

(((cout << "something here") << tab ) << "something else" ) << endl);

其中每组括号都执行cout(写入,修改等),然后返回cout,以便下一组括号可以对其进行操作。

如果你的制表符修改器/函数没有引用ostream,它必须以某种方式猜测ostream剩下的是&lt;&lt;操作员执行其任务。你在使用cour,cerr,一些文件流......?函数的内部结构将永远不会知道,除非他们将这些信息传递给某些信息,为什么不如此简单,如何引用它。

现在要真正推动这一点,让我们来看看 endl 到底是什么以及&lt;&lt;&lt;&lt;&lt;&lt;&lt;我们正在使用的运算符:

此运算符如下所示:

  ostream& ostream::operator<<(ostream& (*m)(ostream&)) 
  {  
      return (*m)(*this);
  }

endl看起来像这样:

  ostream& endl(ostream& os)      
  {  
      os << '\n'; 
      os.flush();     
      return os;
  }

endl的目的是添加换行符并刷新流,确保流的内部缓冲区的所有内容都已写入设备。为此,首先需要为此流写一个'\ n'。然后它需要告诉流冲洗。 endl知道要写入和刷新哪个流的唯一方法是让操作员在调用它时将该信息传递给endl函数。就像我告诉你洗车一样,但从来没有告诉你在整个停车场里哪辆车是我的。你永远无法完成你的工作。你需要我把你的车交给我,或者我可以自己洗车。

我希望能够解决问题

PS - 如果你偶然发现我的车,请洗一下。

答案 2 :(得分:4)

通常,流操纵器会在流对象上设置一些标志(或其他设置),以便下次使用时,它将根据标志进行操作。因此,操纵器返回其传递的相同对象。当然,调用操纵器的operator<<重载已经具有此对象,因此您注意到,对于该情况,并不严格需要返回值。我认为这涵盖了所有标准操纵者 - 他们都返回了他们的意见。

但是,使用返回值,框架足够灵活,自定义流操纵器可以返回不同的对象,可能是其给定对象的包装器。然后,这个其他对象将从cout << 'a' << tab返回,并且可以执行内置ostream格式设置不支持的操作。

不知道你如何安排释放这个其他对象,所以我不知道这是多么实用。它可能必须是特殊的东西,就像由ostream本身管理的代理对象。然后操纵器只适用于主动支持它的自定义流类,这通常不是操纵器的重点。