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

文本和二进制文件 API 的统一

starIconstarIconstarIconstarIconstarIcon

5.00/5 (28投票s)

2018年1月9日

CPOL

19分钟阅读

viewsIcon

74657

downloadIcon

3941

一个可移植且易于使用的 C++ 文件库,用于读写结构化数据

目录

引言

对于 C/C++ 程序员来说,读写文件的标准方法是通过 C 文件 API 和 C++ 文件流。由于 C++ 文件流的类名和函数名晦涩难懂且不直观,我一直觉得 C++ 文件流“太难”用了,而 C 文件 API 又不是类型安全的。在这篇文章中,我将介绍我自己的类型安全文件库,它基于 C 文件 API,以几乎无缝的方式统一了文本文件和二进制文件的 API。在文本和二进制 API 之间仍然存在一些差异,因为将它们做得完全一致没有任何意义。该库旨在写入和读取**结构化**数据,也就是说写入和读取整数、布尔值、浮点数、字符串等。该库也可以用于写入和读取非结构化数据(例如 C++ 源文件),通过其低级类。然而,这并不是该库和本文的重点。本文旨在教读者如何轻松地访问文件中的结构化数据。

对于偶然看到这篇文章的 .NET 人员,您可以停止阅读本文了。本文是关于原生 C++ 的,而不是 .NET 的,尽管我尝试编写了我文件库的 C# 版本,但我失败了,因为 C# 不允许开发人员在方法返回后保留传递引用的 POD 参数的副本。

文本文件用法

在本节中,我们将研究如何读写文本文件。让我们开始学习如何将整数和双精度浮点数写入文本文件。

using namespace Elmax;

xTextWriter writer;
std::wstring file = L"Unicode.txt";
if(writer.Open(file, FT_UNICODE, NEW))
{
    int i = 25698;
    double d = 1254.69;
    writer.Write(L"{0},{1}", i, d);
    writer.Close();
}

上面的代码尝试打开一个新的 Unicode 文件,并在成功后写入一个整数和双精度浮点数值,然后关闭文件。支持的其他文本文件类型包括 ASCII、Big Endian Unicode 和 UTF-8。虽然代码中没有显示,但用户应该检查 `write` 的布尔返回值。`xTextWriter` 将其文件工作委托给 `AsciiWriter`、`UnicodeWriter`、`BEUnicodeWriter` 和 `UTF8Writer`。同样,`xTextReader` 将其文件工作委托给 `AsciiReader`、`UnicodeReader`、`BEUnicodeReader` 和 `UTF8Reader`。这些文件写入器在第一次写入时写入 BOM,而读取器会在 BOM 存在时自动读取 BOM。对于不熟悉 BOM 的读者来说,BOM 是字节顺序标记的缩写。BOM 是一个 Unicode 字符,用于指示文本文件或流的字节序。BOM 是可选的,但通常被认为是写入 BOM 的良好实践。读者可能会问为什么需要编写一个 Unicode 文件库,而不是从 CodeProject 上挑选一个。我决定编写自己的 Unicode 文件类,因为 CodeProject 上的大多数文件都使用了 `MFC CStdioFile` 类,而该类在其他平台上不起作用。现在让我们看看如何读取我们刚刚写入的相同数据。

using namespace Elmax;

