`
java-mans
  • 浏览: 11414042 次
文章分类
社区版块
存档分类
最新评论

输入输出流,看似复杂却更好用----小话c++(2)

 
阅读更多

[Mac 10.7.1 Lion Intel-based x64 gcc4.2.1 xcode4.2]


Q:c++的默认输入输出和c的有什么优缺点?

A:任何一种语言都会有自身最推荐的一套模式,因为设计者可能已经很认真考虑了。没有必要说哪种最好,哪种很差,最好和很差都有条件限制的。不同的使用者总能找到一种最适合自身的方式。

c++的输入输出更面向对象化和模块化,它的操作其实蕴含着对于输入或者输出对象的操作,是输出一个整形,还是一个字符,或是一个自定义对象。而c中更能感受到函数和过程化的思维,输出没必要告诉使用者要使用一个对象,告诉我要输出什么,我就给你输出什么。

因为c++的对象化,所以不用像printf一样一定要传入一个字符串,而是将输出的变量单独写出;这就导致有时用c++的方式写起来更方便,而有的时候在控制复杂的输出的时刻显得更复杂,printf的格式字符串中一个小小的格式变化即可输出不同情况,而使用cout可能需要修改更多的代码。

c++的对象化以及重载运算符的方式,让输出一个对象的具体操作可以转移到对象内部,让对象自身来实现具体的输出;而printf默认没有此功能,程序员不得不在printf的时候加上具体对象输出逻辑或者封装一个输出对象数据的函数,这可能让程序员有时不能很好地记忆到底某个对象用什么输出函数,而c++将接口定义好了,就是<<,看起来更易于使用。

c语言中的printf函数的执行时采用动态解析格式字符串,根据是否是%d来输出一个整形,是否是%c来输出一个字符的方式,可能会导致运行时效率损失;而c++的输出方式,编译期间即可确定输出的是什么,这是printf效率在有时有可能低于cout的一个因素。

c++和c语言的输入输出混用有时可能导致一些意想不到的问题,主要在于c++库对于c库的部分依赖性。所以,要特别小心,如果对于内部的机制很不清楚,最好不要随便混用。


Q:对于输入输出中的各个类,它们之间到底是什么关系?

A:c++对于输入输出,有很多设计,它们共同形成了可以进行输入输出功能的简洁且易扩展的类设计图。下面将一一分析:

对于cout对象,它是ostream的一个对象,ostream的定义如下:

  typedef basic_istream<char> 		istream;	///< @isiosfwd
  typedef basic_ostream<char> 		ostream;	///< @isiosfwd
  typedef basic_iostream<char> 		iostream;	///< @isiosfwd
同样,包括istream和iostream的定义。对于istream, ostream和iostream,它们实现了输入和输出的具体功能(当然,它们内部还将调用其它内部函数实现最终的输入输出)。

basic_istream类的实现如下(部分):

  template<typename _CharT, typename _Traits>
    class basic_istream : virtual public basic_ios<_CharT, _Traits>
__istream_type& 
      operator>>(bool& __n)
      { return _M_extract(__n); }
      
      __istream_type& 
      operator>>(short& __n);
      
      __istream_type& 
      operator>>(unsigned short& __n)
      { return _M_extract(__n); }

      __istream_type& 
      operator>>(int& __n);
    
      __istream_type& 
      operator>>(unsigned int& __n)
      { return _M_extract(__n); }

      __istream_type& 
      operator>>(long& __n)
      { return _M_extract(__n); }
 
    __istream_type&
      get(__streambuf_type& __sb)
      { return this->get(__sb, this->widen('\n')); }
 
      int_type 
      peek();      

      __istream_type& 
      putback(char_type __c);
可以看出,它实现了输入一个变量以及获取输入流最后数据、返回给输入流数据等一些列操作。

如下是basic_ostream的实现(部分):

  template<typename _CharT, typename _Traits>
    class basic_ostream : virtual public basic_ios<_CharT, _Traits>
      __ostream_type& 
      operator<<(long __n)
      { return _M_insert(__n); }
      
      __ostream_type& 
      operator<<(unsigned long __n)
      { return _M_insert(__n); }	

      __ostream_type& 
      operator<<(bool __n)
      { return _M_insert(__n); }

      __ostream_type& 
      operator<<(short __n);
      __ostream_type& 
      put(char_type __c);

      __ostream_type& 
      flush();

可以看出,它实现了具体的输出不同类型数据以及刷新等操作。同时,也可以看到,它们均继承了basic_ios类,下面是它的部分实现:
  template<typename _CharT, typename _Traits>
    class basic_ios : public ios_base
      void
      clear(iostate __state = goodbit);

      bool
      good() const
      { return this->rdstate() == 0; }

      bool
      eof() const
      { return (this->rdstate() & eofbit) != 0; }

      bool
      fail() const
      { return (this->rdstate() & (badbit | failbit)) != 0; }

      bool
      bad() const
      { return (this->rdstate() & badbit) != 0; }

可以看出,basic_ios内部实现了basic_istream和basic_ostream类共同可能需要执行的地方:缓冲区状态等。basic_ios继承了ios_base类,它的部分实现如下:
  class ios_base
   
 inline streamsize
    precision() const { return _M_precision; }

    inline streamsize
    precision(streamsize __prec)
    {
      streamsize __old = _M_precision;
      _M_precision = __prec;
      return __old;
    }

    inline streamsize
    width() const { return _M_width; }

  inline ios_base&
  boolalpha(ios_base& __base)
  {
    __base.setf(ios_base::boolalpha);
    return __base;
  }
它主要实现了精度控制、宽度等一些更基本的流控制。

而对于iostream类,它继承了istream和ostream.

  template<typename _CharT, typename _Traits>
    class basic_iostream
    : public basic_istream<_CharT, _Traits>, 
      public basic_ostream<_CharT, _Traits>
可以看出,它可以实现istream和ostream的所有功能。还有,ifstream, ofstream, fstream, istrstream, ostrstream, strstream以及strstreambase和上面各个类的继承关系可以类似分析。

总的说来,按照输入输出的最终功能,将它的实现抽象出共同完成的部分以及单独完成的部分,即为上面的设计图。


Q:istrstream, ostrstream以及strstream,它们究竟要实现什么?

A: 把它们和c中的sprintf, sscanf联系起来就理解了。它们只不过是要实现如何对内存中一块所谓的字符串进行拼接或者取出的操作。不过c++已经将这几个类标记为废弃了,类似功能的类为stringstream, istringstream, ostringstream.如下举个例子:

因为mac下xcode对于stringstream的处理稍有bug,下面直接使用gcc4.2.1编译:

#include <iostream>
#include <sstream>

#define COUT_ENDL(str)  std::cout << #str << " is " << (str) << std::endl;

int main()
{
    std::stringstream s;
    int i = 128;
    char buf[32];
    int value;
    s << "hello" << " " << i;
    s >> buf;
    s >> value;
    COUT_ENDL(buf)
    COUT_ENDL(value)
    return 0;
}
保存为stringstream.cpp, 使用g++ stringstream.cpp -o stringstream编译,运行:
buf is hello
value is 128

其实,很简单,使用 <<将后面的数据存入字符串中,注意是用字符串格式,i是个整形依然被转换成字符串格式; >>将数据提取出来,到后面的变量中。


Q:感觉c++的输入输出的形式很多很多,而且有的地方可能会混淆一些操作,为什么会出现这种情况?

A: 出现这个问题的原因主要在于c++使用类的概念,而且输入输出牵扯到较多的类继承体系,每个类都可能有一些可以被调用的公共函数或者可使用的公共变量,导致最终使用子类进行操作的时候,同样可以使用基类的公共函数得到一些信息,这个可能导致可以操作输入输出流的方式极大增加。但是,不管有多少变形的方式,基本功能依然是那些:输入,输出,查询流状态,设置流参数和刷新。总而言之,不管你想封装多少函数,c++不会阻止你,不过链接器可能阻止最终的可执行文件的生成(如果最终的可执行文件超过系统限制).


Q:对于cout,形如cout << endl 这种形式为什么可以通过编译?

A: 这种形式和cout << boolalpha也是类似的。形如如下代码:

#include <iostream>

using namespace std;

int main (int argc, const char * argv[])
{
    bool b = true;
    cout << boolalpha << b << endl;
    return 0;
}
根据重载运算符,如下代码亦是可以的:
#include <iostream>

using namespace std;

int main (int argc, const char * argv[])
{
    bool b = true;
    cout.operator<<(boolalpha).operator<<(b).operator<<(endl);
    return 0;
}
那么boolalpha和endl到底是什么呢?

查看endl的声明和定义,发现如下,在basic_ostream类中:

  template<typename _CharT, typename _Traits>
    inline basic_ostream<_CharT, _Traits>& 
    endl(basic_ostream<_CharT, _Traits>& __os)
    { return flush(__os.put(__os.widen('\n'))); }
boolalpha的声明和定义,如下在ios_base类中:
  inline ios_base&
  boolalpha(ios_base& __base)
  {
    __base.setf(ios_base::boolalpha);
    return __base;
  }
既然如此,如下的代码就应该可以运行:
#include <iostream>

using namespace std;

int main (int argc, const char * argv[])
{
    bool b = true;

    boolalpha(cout);
    cout << b;
    endl(cout);

    return 0;
}
运行,和上面代码效果一致。从上可以看出,boolalpha和endl是函数,它们的原型分别为:
  inline ios_base&
  boolalpha(ios_base& __base);
  template<typename _CharT, typename _Traits>
    inline basic_ostream<_CharT, _Traits>& 
    endl(basic_ostream<_CharT, _Traits>& __os);

从basic_ostream类中可以找到如下函数:
      __ostream_type&
      operator<<(__ostream_type& (*__pf)(__ostream_type&))
      {
	// _GLIBCXX_RESOLVE_LIB_DEFECTS
	// DR 60. What is a formatted input function?
	// The inserters for manipulators are *not* formatted output functions.
	return __pf(*this);
      }

__ostream_type&
      operator<<(ios_base& (*__pf) (ios_base&))
      {
	// _GLIBCXX_RESOLVE_LIB_DEFECTS
	// DR 60. What is a formatted input function?
	// The inserters for manipulators are *not* formatted output functions.
	__pf(*this);
	return *this;
      }

typedef basic_ostream<_CharT, _Traits> __ostream_type;


这样就简单了,cout.operator<<(boolalpha)实际上是调用了cout的operator<<(ios_base &(*__pf)(ios_base &))函数,而cout.operator<<(endl)实际上是调用了cout的operator<<(__ostream_type &(*__pf)(__ostream_type &))函数。boolalpha和endl只不过是被当做函数指针传入当参数而已。内部将分别调用boolalpha(cout);和endl(cout);来实现最终功能。我觉得这么设计,其一好处无非是让使用者更分便, 到处使用<<即可,不用想着到底是重载哪个函数,内部已经为上层做好了转换。同时,对于操作子,最终不是cout在操作它,而是操作子在操作cout.

Q:cout << "\n";到底会不会刷新缓冲区?

A:对于行缓冲来说,它当然会刷新缓冲区。千万不要认为只有cout << endl;才会强制刷新缓冲区。如下代码,

#include <iostream>

using namespace std;

int main (int argc, const char * argv[])
{
    cout << "hello";
    cout << "ok\n"; 
    cout << endl;
    cout << "thanks";
    
    return 0;
}
在中间两条cout语句和return 0; 处打上断点,调试运行:

运行到cout << "ok\n"时,上面的"hello"并没有被输出,这说明在缓冲区中;当运行到cout << endl;时,hellook\n均输出了,这说明\n确实导致了刷新缓冲区。当然也有可能是缓冲区太小(虽然不太可能是实际的状态),把cout << "ok\n";改为cout << "ok";后再次调试到此,hellook并没有输出。运行到return 0;时发现thanks没有被输出,运行完,thanks才输出。

当然,可以肯定的是,使用\n可能不会刷新缓冲区,但是endl一定会刷新缓冲区。

  template<typename _CharT, typename _Traits>
    inline basic_ostream<_CharT, _Traits>& 
    endl(basic_ostream<_CharT, _Traits>& __os)
    { return flush(__os.put(__os.widen('\n'))); }

Q:经常使用cout的setf函数、flags函数以及使用操作子比如hex, dec等,这样可能造成一个后果:此时输出流的状态已经不是很确定了,怎么很好地区分它们的作用?

A:看源码是了解一个问题最直接的方式。

如下setf函数源码:

    inline fmtflags
    setf(fmtflags __fmtfl)
    {
      fmtflags __old = _M_flags;
      _M_flags |= __fmtfl;
      return __old;
    }
如下flags源码:
    inline fmtflags
    flags(fmtflags __fmtfl)
    {
      fmtflags __old = _M_flags;
      _M_flags = __fmtfl;
      return __old;
    }
其中_M_flags是ios_base的一个成员,保存流的一些配置信息,比如是否采用16进制输出,是否左对齐等等。可以看出,setf仅仅是将一种配置信息加入原有的配置信息中,而flags是完全替换了原有的配置信息。同时,也提供了unsetf和setf相搭配使用。如下是流的配置信息:
  enum _Ios_Fmtflags 
    { 
      _S_boolalpha 	= 1L << 0,
      _S_dec 		= 1L << 1,
      _S_fixed 		= 1L << 2,
      _S_hex 		= 1L << 3,
      _S_internal 	= 1L << 4,
      _S_left 		= 1L << 5,
      _S_oct 		= 1L << 6,
      _S_right 		= 1L << 7,
      _S_scientific 	= 1L << 8,
      _S_showbase 	= 1L << 9,
      _S_showpoint 	= 1L << 10,
      _S_showpos 	= 1L << 11,
      _S_skipws 	= 1L << 12,
      _S_unitbuf 	= 1L << 13,
      _S_uppercase 	= 1L << 14,
      _S_adjustfield 	= _S_left | _S_right | _S_internal,
      _S_basefield 	= _S_dec | _S_oct | _S_hex,
      _S_floatfield 	= _S_scientific | _S_fixed,
      _S_ios_fmtflags_end = 1L << 16 
    };
对于hex操作,举个例子:
#include <iostream>

using namespace std;

int main (int argc, const char * argv[])
{
    cout.setf(ios::hex);
    cout << 15 << endl;

    return 0;
}
程序输出:
15
setf并没有起到让输出流按照十六进制输出的作用,而如果使用cout << hex << 15 << endl; 就能够达到效果,为什么呢?

先看看cout << hex实际调用的hex函数的定义:

  inline ios_base&
  hex(ios_base& __base)
  {
    __base.setf(ios_base::hex, ios_base::basefield);
    return __base;
  }
可以看出,它和cout.setf(ios::hex);是不一致的。

看看,hex函数内部调用的setf函数的定义:

    inline fmtflags
    setf(fmtflags __fmtfl, fmtflags __mask)
    {
      fmtflags __old = _M_flags;
      _M_flags &= ~__mask;
      _M_flags |= (__fmtfl & __mask);
      return __old;
    }

_S_basefield 	= _S_dec | _S_oct | _S_hex,

static const fmtflags basefield = _S_basefield;可以看出, _M_flags &= ~__mask; 是将流状态中的_S_dec, _S_oct, _S_hex标志全部清除,然后加上特定的标志。而cout.setf(ios::hex);的方式仅仅是将十六进制标志加入流状态中,但是流状态中的十进制输出状态依然保留,导致依然按照十进制输出了。

了解了代码内部原理,外部不管发生了多么神奇的现象,都显得很单纯。

Q: 对于输入和输出流,clear和sync函数的区别是什么?
A: clear只表示流状态标志的清理。

      /**
       *  @brief  [Re]sets the error state.
       *  @param  state  The new state flag(s) to set.
       *
       *  See std::ios_base::iostate for the possible bit values.  Most
       *  users will not need to pass an argument.
      */
      void
      clear(iostate __state = goodbit);

而对于sync函数或者flush函数才真正将缓冲区数据进行刷新。


xichen

2012-5-31 11:47:56


分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics