C++中操控器Manipulator控制IO格式浅析

背景

对于从C语言过渡到C++的筒子来说,输出流的格式控制是个头痛的事,printf的字符串格式控制不香吗?确实香,很多C++的开源类库都实现了printf的格式控制,函数签名大概类似Format(const std::string &, ...),甚至其他语言都借鉴了printf这种格式控制方式。

而C++的输入输出是基于流的方式,类似于std::cout << "FlushHip height : [" << 3 << "]"C++的格式控制是针对输入输出流的printf的格式控制是针对字符串的。流的格式控制由操控器Manipulator来控制,操控器的功能很强大,比printf的功能还强大。举个例子:

输出bool值

  • printfprintf("%s", f ? "true" : "false")
  • 操控器:std::cout << std::boolalpha << f

如果熟悉了操控器,你一定会喜欢上操控器来控制IO格式。

操控器在C++中实际上是什么

先说结论,操控器实际上是一个函数,这个函数通过设置流的状态来改变输入输出的解释方式

源码解析

经常使用的换行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
	};

自定义操控器

了解了上述源码之后,自己写一个操控器就简单多了。

  • 对于无参数的操控器,只需要按照上述签名实现,然后重载<<即可;
  • 对于有参数的操控器,需要把函数本身和参数作为返回值返回,同时,重载<<即可。

不过一般都不需要我们重载操控器了,标准库中提供的操控器就足够我们处理绝大部分格式操作了。

标准库中的操控器

概览

C++中操控器Manipulator控制IO格式浅析_第1张图片

这里例举出操控器,具体到每一个操控器的注意事项,具体用法,这里就不展开了。可以参考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];

其他

其他的太多了,一下想不来了,总之就是:多看文档,多看例子,自然而然就能游刃有余地使用这些东西了。

参考

  • https://en.cppreference.com/w/cpp/io/manip
  • C++标准库(第2版)

你可能感兴趣的:(C/C++技巧)