序列化一个包含std::string的类

16

我不是C++专家,但之前已经几次序列化过东西。不幸的是,这一次我试图序列化一个包含 std::string 的类,这个类似乎就像序列化一个指针。

我可以把这个类写入文件并读回来。所有的 int 字段都没问题,但是 std::string 字段会出现“地址越界”的错误,可能是因为它指向的数据不再存在。

有没有标准的解决办法?我不想回到使用 char 数组,但至少我知道在这种情况下它们是有效的。如果需要,我可以提供代码,但我希望我已经清楚地解释了我的问题。

我通过将该类转换为 char* 并使用 std::fstream 将其写入文件来进行序列化。当然,读取也是反过来的。


似乎唯一的真正问题是如何定界字符串,但使用字符数组也会遇到这个问题。我不知道你遇到了什么麻烦,对我来说将字符串序列化似乎非常容易。也许你最好发布一些代码。 - john
Java有标准序列化功能(在标准库中)。C++语言和STL库中都没有这样的功能。有一些外部库可以实现,例如boost可以做到这一点。另一种选择是使用Google的协议缓冲区。 - osgx
1
吹毛求疵:你正在序列化一个对象。 - xtofl
一个中间结构对我来说是有意义的。这确实引出了一个问题,为什么我首先要费心处理这些字符串,从长远来看似乎是一种虚假的节约。 - iwasinnamuknow
如果你使用的是Linux系统,另一种好的方法是构建一个IOV数组,并将其传递给writev函数(http://linux.die.net/man/2/writev),以便一次性写入所有内容。 - RocketR
显示剩余4条评论
7个回答

16

我通过将类强制转换为char*并使用fstream将其写入文件来进行序列化,然后反向读取。但是如果涉及到指针,这种方法就不起作用了。你可能希望给你的类添加void MyClass::serialize(std::ostream)void MyClass::deserialize(std::ifstream)方法,并调用它们。对于这种情况,你需要

std::ostream& MyClass::serialize(std::ostream &out) const {
    out << height;
    out << ',' //number seperator
    out << width;
    out << ',' //number seperator
    out << name.size(); //serialize size of string
    out << ',' //number seperator
    out << name; //serialize characters of string
    return out;
}
std::istream& MyClass::deserialize(std::istream &in) {
    if (in) {
        int len=0;
        char comma;
        in >> height;
        in >> comma; //read in the seperator
        in >> width;
        in >> comma; //read in the seperator
        in >> len;  //deserialize size of string
        in >> comma; //read in the seperator
        if (in && len) {
            std::vector<char> tmp(len);
            in.read(tmp.data() , len); //deserialize characters of string
            name.assign(tmp.data(), len);
        }
    }
    return in;
}

您可能还想重载流运算符以便更轻松地使用。

std::ostream &operator<<(std::ostream& out, const MyClass &obj)
{obj.serialize(out); return out;}
std::istream &operator>>(std::istream& in, MyClass &obj)
{obj.deserialize(in); return in;}

看起来很有趣,对现有的代码/工作流程没有太大影响。我会试一下。谢谢。 - iwasinnamuknow
1
(1) 您的流需要通过引用传递,istream和ostream的复制构造函数已禁用。 (2) 宽度和高度以及字符串的大小将在输出时连接在一起,因此读取它们将得到一个单独的数字。 - Benjamin Lindley
in.read(&name[0], len); 这肯定是错误的。你不能把一个字符串当作向量来处理。即使作为向量,如果len == 0,它也会失败。 - john
@John 和 Rudy:不是的。std::string(非const)operator[]返回一个char&,因此该地址是字符串中的char *。由于我已调整大小为确切长度,因此这是所有定义行为,并且有效。 (如果len为零,则会失败) - Mooing Duck
如果字符串中包含嵌入的NUL字符(\0),使用c_str()写入将会创建问题,因为会写入较少的字符。您应该循环遍历字符串并编写正确数量的字符,或者如果想要在第一个NUL之后丢弃字符,则应编写strlen(c_str())而不是size。如果在字符串中存储了NUL,则编写sizec_str将使您读取损坏的数据。 - 6502
显示剩余5条评论

10

将对象的二进制内容直接写入文件不仅不可移植,而且正如您所认识到的那样,对于指针数据也行不通。你基本上有两个选择:要么编写一个真正的序列化库,通过例如使用c_str()输出实际字符串到文件中来正确处理std::strings,要么使用出色的boost序列化库。如果可能的话,我建议使用后者,然后可以像这样使用简单的代码进行序列化:

#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <boost/serialization/string.hpp>

class A {
    private:
        std::string s;
    public:
        template<class Archive>
        void serialize(Archive& ar, const unsigned int version)
        {
            ar & s;
        }
};

这里的serialize函数可用于将数据进行序列化或反序列化处理,具体取决于你如何调用它。请查看文档以获取更多信息。


1
优秀的想法。然而,看起来你展示了“后者”的例子 - 使用boost库,而你建议使用“前者”... - xtofl
我以前没有深入了解过boost,但下次我会去看看。谢谢。 - iwasinnamuknow

3

对于可变大小的字符串或其他二进制数据,最简单的序列化方法是先序列化大小(如序列化整数一样),然后将内容复制到输出流中。

在读取时,您首先读取大小,然后分配字符串,最后通过从流中读取正确数量的字节来填充它。

另一种选择是使用定界符和转义,但需要更多代码,并且在序列化和反序列化方面都较慢(但结果可以保持易于阅读)。


1
如果您的类包含任何外部数据(例如string),则必须使用比将类强制转换为char*并将其写入文件更复杂的序列化方法。您关于为什么出现分段错误的正确。
我会创建一个成员函数,它将接受一个fstream并从中读取数据,以及一个反向函数,它将接受一个fstream并将其内容写入文件以便稍后恢复,就像这样:
class MyClass {
pubic:
    MyClass() : str() { }

    void serialize(ostream& out) {
        out << str;
    }

    void restore(istream& in) {
        in >> str;
    }

    string& data() const { return str; }

private:
    string str;
};

MyClass c;
c.serialize(output);

// later
c.restore(input);

您还可以定义operator<<operator>>来与istreamostream一起使用,以便序列化和恢复您的类,如果您想要这种语法糖。


如果将write/read操作用作成员函数,它们会有不同的行为吗?我真的不明白它如何写入实际字符而不是指针地址。 - iwasinnamuknow
@iwasinnamuknow:作为成员函数使用时,读写操作并没有表现出不同的行为,你是怎么想到的? - john
@iwasinnamuknow 这里使用了 (i|o)streamoperator<<>>,用于将 string 的内容写入文件。显然,你可能有多个数据成员,所以只需将它们全部写入输出文件,然后按照相同的顺序从输入文件中读取即可。 - Seth Carnegie
1
@john 这只是一个快速的例子。 - Seth Carnegie
这将在字符串中第一个空格处停止阅读。 - Costantino Grana

0
/*!
 * reads binary data into the string.
 * @status : OK.
*/

class UReadBinaryString
{
    static std::string read(std::istream &is, uint32_t size)
    {
        std::string returnStr;
        if(size > 0)
        {
            CWrapPtr<char> buff(new char[size]);       // custom smart pointer
            is.read(reinterpret_cast<char*>(buff.m_obj), size);
            returnStr.assign(buff.m_obj, size);
        }

        return returnStr;
    }
};

class objHeader
{
public:
    std::string m_ID;

    // serialize
    std::ostream &operator << (std::ostream &os)
    {
        uint32_t size = (m_ID.length());
        os.write(reinterpret_cast<char*>(&size), sizeof(uint32_t));
        os.write(m_ID.c_str(), size);

        return os;
    }
    // de-serialize
    std::istream &operator >> (std::istream &is)
    {
        uint32_t size;
        is.read(reinterpret_cast<char*>(&size), sizeof(uint32_t));
        m_ID = UReadBinaryString::read(is, size);

        return is;
     }
};

@RocketR。我写了union吗?好的,已经修复了。你知道这只是从我的一些旧项目文件中快速复制的代码部分。 - legion

0
为什么不考虑类似这样的东西:
std::ofstream ofs;
...

ofs << my_str;

然后:

std::ifstream ifs;
...

ifs >> my_str; 

那不是假设字符串与其他内容分开吗?我想要整个类及其内容一次性地写入/读取。 - iwasinnamuknow
这个能处理包含空格和/或换行符的字符串吗? - 6502
@John:说得好,确实不会。但是它也不能与原始的char *一起使用。 - Oliver Charlesworth
1
@Oli:这肯定是重点,OP声称序列化std::string比序列化char数组更难,这就是我不理解的部分,除非他解释清楚,否则我认为我们不会有太大进展。 - john
@iwas:不,还有很多情况下这种方法行不通。它只可能适用于POD(纯数据结构)。 - Oliver Charlesworth
显示剩余4条评论

-3

我已经很久没写C++了,但也许你可以将一个char数组序列化。

然后,当你打开文件时,你的string就只需指向该数组。

仅供参考。


1
LPTSTR 不具备可移植性(仅限 Windows)。 - osgx
他不想回到使用数组的方式。 - Seth Carnegie
我并不完全反对使用字符数组,但我已经努力使用std::strings代替它们,因为厌倦了被告知我过时了。如果它们能让事情变得更容易,那么我可能会回头使用字符数组。 - iwasinnamuknow
1
他们肯定不会让事情变得更容易。但是为什么不先读入一个字符数组,然后再将字符数组赋值给字符串呢?这很难吗? - john
那个,我认为这是可移植的。我并不是在告诉那个人如何编写健壮的代码,我只是提出了一个可能解决他问题的想法。 - jp2code
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接