IOStream 插入器和提取器






4.29/5 (9投票s)
2002年4月17日
7分钟阅读

84336

998
演示如何扩展 iostreams 以流式传输自定义类型
概述
本文旨在解释如何扩展 iostream
以支持自定义类型。iostream
库是 C++ 标准库中用于将数据流式传输到文件 (fstream
)、字符串 (sstream
) 和控制台 (cin
/cout
) 等源以及从这些源流式传输数据的设施。使用 iostreams 的典型 'Hello World' 程序如下所示:
#include<iostream> using std::cout; using std::endl; int main() { cout << "Hello World" << endl; }
使用 iostream
,我们使用 <<
运算符将变量中的值流式传输到 iostream
,并使用 >>
运算符提取它们。某些流类型是双向的,但本文将使用所有单向类型,即我更喜欢使用 ifstream
和 ofstream
而不是 fstream
。这仅仅是因为我从不需要双向功能,并且设置双向流需要更多选项,而单向文件流都有有意义的默认参数值。我应该指出,虽然这里呈现的核心代码 (在 stdafx.h
中) 将在任何符合标准的 C++ 编译器上运行 (它也将与 Visual C++ 一起运行),但该项目是使用 VS.NET 编写的,因此如果您使用不同的编译器,则必须创建自己的项目。
插入器和提取器
支持一种类型涉及两个步骤,这两个步骤可以独立进行,具体取决于需要。提取器为给定类型定义 >>
运算符,插入器定义 <<
运算符。正如我所说,我们可以选择仅定义一个而不定义另一个,事实上我们已经为 CString
这样做了,我们只定义了一个插入器。
定义插入器
为了定义插入器,我们使用以下原型:
template<class charT, class Traits> std::basic_ostream<charT, Traits> & operator << (std::basic_ostream<charT, Traits> & os, const TYPE & t)
类型需要定义为 const
,以便我们可以将 const
类型传递给它。无论如何,在可能的情况下使用 const
是明智的,而且我们绝对不希望修改参数的值。
我们传递了流和变量,还需要返回流。这是因为流值可以连接起来,如下所示:
cout << "The number of " << strType[i] " << " it takes to change a lightbulb is " << nNumber[i]<< " because " << strReason[i] << endl;
因此,必须返回流对象以传递给下一个 <<
运算符。
第一次尝试
在这种情况下,最明显的事情就是这样做:
template<class charT, class Traits> std::basic_ostream<charT, Traits> & operator << (std::basic_ostream<charT, Traits> & os, const POINT & pt) { os << "x: " << pt.x; os << " y: " << pt.y; return os; }
这确实会将点值流式传输,以便我们得到类似“x: 0 y: 48”的内容,但它只在某些情况下有效。
检查流状态
首先,只有当传入的流有效时,它才起作用。看起来很明显,但最好先检查一下,如果流无效,就直接传递流? (iostream
有一个异常机制,即如果使用流的人想要在发生特定类型的错误时抛出异常,那么如果流已损坏,代码就不会到达我们的插入器。)
流有一个 isgood()
方法为我们执行此测试,如下所示:
if (!os.good()) return os;
下一个问题是流可以定义前缀和后缀操作,即在每次插入之前和之后应该发生的操作。这些也可以添加。这些由 sentry
对象处理,该对象需要在我们的插入之前实例化,构造函数执行我们的前缀操作,析构函数处理后缀操作。
typename std::basic_ostream<chart Traits,>::sentry opfx(os);
此对象还定义了 bool
运算符,以便于检查成功,因此我们将在操作之前检查此项。
维护流的完整性
主要问题稍微严重一些。正如我在 ostringstream
文章中涵盖的那样,事实上,有许多修饰符可以传递给流,这些修饰符要么需要作为单个操作应用于该操作 (例如对齐),要么仅应用于流的下一次插入然后被重置。我见过许多针对此问题的冗长解决方案,但我的解决方案非常简单。使用 ostringstream
来构建要插入的项,然后将其作为一个单个字符串插入到流中,从而使所有格式化等工作都能按默认方式完美进行。
最终结果,显示此操作以及所有先前提到的更改,如下所示:
template<class charT, class Traits> std::basic_ostream<charT, Traits> & operator << (std::basic_ostream<charT, Traits> & os, const POINT & pt) { //Check stream state first if (!os.good()) return os; // Create sentry for prefix operations ( it's destructor will // carry out postfix operations ) typename std::basic_ostream<charT, Traits>::sentry opfx(os); if (opfx) { std::ostringstream str; str << "x: " << pt.x; str << " y: " << pt.y; os << str.str().c_str(); } return os; }
定义提取器
提取器的原型没有太大区别 - 主要要注意的是,我们的对象不再是 const
,因为我们打算用来自流的值来填充它。
template<class charT, class Traits> std::basic_istream<charT, Traits> & operator >> (std::basic_istream<charT, Traits> & is, TYPE & T)
除此之外,我们的策略是相似的,区别在于由于我们提供的格式,流中包含我们希望丢弃的信息。我们使用 std::string
来保存我们的数据,并调用 >>
运算符 (该运算符的操作由空格和换行符分隔),当我们读取到想要的值时,我们使用 atoi
将其转换为数字。
template<class charT, class Traits> std::basic_istream<charT, Traits> & operator >> (std::basic_istream<charT, Traits> & is, POINT & pt) { if (!is.good()) return is; typename std::basic_istream<charT, Traits>::sentry opfx(is); if (opfx) { std::string s; is >> s; is >> s; pt.x = atoi(s.c_str()); is >> s; is >> s; pt.y = atoi(s.c_str()); } return is; }
示例程序
示例程序使用一个 doc/view MFC 程序,其中有一个编辑视图。当视图移动时,或者当我们在按下鼠标按钮时在视图中移动鼠标时,我们会将窗口或鼠标的坐标流式传输到函数中,该函数将它们输出到视图。
void CMainFrame::OnMove(int x, int y) { if (::IsWindowVisible(m_hWnd)) { CRect rc; GetWindowRect(&rc); ostringstream strm; strm << rc; CIOStreamInsertersView * pView = dynamic_cast<CIOStreamInsertersView *>(GetActiveView()); ASSERT(pView); pView->InsertString(s); } } void CIOStreamInsertersView::OnMouseMove(UINT nFlags, CPoint point) { if (::GetAsyncKeyState(VK_LBUTTON) && ::GetAsyncKeyState(VK_LBUTTON)) { ostringstream strm; strm << point; InsertString(strm.str().c_str()); } CRichEditView::OnMouseMove(nFlags, point); }
顺便说一句,我调用 GetAsyncKeystate
两次的原因是它会返回自上次检查以来按键是否被按下。我曾经在工作中的一个人不知道这一点,他遇到了一个非常有趣的错误,即一个键每隔一次按下才起作用 - 你已被警告。
在 CEditView 末尾添加字符串
这两个函数都调用 InsertString
,它看起来像这样:
void CIOStreamInsertersView::InsertString (CString s, bool bAdd /* = true */) { // Add string to document object if (bAdd) GetDocument()->m_vecDocument.push_back(s); s += "\r\n"; m_bOverflow = false; int nLength = (int) SendMessage(WM_GETTEXTLENGTH, 0, 0); SendMessage(EM_SETSEL, nLength, nLength); SendMessage(EM_REPLACESEL, 0, (LPARAM)s.GetBuffer(s.GetLength())); s.ReleaseBuffer(); if (m_bOverflow) { SendMessage(EM_SETSEL, 0, s.GetLength()); char empty = 0; SendMessage(EM_REPLACESEL, 0, (LPARAM)&empty); nLength = (int) SendMessage(WM_GETTEXTLENGTH, 0, 0); SendMessage(EM_SETSEL, nLength, nLength); SendMessage(EM_REPLACESEL, 0, (LPARAM)s.GetBuffer(s.GetLength())); s.ReleaseBuffer(); } }
当读取文件时,我们将 bAdd
标志设置为 false
,以便我们的 vector
不会被再次填充 (这会导致严重后果,因为它会使我们当时正在迭代的迭代器失效)。vector
本身包含我们显示在屏幕上的字符串。这是一个糟糕的设计,因为我们的显示与我们加载/保存的数据不关联,但修复它并不能真正增强示例,所以我以一种更强调使用新运算符而不是健壮设计的方式来完成它。
在编辑视图末尾插入字符串的顺序是调用 WM_GETTEXTLENGTH
以确定视图中有多少个字符,调用 EM_SETSEL
将光标设置为仅选择文件末尾的位置,然后调用 EM_REPLACESEL
将该末尾位置替换为所讨论的字符串。我们还需要处理 EN_MAXTEXT
,如果我们要插入的字符串导致溢出,就会调用它。
void CIOStreamInsertersView::OnEnMaxtext() { m_bOverflow = true; }
我们在 EN_MAXTEXT
中设置了我们的成员 bool
。(如果它是 static
也可以,因为它在开始时被设置为 false,而我们只是在测试 OnEnMaxText
是否已被调用。)如果调用了,我们就选择从视图顶部到字符串长度的字符串,用空字符串替换它,然后再次在底部插入我们的字符串,从而腾出了足够的空间来插入它。
序列化
为了举例说明,我重载了 OnFileLoad
和 OnFileSave
,并且我没有费心添加文件选择对话框,只是一个硬编码的路径。加载函数创建一个 ifstream
,然后读取文件,使用值 1 和 2 来判断一个值是 RECT
还是 POINT
。然后读取 RECT
和 POINT
值,将它们转换回字符串并传递给视图,使用 false
标志,以便我们的 vector
不会被修改,否则插入会使我们正在迭代的迭代器失效,并导致程序崩溃。加载函数如下所示:
void CIOStreamInsertersDoc::OnFileOpen() { ifstream file("iostream test.txt"); m_vecDocument.clear(); std::string s; RECT rc; POINT pt; while (!file.eof()) { file >> s; ostringstream str; if ('1' == s[0]) { file >> pt; str << pt; CString sData(str.str().c_str()); m_vecDocument.push_back(sData); } else if ('2' == s[0]) { file >> rc; str << rc; CString sData(str.str().c_str()); m_vecDocument.push_back(sData); } } CIOStreamInsertersView * pView = dynamic_cast<CIOStreamInsertersView *> (dynamic_cast<CMainFrame*> (AfxGetMainWnd())->GetActiveView()); ASSERT(pView); pView->SetWindowText(""); std::vector<CString>::iterator it = m_vecDocument.begin(); std::vector<CString>::iterator end = m_vecDocument.end(); for (;it != end; ++it) { CString s(*it); pView->InsertString(*it, false); } }
显然,直接读取字符串会容易得多,但这违背了示例的初衷,不是吗?
OnFileSave
保存函数几乎完全按照加载函数的方式进行。它遍历向量,并写出分隔符 1 或 2,后跟向量中的字符串。
void CIOStreamInsertersDoc::OnFileSave() { ofstream file("iostream test.txt"); std::vector<CString>::iterator it = m_vecDocument.begin(); std::vector<CString>::iterator end = m_vecDocument.end(); CString strTemp; for (;it != end; ++it) { strTemp = *it; if ("x" == strTemp.Left(1)) file << 1 << endl; else file << 2 << endl; file << strTemp << endl; } file.close(); }
摘要
本文的重点是展示 iostream
s 可以扩展以处理自定义类型的方法。我们已经处理了 POINT
和 RECT
(默认情况下也包括 CPoint
和 CRect
),但通过本文应该清楚如何继续为任何其他类型(包括您自己的类)提供处理程序,您可能出于某种原因想要将它们传递到流中。我还介绍了如何将字符串传递到 CEditView
的末尾,以及通过其他一些主题,并通过示例展示了如何使用 iostream
s 将文件读写到磁盘。我希望您觉得这很有用,并且有充分的理由使用 iostream
s 而不是 MFC 类,如 CFile
。