所谓操控器是一种专门用来操控 stream 的一种对象,通常它只会改变输入的解释方式或输出的格式化方式。带实参的操控器被定义在 文件中,如 setw 等;不带实参的操控器包括 endl、flush。
操控器的实现原理其实是函数指针。在 ostream 中定义了 operator<< 的多种重载,其中包括:
__ostream_type& operator<<(__ostream_type& (*__pf)(__ostream_type&)) { return __pf(*this); }
我们通过传入不同的操控器函数指针,即可触发不同的处理。以 endl 为例:
template<typename _CharT, typename _Traits> inline basic_ostream<_CharT, _Traits>& endl(basic_ostream<_CharT, _Traits>& __os) { return flush(__os.put(__os.widen('\n'))); }
widen 函数用于国际化相关操作。
参考标准库中操控器的定义,我们可以自定义操控器用于读取忽略一行中的剩余输入:
#include <iostream> #include <limits> using namespace std; template<typename _CharT, typename _Traits> inline basic_istream<_CharT, _Traits>& ignoreLine(basic_istream<_CharT, _Traits>& is) { is.ignore(numeric_limits<streamsize>::max(), is.widen('\n')); return is; } int main(int argc, char* argv[]) { char ch; int numOfLinesStartWithA = 0; while((ch = cin.get()) != 'q') { if (ch == 'A') { numOfLinesStartWithA++; } cin >> ignoreLine; } cout << numOfLinesStartWithA << " lines start with A" << endl; return 0; }
我们可以用 setw 控制输出数据的宽度。此操作符同样可以用于输入上,主要作用在与C风格字符串共同使用:
char buffer[80]; cin >> setw(sizeof(buffer))>> buffer;
这样可以防止输入溢出。
假设我们有这样一个处理分数的类:
class Fraction { public: Fraction(int numerator, int denominator): numerator(numerator), denominator(denominator) {} int getDenominator() const { return denominator; } int getNumerator() const { return numerator; } private: int numerator; int denominator; }
一般情况下,我们可能想要这样实现:
inline ostream& operator<<(ostream& os, const Fraction& fraction) { os << fraction.getNumerator() << "/" << fraction.getDenominator(); return os; }
这种实现存在两个问题:
① 这样的输出只适合使用 char 的 stream(对本例来说当然没有什么区别)。
② 一次性的格式化标志仅对分子起作用。换句话说,如果我们尝试这样调用:
#include <iomanip> #include <iostream> int main(int argc, char* argv[]) { Fraction f(5,20); cout << setw(5) << f << endl; cout << setw(5) << 5 << endl; return 0; }
得到的结果是:
这里的宽度仅对分子起作用。
简单修改即可解决上述问题:
#include <sstream> template<typename charT, typename traits> inline basic_ostream<charT, traits>& operator<<(basic_ostream<charT, traits> &os, const Fraction& fraction) { basic_ostringstream<charT, traits> s; s.copyfmt(os); s.width(0); s << fraction.getNumerator() << "/" << fraction.getDenominator(); os << s.str(); return os; }
简单的输入操作符实现如下:
inline istream& operator>>(istream& is, Fraction& fraction) { int numerator; int denominator; is >> numerator; is.ignore(); is >> denominator; fraction = Fraction(numerator, denominator); return is; }
这种实现的考虑也很不周到:
① 没有考虑 char 类型的 stream,同时没有考虑分子分母之间输入的是否为 /(如果不是,应该报告格式错误)。
② 如果输入的为字符等其他无效输入,会引发格式异常。此异常不应传递进入 fraction 中,而是通过设置 failbit 提醒用户。
③ 未考虑部分读取错误或全部错误情况下对 fraction 的修改不应该进行。
优化后的代码如下:
template<typename charT, typename traits> inline basic_istream<charT, traits>& operator>>(basic_istream<charT, traits>& is, Fraction& fraction) { int numerator; int denominator; is >> numerator; if (is.peek() == '/') { is.ignore(); is >> denominator; } else { denominator = 1; } if (denominator == 0) { is.setstate(std::ios_base::failbit); return is; } if (is) { fraction = Fraction(numerator, denominator); } return is; }
我们简单测试一下:
int main(int argc, char* argv[]) { Fraction f(1,1); for (int i = 0; i < 2; ++i) { while (true) { cout << "请输入分数:"; if (!(cin >> f)) { break; } cout << "输入的分数为:" << f << endl; } cout << "无效分数,原分数为:" << f << endl; cin.clear(); cin.get(); } return 0; }
我们可以借助 iword 和 pword 两个函数用于设置和获取格式化标志,其参数均为 int 索引,返回类型为 long& 和 void&* 指针。我们可以使用 ios_base 中的惊跳函数 xalloc 来取得一个尚未被用于此目的的索引:
static const int iword_index = std::ios_base::xalloc(); template<typename charT, typename traits> inline basic_ostream<charT, traits>& operator<<(basic_ostream<charT, traits>& os, const Fraction& fraction) { if (os.iword(iword_index)) { os << fraction.getNumerator() << " / " << fraction.getDenominator(); } else { os << fraction.getNumerator() << "/" << fraction.getDenominator(); } return os; } ostream& fraction_spaces(std::ostream& strm) { strm.iword(iword_index) = true; return strm; } int main(int argc, char* argv[]) { Fraction f(5, 20); cout << f << endl; cout << fraction_spaces << f << endl; cout << f << endl; return 0; } int main(int argc, char* argv[]) { Fraction f(5, 20); cout << f << endl; cout << fraction_spaces << f << endl; cout << f << endl; return 0; }
注意这种状态不是一次性状态。除非我们后续重新设置,此标志位不会自动清除。
我们使用 copyfmt 函数会拷贝所有的格式信息,包括使用 iword 和 pword 设置的格式数组。对于 pword 来讲,这可能会导致浅拷贝的问题。因为 pword 仅保存了格式对象的地址。这可能会导致对一个格式对象的修改影响到多个流对象。ios_base 定义了一个回调,用以支持 必要时执行深拷贝 和 销毁 stream 时销毁格式对象:
void eventCb(ios_base::event e, ios_base& base, int userdata) { cout << e << " " << userdata << endl; } int main(int argc, char* argv[]) { Fraction f(5, 20); cout.register_callback(eventCb, 12345); ostringstream str; str.copyfmt(cout); }
在 copy_format 被调用后,将会先后触发 copyfmt_event 和 erase_event 两个回调。
前面我们学习 《C++ Primer Plus》曾经学习过这个方法。tie 用于将某个 stream 连接到另一个 stream 上:
通过函数 rdbuf,可以使不同的 stream 共享同一个缓冲区,从而实现 stream 的紧耦合。其声明如下:
成员函数 rdbuf 允许多个 stream 对象从同一个缓冲区中读取或写入信息,而不必困扰与 I/O 次序。由于 I/O 操作被施以缓冲措施,所以同时使用多个 stream 缓冲区是很麻烦的。因为,对同一个 I/O 通道使用不同的 stream,而这些 stream 的缓冲区又不相同,意味着 I/O 得传递给其他 I/O。
basic_istream 和 basic_ostream 支持以缓冲区为参数的构造函数:
#include <iostream> using namespace std; int main(int argc, char* argv[]) { ostream hexout(cout.rdbuf()); hexout.setf(ios::hex, ios::basefield); hexout.setf(ios::showbase); hexout << "hexout: " << 177 << " "; cout << "cout: " << 177 << " "; hexout << "hexout: " << -49 << " "; cout << "cout: " << -49 << " "; hexout << endl; }
这里二者能共享缓冲区,是因为 basic_istream 和 basic_ostream 对象并不会在析构时释放对应的缓冲区。其他 stream 类对象都会释放它们最初分配的缓冲区,但它们不会销毁以 rdbuf 设置的缓冲区。这是因为 basic_istream 和 basic_ostream 使用 basic_ios 中提供的缓冲区对象,该对象是在堆上构建的;而如 stringstream 的类保存了自己的缓冲区对象,并重载了 rdbuf 方法已返回该对象。该对象在栈上分配,随着流对象的销毁而销毁。
此外,由于输出的格式保存在流对象而非缓冲区中,因此二者的输出格式不会互相影响。
使用缓冲区绑定,我们可以实现标准流的重定向。需要注意的是,如果我们将标准流的输出重定向到 filestream 或 stringstream 的缓冲区上,需要在相应的流对象被释放后复原缓冲区(一个好的选择是使用智能指针):
#include <iostream> #include <fstream> #include <memory> using namespace std; void redirect(ostream& os); int main(int argc, char* argv[]) { cout << "before redirect" << endl; redirect(cout); cout << "after redirect" << endl; } void redirect(ostream& os) { auto delFunc = [&](streambuf* buf) { os.rdbuf(buf); }; unique_ptr<streambuf, decltype(delFunc)> bufPtr(os.rdbuf(), delFunc); ofstream fs("redirect.txt"); if (fs.is_open()) { os.rdbuf(fs.rdbuf()); fs << "one row from fs" << endl; os << "one row from os" << endl; } }
如果我们使用 fstream 声明一个对象,指定 ios::in 和 ios::out 两个标识,那么该流就是可读写的。我们也可以对于一个输出流对象和输入流对象使用相同的缓冲区达到相同的效果:
#include <iostream> #include <fstream> using namespace std; int main(int argc, char* argv[]) { filebuf buffer; ostream os(&buffer); istream is(&buffer); buffer.open("test.txt", ios::in | ios::out | ios::trunc); for (int i = 0; i < 4; ++i) { os << i << ". line" << endl; is.seekg(0); char c; while (is.get(c)) { cout.put(c); } cout << endl; is.clear(); } }
每次我们读取时都需要使用 seekg 将读取流指针定位到流开头。就像我们前面所学,读取流和写入流指针是不同的。