对于从C语言过渡到C++的筒子来说,输出流的格式控制是个头痛的事,printf
的字符串格式控制不香吗?确实香,很多C++的开源类库都实现了printf
的格式控制,函数签名大概类似Format(const std::string &, ...)
,甚至其他语言都借鉴了printf
这种格式控制方式。
而C++的输入输出是基于流的方式,类似于std::cout << "FlushHip height : [" << 3 << "]"
。C++的格式控制是针对输入输出流的,printf
的格式控制是针对字符串的。流的格式控制由操控器Manipulator来控制,操控器的功能很强大,比printf
的功能还强大。举个例子:
输出bool值
printf
:printf("%s", f ? "true" : "false")
;- 操控器:
std::cout << std::boolalpha << f
;
如果熟悉了操控器,你一定会喜欢上操控器来控制IO格式。
先说结论,操控器实际上是一个函数,这个函数通过设置流的状态来改变输入输出的解释方式。
经常使用的换行std::endl
就是一个操控器,类似的还有std::ends
, std::flush
。源码如下:
// MANIPULATORS
template<class _Elem,
class _Traits> inline
basic_ostream<_Elem, _Traits>&
__CLRCALL_OR_CDECL endl(basic_ostream<_Elem, _Traits>& _Ostr)
{ // insert newline and flush stream
_Ostr.put(_Ostr.widen('\n'));
_Ostr.flush();
return (_Ostr);
}
template<class _Elem,
class _Traits> inline
basic_ostream<_Elem, _Traits>&
__CLRCALL_OR_CDECL ends(basic_ostream<_Elem, _Traits>& _Ostr)
{ // insert null character
_Ostr.put(_Elem());
return (_Ostr);
}
template<class _Elem,
class _Traits> inline
basic_ostream<_Elem, _Traits>&
__CLRCALL_OR_CDECL flush(basic_ostream<_Elem, _Traits>& _Ostr)
{ // flush stream
_Ostr.flush();
return (_Ostr);
}
再看下std::boolalpha
和其他一些操控器的源码。通过setf, unsetf
来改变标志变量,从而达到控制格式的目的。
// MANIPULATORS
inline ios_base& __CLRCALL_OR_CDECL boolalpha(ios_base& _Iosbase)
{ // set boolalpha
_Iosbase.setf(ios_base::boolalpha);
return (_Iosbase);
}
inline ios_base& __CLRCALL_OR_CDECL noboolalpha(ios_base& _Iosbase)
{ // clear boolalpha
_Iosbase.unsetf(ios_base::boolalpha);
return (_Iosbase);
}
inline ios_base& __CLRCALL_OR_CDECL dec(ios_base& _Iosbase)
{ // set basefield to dec
_Iosbase.setf(ios_base::dec, ios_base::basefield);
return (_Iosbase);
}
inline ios_base& __CLRCALL_OR_CDECL defaultfloat(ios_base& _Iosbase)
{ // clear floatfield
_Iosbase.unsetf(ios_base::floatfield);
return (_Iosbase);
}
inline ios_base& __CLRCALL_OR_CDECL fixed(ios_base& _Iosbase)
{ // set floatfield to fixed
_Iosbase.setf(ios_base::fixed, ios_base::floatfield);
return (_Iosbase);
}
inline ios_base& __CLRCALL_OR_CDECL hex(ios_base& _Iosbase)
{ // set basefield to hex
_Iosbase.setf(ios_base::hex, ios_base::basefield);
return (_Iosbase);
}
inline ios_base& __CLRCALL_OR_CDECL hexfloat(ios_base& _Iosbase)
{ // set floatfield to hexfloat
_Iosbase.setf(ios_base::hexfloat, ios_base::floatfield);
return (_Iosbase);
}
std::boolalpha(std::cout)
可以这么用的,标准库为了方便,操纵器以不同的函数签名的函数重载了流操作符<<
,因此可以这样使用std::cout << std::boolalpha
。
typedef basic_istream<_Elem, _Traits> _Myt;
typedef basic_ios<_Elem, _Traits> _Myios;
_Myt& __CLR_OR_THIS_CALL operator<<(_Myt& (__cdecl *_Pfn)(_Myt&))
{ // call basic_ostream manipulator
_DEBUG_POINTER(_Pfn);
return ((*_Pfn)(*this));
}
_Myt& __CLR_OR_THIS_CALL operator<<(_Myios& (__cdecl *_Pfn)(_Myios&))
{ // call basic_ios manipulator
_DEBUG_POINTER(_Pfn);
(*_Pfn)(*(_Myios *)this);
return (*this);
}
_Myt& __CLR_OR_THIS_CALL operator<<(ios_base& (__cdecl *_Pfn)(ios_base&))
{ // call ios_base manipulator
_DEBUG_POINTER(_Pfn);
(*_Pfn)(*(ios_base *)this);
return (*this);
}
这些操纵器是没有带参数的,如果带参数则会复杂一些,这里看下std::setw
的实现。带参数的操控器都在头文件
中实现。
_MRTIMP2 _Smanip<streamsize> __cdecl setw(streamsize);
template<class _Elem,
class _Traits,
class _Arg> inline
basic_ostream<_Elem, _Traits>& operator<<(
basic_ostream<_Elem, _Traits>& _Ostr, const _Smanip<_Arg>& _Manip)
{ // insert by calling function with output stream and argument
(*_Manip._Pfun)(_Ostr, _Manip._Manarg);
return (_Ostr);
}
// TEMPLATE STRUCT _Smanip
template<class _Arg>
struct _Smanip
{ // store function pointer and argument value
_Smanip(void (__cdecl *_Left)(ios_base&, _Arg), _Arg _Val)
: _Pfun(_Left), _Manarg(_Val)
{ // construct from function pointer and argument value
}
void (__cdecl *_Pfun)(ios_base&, _Arg); // the function pointer
_Arg _Manarg; // the argument value
};
了解了上述源码之后,自己写一个操控器就简单多了。
<<
即可;<<
即可。不过一般都不需要我们重载操控器了,标准库中提供的操控器就足够我们处理绝大部分格式操作了。
这里例举出操控器,具体到每一个操控器的注意事项,具体用法,这里就不展开了。可以参考https://en.cppreference.com/w/cpp/io/manip。
for (int i = 1; i < 10; ++i)
for (int j = 1; j < i + 1; ++j)
std::cout << j << " * " << i << " = " << std::setw(2) << std::right << i * j << " \n"[i == j];
其他的太多了,一下想不来了,总之就是:多看文档,多看例子,自然而然就能游刃有余地使用这些东西了。