65.9K
CodeProject 正在变化。 阅读更多。
Home

IOStream 插入器和提取器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.29/5 (9投票s)

2002年4月17日

7分钟阅读

viewsIcon

84336

downloadIcon

998

演示如何扩展 iostreams 以流式传输自定义类型

Sample Image

概述

本文旨在解释如何扩展 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,并使用 >> 运算符提取它们。某些流类型是双向的,但本文将使用所有单向类型,即我更喜欢使用 ifstreamofstream 而不是 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 是否已被调用。)如果调用了,我们就选择从视图顶部到字符串长度的字符串,用空字符串替换它,然后再次在底部插入我们的字符串,从而腾出了足够的空间来插入它。

序列化

为了举例说明,我重载了 OnFileLoadOnFileSave,并且我没有费心添加文件选择对话框,只是一个硬编码的路径。加载函数创建一个 ifstream,然后读取文件,使用值 1 和 2 来判断一个值是 RECT 还是 POINT。然后读取 RECTPOINT 值,将它们转换回字符串并传递给视图,使用 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();
}

摘要

本文的重点是展示 iostreams 可以扩展以处理自定义类型的方法。我们已经处理了 POINTRECT (默认情况下也包括 CPointCRect),但通过本文应该清楚如何继续为任何其他类型(包括您自己的类)提供处理程序,您可能出于某种原因想要将它们传递到流中。我还介绍了如何将字符串传递到 CEditView 的末尾,以及通过其他一些主题,并通过示例展示了如何使用 iostreams 将文件读写到磁盘。我希望您觉得这很有用,并且有充分的理由使用 iostreams 而不是 MFC 类,如 CFile

© . All rights reserved.