xTextReader reader;
std::wstring file = L"Unicode.txt";
if(reader.Open(file))
{
    if(reader.IsEOF()==false)
    {
        int i2 = 0;
        double d2 = 0.0;

        StrtokStrategy strat(L",");
        reader.SetSplitStrategy(&strat);
        size_t totalRead = reader.ReadLine(i2, d2); // i2 = 25698 and d2 = 1254.69
}
reader.Close();

读取器打开同一个文件并设置其文本分割策略。在这种情况下,它被设置为使用 `strtok` 并且分隔符设置为逗号。其他分割策略包括 Boost 和 Regex,但强烈建议用户选择 `strtok`,因为它速度很快。我们已经了解了如何读写整数和双精度浮点数。读写 `string` 没有区别,但必须特别注意字符串中可能出现的分隔符。这意味着我们在写入时必须转义 `string`,在读取时必须反转义 `string`。`StrUtil` 类中有一个 `ReplaceAll` 函数,用户可以使用它来转义和反转义他们的字符串。**注意**:对于 2.0.2 版本(内部使用流),这不再适用:您不需要设置分割策略,但必须调用 `SetDelimiter`。

有一个重载的 `Open` 函数,它接受额外的 Unicode 文件类型作为参数。但最重要的是,它将始终尊重检测到的 BOM。只有在没有 BOM 的情况下,`xTextReader` 才会根据用户指定的 Unicode 文件类型打开文件。

二进制文件用法

写入二进制文件与写入文本文件类似,只是用户不必在数据之间写入分隔符。

using namespace Elmax;

xBinaryWriter writer;
std::wstring file = L"Binary.bin";
if(writer.Open(file))
{
    int i = 25698;
    double d = 1254.69;
    writer.Write(i, d);
    writer.Close();
}

`Write` 返回成功写入的值的数量。如下所示,读取与写入几乎相同。

using namespace Elmax;

xBinaryReader reader;
std::wstring file = L"Binary.bin";
if(reader.Open(file))
{
    if(reader.IsEOF())
    {
        int i2 = 0;
        double d2 = 0.0;
        size_t totalRead = reader.Read(i2, d2); // i2 = 25698 and d2 = 1254.69
    }
    reader.Close();
}

二进制写入字符串通常涉及提前写入字符串长度,在读取字符串之前,我们需要读取长度并先分配数组。

using namespace Elmax;

xBinaryWriter writer;
std::wstring file = GetTempPath(L"Binary.bin");
if(writer.Open(file))
{
    std::string str = "Coding Monkey";
    double d = 1254.69;
    writer.Write(str.size(), str, d);
    writer.Close();
}

xBinaryReader reader;
if(reader.Open(file))
{
    if(reader.IsEOF()==false)
    {
        size_t len = 0;
        double d2 = 0.0;
        StrArray arr;
        size_t totalRead = reader.Read(len);

        totalRead = reader.Read(arr.MakeArray(len), d2);

        std::string str2 = arr.GetPtr(); // str2 contains "Coding Monkey"
    }
    reader.Close();
}

我们使用 `StrArray` 来读取 `char` 数组。我们先读取其长度,然后使用该长度通过 `MakeArray` 方法分配数组。可以使用 `DeferredMake` 一次性读取长度并创建数组。与 `MakeArray` 不同,`DeferredMake` 不分配数组:分配被延迟到轮到读取文件时。`DeferredMake` 捕获 `len` 的地址,因此当 `len` 被长度更新时,它也获取了长度。请参阅下文。

xBinaryReader reader;
if(reader.Open(file))
{
    if(reader.IsEOF()==false)
    {
        size_t len = 0;
        double d2 = 0.0;
        StrArray arr;
        size_t totalRead = reader.Read(len, arr.DeferredMake(len), d2);

        std::string str2 = arr.GetPtr(); // str2 contains "Coding Monkey"
    }
    reader.Close();
}

可以将结构体作为数组写入。但不建议这样做,因为不同平台出于性能原因,可能会在结构体成员之间填充未知字节数。为了可移植性,建议逐个写入结构体成员,而不是将结构体写入为扁平数组。如果您仍然想这样做,请为您的结构体指定无填充(如下)。

#pragma pack(push, 1) // exact fit - no padding
struct MyStruct
{
  char b; 
  int a; 
};
#pragma pack(pop)

`WStrArray` 可用于读取 `wchar_t` 数组。但是,如果您希望您的文件格式在不同操作系统之间具有可移植性,则不建议写入 `std::wstring` 并使用 `WStrArray` 读取它。原因是 `wchar_t` 的大小在 Windows、Linux 和 Mac OSX 上不同。我们将在后面的章节中探讨这个问题。**注意**:文本文件 API 没有这个问题,因为已进行转换以使其自动。如果您需要写入 Unicode 字符串,解决方法是写入 UTF-8 字符串。另一种选择是使用 `BaseArray` 类来写入 16 位字符串。Unicode 有两种 16 位编码,即 UCS-2 和 UTF-16。UCS-2 单位始终是 16 位,只能表示 97% 的 Unicode。UTF-16 可以编码所有 Unicode 代码点,但其单位可能包含一个或两个 16 位字。在某些用例中,UCS-2 足以存储所选语言的文本。UTF-16 能够存储所有 Unicode 内容,但权衡是转换时间和需要注意转换前后文本长度的潜在差异。

`xBinaryWriter` 和 `xBinaryReader` 还提供 `Seek` 和 `GetCurrPos` 来进行文件定位(二进制文件解析中的常见操作)。

代码设计

`xTextWriter` 和 `xTextReader` 分别使用 `DataType` 和 `DataTypeRef` 来执行数据类型和字符串之间的转换。基本上,该库依赖于 Plain Old Data (POD) 到 `DataType` 对象的隐式转换来工作。`xTextWriter` 有许多重载的 `Write` 和 `WriteLine` 函数,它们在 `DataType` 参数的数量上有所不同。`WriteLine` 基本上只是在写入 `string` 后添加换行符 (LF)。下面的 `Write` 函数有五个 `DataType` 参数。

bool xTextWriter::Write( const wchar_t* fmt, DataType D1, DataType D2, 
                         DataType D3, DataType D4, DataType D5 )
{
    if(pWriter!=NULL)
    {
        std::wstring str = StrUtilRef::Format(fmt, D1, D2, D3, D4, D5);
        return pWriter->Write(str);
    }

    return false;
}

`DataType` 包含许多重载的构造函数,它们将 Plain Old Data (POD) 转换为 `string` 并将其存储在 `string` 成员 (m_str) 中。

namespace Elmax
{
class DataType
{
public:
    ~DataType(void);

    DataType( int i );

    DataType( unsigned int ui );

    DataType( const ELMAX_INT64& i64 );

    DataType( const unsigned ELMAX_INT64& ui64 );

    DataType( float f );

    DataType( const double& d );

    DataType( const std::string& s );

    DataType( const std::wstring& ws );

    DataType( const char* pc );

    DataType( const wchar_t* pwc );

    DataType( char c );

    DataType( unsigned char c );

    DataType( wchar_t wc );

    std::wstring& ToString() { return m_str; }

protected:
    std::wstring m_str;
};

这是 C++11 可变参数模板 `Write` 版本,它支持任意数量的参数。但您需要下载并安装Visual C++ Compiler November 2012 CTP 才能编译代码。**注意**:代码量大大减少,无需再编写所有那些重载函数。

bool Write( const wchar_t* str )
{
    if(pWriter!=nullptr)
    {
        return pWriter->Write(std::wstring(str));
    }

    return false;
}

template<typename... Args>
bool Write( const wchar_t* fmt, Args&... args )
{
    std::wstring str = StrUtilRef::Format(std::wstring(fmt), 0, args...);

    if(pWriter!=nullptr)
    {
        return pWriter->Write(str);
    }

    return false;
}

如前所述,`xTextReader` 使用 `DataTypeRef` 将字符串转换为 Plain Old Data (POD)。`xTextReader` 有 10 个重载的 `Read` 和 `ReadLine` 函数,它们仅在 `DataTypeRef` 参数的数量上有所不同。下面的 `ReadLine` 函数有 5 个 `DataTypeRef` 参数。

size_t xTextReader::ReadLine( DataTypeRef D1, DataTypeRef D2, DataTypeRef D3, DataTypeRef D4,
    DataTypeRef D5 )
{
    if(pReader!=NULL)
    {
        std::wstring text;
        bool b = pReader->ReadLine(text);

        if(b)
        {
            StrUtilRef strUtil;
            strUtil.SetSplitStrategy(m_pSplitStrategy);

            return strUtil.Split(text.c_str(), D1, D2, D3, D4, D5);
        }
    }

    return 0;
}

size_t StrUtilRef::Split( const std::wstring& StrToExtract, 
                          DataTypeRef& D1, DataTypeRef& D2, DataTypeRef& D3, 
                          DataTypeRef& D4, DataTypeRef& D5 )
{
    std::vector<DataTypeRef*> vecDTR;
    vecDTR.push_back(&D1);
    vecDTR.push_back(&D2);
    vecDTR.push_back(&D3);
    vecDTR.push_back(&D4);
    vecDTR.push_back(&D5);

    assert( m_pSplitStrategy );
    return m_pSplitStrategy->Extract( StrToExtract, vecDTR );
}

size_t StrtokStrategy::Extract( 
    const std::wstring& StrToExtract, 
    std::vector<Elmax::DataTypeRef*> vecDTR )
{
    std::vector<std::wstring> vecSplit;
    const size_t size = StrToExtract.size()+1;
    wchar_t* pszToExtract = new wchar_t[size];
    wmemset( pszToExtract, 0, size );
    Wcscpy( pszToExtract, StrToExtract.c_str(), size );

    wchar_t *pszContext = 0;
    wchar_t *pszSplit = 0;
    pszSplit = wcstok( pszToExtract, m_sDelimit.c_str() );

    while( NULL != pszSplit )
    {
        size_t len = wcslen(pszSplit);
        if(pszSplit[len-1]==65535&&vecSplit.size()==vecDTR.size()-1) // bug workaround: 
                                       // wcstok_s/wcstok will put 65535 
                                       // at the back of last string.
            pszSplit[len-1] = L'\0';

        vecSplit.push_back(std::wstring( pszSplit ) );

        pszSplit = wcstok( NULL, m_sDelimit.c_str() );
    }

    delete [] pszToExtract;

    size_t fail = 0;
    for( size_t i=0; i<vecDTR.size(); ++i )
    {
        if( i < vecSplit.size() )
        {
            if( false == vecDTR[i]->ConvStrToType( vecSplit[i] ) )
                ++fail;
        }
        else
            break;
    }

    return vecSplit.size()-fail;
}

`DataTypeRef` 包含一个大的联合体,用于存储每个 POD 参数的地址,作为结果的目的地。

namespace Elmax
{
class DataTypeRef
{
public:
    ~DataTypeRef(void);

    union UNIONPTR
    {
        int* pi;
        unsigned int* pui;
        short* psi;
        unsigned short* pusi;
        ELMAX_INT64* pi64;
        unsigned ELMAX_INT64* pui64;
        float* pf;
        double* pd;
        std::string* ps;
        std::wstring* pws;
        char* pc;
        unsigned char* puc;
        wchar_t* pwc;
    };

    enum DTR_TYPE
    {
        DTR_INT,
        DTR_UINT,
        DTR_SHORT,
        DTR_USHORT,
        DTR_INT64,
        DTR_UINT64,
        DTR_FLOAT,
        DTR_DOUBLE,
        DTR_STR,
        DTR_WSTR,
        DTR_CHAR,
        DTR_UCHAR,
        DTR_WCHAR
    };

    DataTypeRef( int& i )                    { m_ptr.pi = &i;       m_type = DTR_INT;   }

    DataTypeRef( unsigned int& ui )          { m_ptr.pui = &ui;     m_type = DTR_UINT;  }

    DataTypeRef( short& si )                 { m_ptr.psi = &si;     m_type = DTR_SHORT; }

    DataTypeRef( unsigned short& usi )       { m_ptr.pusi = &usi;   m_type = DTR_USHORT;}

    DataTypeRef( ELMAX_INT64& i64 )          { m_ptr.pi64 = &i64;   m_type = DTR_INT64; }

    DataTypeRef( unsigned ELMAX_INT64& ui64 ){ m_ptr.pui64 = &ui64; m_type = DTR_UINT64;}

    DataTypeRef( float& f )                  { m_ptr.pf = &f;       m_type = DTR_FLOAT; }

    DataTypeRef( double& d )                 { m_ptr.pd = &d;       m_type = DTR_DOUBLE;}

    DataTypeRef( std::string& s )            { m_ptr.ps = &s;       m_type = DTR_STR;   }

    DataTypeRef( std::wstring& ws )          { m_ptr.pws = &ws;     m_type = DTR_WSTR;  }

    DataTypeRef( char& c )                   { m_ptr.pc = &c;       m_type = DTR_CHAR;  }

    DataTypeRef( unsigned char& uc )         { m_ptr.puc = &uc;     m_type = DTR_UCHAR; }

    DataTypeRef( wchar_t& wc )               { m_ptr.pwc = &wc;     m_type = DTR_WCHAR; }

    bool ConvStrToType( const std::string& Str );

    bool ConvStrToType( const std::wstring& Str );

    DTR_TYPE m_type;

    UNIONPTR m_ptr;
};

下面的 C++11 可变参数模板版本调用 `ReadArg`。第一个 `ReadArg` 是终止可变参数同名的递归的基础函数。请注意,这并不是传统意义上的真正递归,因为函数实际上并没有调用自身:它调用的是一个具有相同名称但参数数量不同的函数。

void ReadArg(std::vector<DataTypeRef*>& vec)
{
}

template<typename T, typename... Args>
void ReadArg(std::vector<DataTypeRef*>& vec, T& t, Args&... args)
{
    vec.push_back(new DataTypeRef(t));
    ReadArg(vec, args...);
}

template<typename... Args>
size_t Read( size_t len, Args&... args )
{
    if(pReader!=nullptr)
    {
        std::wstring text;
        bool b = pReader->Read(text, len);

        if(b)
        {
            std::vector<DataTypeRef*> vec;
            ReadArg(vec, args...);

            size_t ret = m_pSplitStrategy->Extract(text, vec);

            for(size_t i=0; i<vec.size(); ++i)
            {
                delete vec[i];
            }

            vec.clear();

            return ret;
        }
    }

    return 0;
}

`xBinaryWriter` 使用 `BinaryTypeRef`。重载的 `Write` 函数在参数数量上有所不同。`xBinaryWriter` 没有 `WriteLine` 函数。下面的 `Write` 函数有两个 `BinaryTypeRef` 参数。

size_t xBinaryWriter::Write( BinaryTypeRef D1, BinaryTypeRef D2 )
{
    size_t totalWritten = 0;
    if(fp!=NULL)
    {
        if(D1.m_type != BinaryTypeRef::DTR_STR && 
        D1.m_type != BinaryTypeRef::DTR_WSTR && D1.m_type != BinaryTypeRef::DTR_BASEARRAY)
        {
            size_t len = fwrite(D1.GetAddress(), D1.size, 1, fp);
            if(len==1)
                ++totalWritten;
        }
        else
        {
            size_t len = fwrite(D1.GetAddress(), D1.elementSize, D1.arraySize, fp);
            if(len==D1.arraySize)
                ++totalWritten;
        }

        if(D2.m_type != BinaryTypeRef::DTR_STR && D2.m_type 
        != BinaryTypeRef::DTR_WSTR && D2.m_type != BinaryTypeRef::DTR_BASEARRAY)
        {
            size_t len = fwrite(D2.GetAddress(), D2.size, 1, fp);
            if(len==1)
                ++totalWritten;
        }
        else
        {
            size_t len = fwrite(D2.GetAddress(), D2.elementSize, D2.arraySize, fp);
            if(len==D2.arraySize)
                ++totalWritten;
        }
    }

    if(totalWritten != 2)
    {
        errNum = ELMAX_WRITE_ERROR;
        err = StrUtil::Format(L"{0}: Less than 2 elements are written! 
        ({1} elements written)", GetErrorMsg(errNum), totalWritten);
        if(enableException)
            throw new std::runtime_error(StrUtil::ConvToString(err));
    }

    return totalWritten;
}

`BinaryTypeRef` 包含一个联合体,用于存储 POD 的地址。无需进行文本到字符串的转换:POD 按原样写入二进制文件。

namespace Elmax
{
class BinaryTypeRef
{
public:
    ~BinaryTypeRef(void);

    union UNIONPTR
    {
        const int* pi;
        const unsigned int* pui;
        const short* psi;
        const unsigned short* pusi;
        const ELMAX_INT64* pi64;
        const unsigned ELMAX_INT64* pui64;
        const float* pf;
        const double* pd;
        std::string* ps;
        const std::wstring* pws;
        const char* pc;
        const unsigned char* puc;
        const wchar_t* pwc;
        const char* arr;
    };

    enum DTR_TYPE
    {
        DTR_INT,
        DTR_UINT,
        DTR_SHORT,
        DTR_USHORT,
        DTR_INT64,
        DTR_UINT64,
        DTR_FLOAT,
        DTR_DOUBLE,
        DTR_STR,
        DTR_WSTR,
        DTR_CHAR,
        DTR_UCHAR,
        DTR_WCHAR,
        DTR_BASEARRAY
    };

    BinaryTypeRef( const int& i )                     
    { m_ptr.pi = &i; m_type = DTR_INT; size=sizeof(i); }

    BinaryTypeRef( const unsigned int& ui )      
    { m_ptr.pui = &ui; m_type = DTR_UINT; size=sizeof(ui); }

    BinaryTypeRef( const short& si )              
    { m_ptr.psi = &si; m_type = DTR_SHORT; size=sizeof(si); }

    BinaryTypeRef( const unsigned short& usi )    
    { m_ptr.pusi = &usi; m_type = DTR_USHORT; size=sizeof(usi); }

    BinaryTypeRef( const ELMAX_INT64& i64 )       
    { m_ptr.pi64 = &i64; m_type = DTR_INT64; size=sizeof(i64); }

    BinaryTypeRef( const unsigned ELMAX_INT64& ui64 )
    { m_ptr.pui64 = &ui64; m_type = DTR_UINT64; size=sizeof(ui64); }

    BinaryTypeRef( const float& f )               
    { m_ptr.pf = &f; m_type = DTR_FLOAT; size=sizeof(f); }

    BinaryTypeRef( const double& d )              
    { m_ptr.pd = &d; m_type = DTR_DOUBLE; size=sizeof(d); }

    BinaryTypeRef( std::string& s )               
    { m_ptr.ps = &s; m_type = DTR_STR; elementSize=sizeof(char);size=s.length(); 
                                                            arraySize=s.length();}

    BinaryTypeRef( const std::wstring& ws )      
    { m_ptr.pws = &ws; m_type = DTR_WSTR; elementSize=sizeof(wchar_t);
      size=ws.length()*sizeof(wchar_t); arraySize=ws.length();}

    BinaryTypeRef( const char& c )                  
    { m_ptr.pc = &c; m_type = DTR_CHAR; size=sizeof(c); }

    BinaryTypeRef( const unsigned char& uc )      
    { m_ptr.puc = &uc; m_type = DTR_UCHAR; size=sizeof(uc); }

    BinaryTypeRef( const wchar_t& wc )            
    { m_ptr.pwc = &wc; m_type = DTR_WCHAR; size=sizeof(wc); }

    BinaryTypeRef( const BaseArray& arr )         
    { m_ptr.arr = arr.GetPtr(); m_type = DTR_BASEARRAY; 
                                size=arr.GetTotalSize(); elementSize=arr.GetElementSize(); 
                                arraySize=arr.GetArraySize(); }
    char* GetAddress();

    DTR_TYPE m_type;

    UNIONPTR m_ptr;

    size_t size;

    size_t elementSize;

    size_t arraySize;
};

这是 C++11 可变参数模板二进制 `Write` 版本。第一个 `Write` 是停止递归调用的基础函数。它还利用了 `BinaryTypeRef` 类。

size_t Write()
{
    return 0;
}

template<typename T, typename... Args>
size_t Write( T t, Args... args )
{
    BinaryTypeRef dt(t);

    size_t totalWritten = 0;
    if(fp!=nullptr)
    {
        if(dt.m_type != BinaryTypeRef::DTR_STR && 
        dt.m_type != BinaryTypeRef::DTR_WSTR && dt.m_type != BinaryTypeRef::DTR_BASEARRAY)
        {
            size_t len = fwrite(dt.GetAddress(), dt.size, 1, fp);
            if(len==1)
                ++totalWritten;
        }
        else
        {
            size_t len = fwrite(dt.GetAddress(), dt.elementSize, dt.arraySize, fp);
            if(len==dt.arraySize)
                ++totalWritten;
        }
    }

    return totalWritten + Write(args...);
}

最后,我们来到了 `xBinaryReader`。`xBinaryReader` 使用 `BinaryTypeReadRef` 进行数据转换。与 `xTextReader` 一样,`xBinaryReader` 有重载的 `Read` 来完成工作,但它没有 `ReadLine`。

size_t xBinaryReader::Read( BinaryTypeReadRef D1, BinaryTypeReadRef D2 )
{
    size_t totalRead = 0;
    if(fp!=NULL)
    {
        if(D1.m_type != BinaryTypeReadRef::DTR_STRARRAY && 
           D1.m_type != BinaryTypeReadRef::DTR_WSTRARRAY && 
           D1.m_type != BinaryTypeReadRef::DTR_BASEARRAY)
        {
            size_t cnt = fread(D1.GetAddress(), D1.size, 1, fp);
            if(cnt==1)
                ++totalRead;
        }
        else
        {
            D1.DeferredMake();
            size_t cnt = fread(D1.GetAddress(), D1.elementSize, D1.arraySize, fp);
            if(cnt == D1.arraySize)
                ++totalRead;
        }

        if(D2.m_type != BinaryTypeReadRef::DTR_STRARRAY && 
           D2.m_type != BinaryTypeReadRef::DTR_WSTRARRAY && 
           D2.m_type != BinaryTypeReadRef::DTR_BASEARRAY)
        {
            size_t cnt = fread(D2.GetAddress(), D2.size, 1, fp);
            if(cnt==1)
                ++totalRead;
        }
        else
        {
            D2.DeferredMake();
            size_t cnt = fread(D2.GetAddress(), D2.elementSize, D2.arraySize, fp);
            if(cnt==D2.arraySize)
                ++totalRead;
        }
    }

    if(totalRead != 2)
    {
        errNum = ELMAX_READ_ERROR;
        err = StrUtil::Format(L"{0}: Less than 2 elements are read! 
        ({1} elements read)", GetErrorMsg(errNum), totalRead);
        if(enableException)
            throw new std::runtime_error(StrUtil::ConvToString(err));
    }

    return totalRead;
}

为了简单起见,我在这里没有展示 `BinaryTypeReadRef` 类,因为代码相当复杂,它支持数组类的 `DeferredMake`。

这是 C++11 可变参数模板二进制 `Read` 版本。与前面的二进制 `Write` 相同,第一个函数是结束递归调用的基础函数。与之前的 `Read` 一样,它也利用了 `BinaryTypeReadRef`。

size_t Read()
{
    return 0;
}

template<typename T, typename... Args>
size_t Read( T& t, Args&... args )
{
    BinaryTypeReadRef dt(t);
    size_t totalRead = 0;
    if(fp!=nullptr)
    {
        if(dt.m_type != BinaryTypeReadRef::DTR_STRARRAY && 
           dt.m_type != BinaryTypeReadRef::DTR_WSTRARRAY && 
           dt.m_type != BinaryTypeReadRef::DTR_BASEARRAY)
        {
            size_t cnt = fread(dt.GetAddress(), dt.size, 1, fp);
            if(cnt==1)
                ++totalRead;
        }
        else
        {
            dt.DeferredMake();
            size_t cnt = fread(dt.GetAddress(), dt.elementSize, dt.arraySize, fp);
            if(cnt == dt.arraySize)
                ++totalRead;
        }
    }

    return totalRead + Read(args...);
}

移植到 Linux

在编写 Windows 代码时,我特别注意使用 `_MICROSOFT` 宏来区分 Windows 和非 Windows 代码。我没有使用 `_WIN32` 宏,因为 Mingw 也定义了它。当时 Windows 和非 Windows 代码的主要区别是,在 Windows 上,换行符 ("\n") 在文件写入期间被转换为回车符和换行符的组合 ("\r\n"),在文件读取期间则应用相反的过程;在非 Windows 平台上,换行符 ("\n") 保持不变:不进行任何转换。

我下载并安装了 Orwell Dev-C++,以便在 Windows 上的 Mingw 和 GCC 上测试我的代码。Orwell Dev-C++ 是(目前不活跃的)Bloodshed Dev-C++ 工作成果的延续。Orwell Dev-C++ 捆绑了 Mingw 和相当新的 GCC 4.6.x。在编译期间,Orwell Dev-C++ 抱怨缺少安全的 C 函数(通常名称以 `_s` 结尾),例如 `_itow_s`。因此,我为非 Windows 实现将其更改为非安全版本,而 Windows 实现仍在使用安全版本。Dev-C++ 还抱怨找不到接受字符串的 `std::exception` 构造函数。事实证明 `std::exception` 是用来派生的,而不是直接使用的。我将 `std::exception` 的用法更改为适当的异常类型,例如 `logic_error`、`runtime_error` 等。完成这些更改后,我认为我的大部分 Linux 工作都完成了。我估计,不包括学习 G++ 和编写 makefile 的时间,最多只需 1 小时就能让代码正常工作。那时我才发现我严重低估了解决 Ubuntu Linux 12.04 上错误的所需时间。

在将 Orwell Dev-C++ 的 makefile 转换为适用于 Ubuntu Linux 和 GCC 4.6.3 后,G++ 抱怨的第一个错误是它不理解包含路径。所以我将反斜杠改为了正斜杠。

#include "..\\..\\Common\\Common.h"

上面的路径已更改为如下所示:

#include "../../Common/Common.h"

这是一个简单的更改,尽管我不得不更新大部分 66 个源文件。下一个 G++ 抱怨是它找不到数据转换函数(通常名称以 `_` 开头),例如 `_ultow`。事实证明 Microsoft 的标准转换函数根本不是标准的。我不得不使用 `stringstream` 来替换 `_ultow` 及其类似函数。此时所有编译错误都已解决。然后我运行了单元测试。它在第一个 Unicode 测试时崩溃了!经过一些调查,我沮丧地发现 Linux 和 Mac OSX 上的 `wchar_t` 大小是 4 字节,而不是 2 字节!这意味着所有与 `wchar_t` 相关的函数在 Linux 和 Mac OSX 上都无法正常工作。这显然是一个致命问题!我花了三个辛苦的日子来实现 UTF-16 转换并处理所有 `wchar_t` 大小为 `4` 的实例;Unicode 文件本质上是 UTF16 文件。在 Windows 上,UTF-16 是本地支持的。在 Ubuntu Linux 上,我必须在写入 Unicode 文件之前将 4 字节 `wchar_t` (UTF-32) 转换为 UTF-16。读取时应用反向转换。

如果您有兴趣运行 Linux 测试,可以运行下面的命令行来构建库(FileLib.a)和测试应用程序(UnitTest.exe)并执行它。

cd FileLib
cd FileIO
make all
cd ..
cd PreVS2012UnitTest
make all
./UnitTest.exe

总共有 55 个 Windows 单元测试和 65 个 Linux 单元测试。每当我为任一操作系统进行更改或修复错误时,我都会运行两个操作系统的单元测试,以确保我没有破坏任何一方。

移植到 Clang

Ubuntu 12.04 上的 Clang 3.1 能够使用 GCC 4.7 标准库编译该库。但是,Clang 在 Mac OSX 10.8 上编译失败,因为它找不到带有 `size_t` 参数的重载构造函数。`size_t` 在 32 位平台上等同于 32 位无符号整数,在 64 位平台上是 64 位。显然,Clang 将 `size_t` 视为另一种类型。添加该构造函数的尝试在 Microsoft 编译器上失败,该编译器抱怨已存在类似的构造函数,修复方法是在 `__APPLE__` 检查下隐藏它。

#ifdef __APPLE__
    DataType( size_t ui );
#endif

为了成功在 Clang 下编译,请删除特定于 Microsoft 的文件,例如 `stdafx.h`、`WinOperation.h` / `cpp` 和 Boost 文件,如 `BoostStrategy.h` / `cpp` 和 `RegExStrategy.h` / `cpp`。对于单元测试,可以使用 `LinuxUnitTest.cpp`。

文本文件库的版本 2.0.2(可变参数模板版本,而不是 1.0.x C++98 版本)在内部使用自定义流,因此用户可以为包括 `enum` 在内的任意数据类型编写非侵入性的插入和提取操作。`istream` 和 `ostream` 类利用 Boost `lexical_cast` 执行数据转换,因此其性能应该优于 STL `stringstream`。使用 `istream` 时,读取时无需设置分割策略,但需要通过 `SetLimiter` 指定分隔符。您重载的 `<<`、`>>` 运算符可以使用相同或不同的分隔符。让我们先来看一下使用与文件格式其余部分相同分隔符的重载。

这是结构体 `MyStruct`。

struct MyStruct
{
    int a;
    int b;
};

这些是放在源文件中的重载的 `<<`、`>>` 运算符。

Elmax::ostream operator <<(Elmax::ostream& os, const MyStruct& val)
{
    os << val.a;
    os << L",";
    os << val.b;
    os << L",";

    return os;
}

Elmax::istream operator >>(Elmax::istream& is, MyStruct& val)
{
    is >> val.a;
    is >> val.b;

    return is;
}

现在我们可以像这样读写 `MyStruct` 对象:

// Writing
xTextWriter writer;
std::wstring file = L"...";
writer.Open(file, FT_UTF8, NEW);
writer.Close();

int i = 25698;
double d = 1254.5;
MyStruct my = { 22, 33 };
writer.Write(L"{0},{1},{2}", i, my, d);

// Reading
xTextReader reader;
reader.Open(file);
int i2 = 0;
double d2 = 0.0;
MyStruct my2 = { 0, 0 };

// do not set split strategy but set delimiters instead.
reader.SetDelimiter(L",");
size_t totalRead = reader.ReadLine(i2, my2, d2);

下一个示例,我们将使用管道符 `|` 作为结构体的分隔符,而文档其余部分使用逗号。

struct DiffDelimiterStruct
{
    int a;
    float b;
};
Elmax::ostream operator <<(Elmax::ostream& os, const DiffDelimiterStruct& val)
{
    os << val.a;
    os << L"|";
    os << val.b;
    os << L"|";

    return os;
}

Elmax::istream operator >>(Elmax::istream& is, DiffDelimiterStruct& val)
{
    std::wstring old_delimiter = is.set_delimiter(L"|");

    is >> val.a;
    is >> val.b;

    is.set_delimiter(old_delimiter);

    return is;
}

如下所示,写入和读取与上一个示例相同。

// Writing
xTextWriter writer;
std::wstring file = L"...";
writer.Open(file, FT_UTF8, NEW);
int i = 25698;
double d = 1254.5;
DiffDelimiterStruct my = { 22, 33 };
writer.Write(L"{0},{1},{2}", i, my, d);
writer.Close();

// Reading
xTextReader reader;
reader.Open(file);

int i2 = 0;
double d2 = 0.0;
DiffDelimiterStruct my2 = { 0, 0 };

reader.SetDelimiter(L",");
size_t totalRead = reader.ReadLine(i2, my2, d2);

注意事项

这是用户在使用此文件库时需要注意的问题列表。

  • **请勿在二进制文件中使用 size_t 类型**:在 32 位平台上,`size_t` 是 32 位无符号整数,在 64 位平台上是 64 位无符号整数。在 64 位操作系统上自动提升到 64 位有时是需要的,但对于文件格式来说是错误的。当一个数据在二进制中是 32 位时,我们总是希望它在文件中保持 32 位,以保持一致性。
  • **非 Windows 实现使用 fopen**:Windows 提供 `_wfopen` 函数来打开带有 Unicode 名称的文件。不幸的是,Linux 和 GCC(或者更确切地说,C 标准库)没有这样的函数。C 和 C++ 标准没有说明如何打开 Unicode 命名文件。解决方法是,在其他平台上,当用户即将打开一个包含 Unicode 代码点(> 255)的名称的文件时,应用程序应将该文件复制到另一个 ASCII 名称的文件中,然后打开该文件。
  • **将文件代码放在 try/catch 中**:库可能抛出的异常是 `logic_error`、`runtime_error`、`overflow_error` 和 `underflow_error`。默认情况下启用异常。虽然可以通过 `EnableException` 函数禁用异常,但在发生数据转换错误时仍会抛出异常。这些错误被认为是严重错误,因为文件可能已损坏,因此不允许静默失败。当禁用异常时,用户必须检查每个函数调用的返回值并调用 `GetLastError`。

数据可移植性

到目前为止,我们主要讨论了源代码的可移植性。让我们来讨论一些数据可移植性问题。我们没有遇到任何数据问题,因为使用的平台基于 Intel x86。其他平台可能有不同的字节序(小端序与大端序);文件格式应该有一个字段来存储字节序,类似于 TIFF 图像格式,并在读取时根据需要翻转字节。由于对齐不同,最好逐个写入 `struct` 成员,而不是将 `struct` 写入为扁平数组。不要使用 `size_t`,因为它的大小取决于处理器宽度(32 位与 64 位)。并非所有平台都使用 2 的补码表示负数;可能使用 1 的补码或符号位表示法;您可能还需要存储该信息。如果您认为 `-1` 具有所有位(例如,`0xFFFF`),那么使用 `~0` 会更具可移植性。`Enum` 可能具有不同的值和数据大小。您可以分配数值并强制 `enum` 为特定大小。但是,建议使用 `switch-case` 而不是将 `enum` 强制转换为整数;`switch-case` 对 `enum` 和 C++11 `enum class` 都有效。

enum MYCOLORS
{
    RED = 0,
    YELLOW = 1,
    ....
    NO_USED = 0xFFFFFFFF // force the enum to be 4 bytes wide
}

对于浮点数可移植性,我们应该使用 `numeric_limits<float>::is_iec559` 来检查 IEEE 754 合规性。IEC 60559 是浮点数 IEEE 754 标准的同义词;IEC 60559 有时也称为 IEC 559。

防止内存泄漏

本文上传的原始源代码经测试,使用 Visual Studio 2010/2012 和 Valgrind(Linux)进行正确程序操作时没有内存泄漏。在发生异常时,可能会发生泄漏,因为阻止了去分配的调用。另一个问题是异常在堆上分配,并且在 `catch` 处理程序中未释放(一个疏忽)。所有这些问题都已得到纠正,使用 资源获取即初始化 (RAII) 来处理所有数组以释放内存,并且异常(如果抛出)现在是在栈上分配的。

未来方向

计划将库迁移到 C++11 特性,如可变参数模板、`nullptr` 和移动语义。使用 `uint32_t` 等标准整数类型比使用 `unsigned int` 更清晰。已经提供了一个初步的 C++11 版本可供下载。C++98 版本将继续在不同的 GIT 分支上维护。

下表显示了 C++98 和 C++11 版本中每个类的代码行数(loc)。应用 C++11 可变参数模板后,loc 减少的百分比大于 50%。

C++98 loc C++11 loc 减少百分比
xTextWriter 437 186 57.4%
xTextReader 636 259 59.3%
xBinaryWriter 1067 180 83.1%
xBinaryReader 1123 181 83.9%

关注点

读者可能已经注意到或未注意到代码片段中使用的 Elmax 命名空间。正如任何人都会猜到的,文件库是为了未来的跨平台 Elmax XML 库,但为什么要包含二进制文件 API 呢?原因是因为将有一个版本的 Elmax 可以将 XML 保存为二进制格式。让我们简要回顾一下 Elmax 将值写入 XML 元素。的语法。

using namespace Elmax;
Element elem;

elem[L"Price"] = 30.65f;
elem[L"Qty"] = 1200;

正如读者可以从上面的示例代码中看到,Elmax 元素在将数据转换为文本形式之前就知道数据类型。通过使用数据类型信息,Elmax 可以构建一个关于 XML 的元数据部分。元数据可以分开或嵌入到二进制 XML 中。如果 XML 主要包含重复的元素,元数据可以简洁小巧。但是,如果 XML 文件是由自由形式的 XML 组成,如 SOAP XML、HTML 或 XAML,则元数据相对于二进制 XML 可能相当大。二进制文件之所以具有速度优势,是因为消除了从文本形式到数据类型的转换。

演示

我修改了一个旧的 OpenGL 演示来读取二进制文件,以展示文件库。根据您希望演示加载的文件类型,设置全局变量 `g_bLoadBinary`。请注意,OpenGL 代码不是跨平台的,只能在 Windows 上运行。以前,我曾为另一篇文章上传过一个 OpenGL 演示。由于我只有 NVidia 显卡,我不知道该代码在 Intel 显卡芯片组上运行不正确。这个演示应该没有同样的问题。如果您在运行 OpenGL 演示时遇到任何问题,请告知我。演示是用 OpenGL 2.0 编写的。正在为未来的 OpenGL 文章开发 OpenGL 4.0 版本。如果您对 OpenGL 4.0 感兴趣,请保持关注!

这是加载的木材片段模型。该模型使用非常旧的 Milkshape 共享软件建模。

这是演示的截图。

结论

在本文中,我们看到了一个新的文件 API,它使得读写结构化数据直观且高效。通过保持文本和二进制 API 的相似性,用户可以以最小的精力维护这两种文件格式。文件库将用于新的 Elmax XML 库,用于保存到文本和二进制 XML 文件。XML 工作是一个持续的努力。预计完成日期未知。源代码目前托管在 Github

测试过的编译器

  • Microsoft Visual C++ 8.0, 9.0, 10.0 和 11.0
  • MingW 4.7.x
  • GCC 4.6 和 4.7 (Ubuntu 12.04)
  • Clang 3.1 (Ubuntu 和 Mac OSX 10.8)

Nuget

Elmax C++ 文件库可在 NuGet Gallery 上下载,支持 VS2010 和 VS2012!请记住先将您的 Nuget 更新到最新版本 2.5。

相关链接

参考

  • Brian Hook 的**编写可移植代码**。

历史

  • 2022 年 6 月 28 日:移除了 Boost `lexical_cast`。
  • 2013 年 11 月 26 日:添加了部分。源代码已更新以使用 Boost `lexical_cast`。
  • 2013 年 10 月 31 日:添加了数据可移植性讨论。重要!请阅读。
  • 2013 年 5 月 4 日:更改了文件打开函数,使其不抛出异常,因为文件打开失败是常见错误,而非异常错误。添加了 Nuget 部分。
  • 2013 年 1 月 2 日:添加了表格,显示了更改为 C++11 可变参数模板后减少的代码行数。
  • 2012 年 12 月 23 日:添加了函数的 C++11 可变参数版本。
  • 2012 年 12 月 14 日:添加了防止内存泄漏部分。
  • 2012 年 12 月 12 日:添加了 Clang 支持。
  • 2012 年 9 月 25 日:初始发布。
© . All rights reserved.