从对象C ++中的文件读取内容时发生分段错误

时间:2018-10-07 05:48:03

标签: c++ file file-io c++14 fstream

首先在代码中,我将名称和手机号码存储在一个对象中,然后使用fstream.write()方法将该对象写入一个文本文件。它可以成功工作,但是当我将写入的内容读入另一个对象并调用display方法时,它将正确显示数据,但是在打印数据后却给我带来了分割错误。 这是我的代码-

#include<iostream>
#include<fstream>
using namespace std;
class Telephone
{
private:
    string name="a";
    int phno=123;
public:
    void getTelephoneData()
    {
        cout<<"Enter Name:";
        cin>>name;
        cout<<"Enter Phone Number:";
        cin>>phno;
    }
    void displayData()
    {
        cout<<"Name\t\tPhone no"<<endl;
        cout<<name<<"\t\t"<<phno<<endl;
    }

    void getData() {
        Telephone temp;
        ifstream ifs("Sample.txt",ios::in|ios::binary);
        ifs.read((char*)&temp,sizeof(temp));
        temp.displayData();
    }   
};
int main()
{
    Telephone t1;
    t1.getTelephoneData();
    cout<<"----Writing Data to file------"<<endl;
    ofstream ofs("Sample.txt",ios::out|ios::binary);
    ofs.write((char*)&t1,sizeof(t1));
    ofs.close();
    t1.getData();
}

请在错误之处帮助我。 预先谢谢...!

2 个答案:

答案 0 :(得分:4)

因此,在我给您提供解决方案之前,让我们简要介绍一下这里发生的事情:

ofs.write((char*)&t1,sizeof(t1));

您正在做的是将t1转换为指向char的指针,然后说“按原样写入to1的内存表示形式”。因此,我们不得不自问:t1的内存表示是什么?

  1. 您要存储一个(实现定义的,很可能是4个字节)整数
  2. 您还将存储一个复杂的std :: string对象。

写4字节整数可能没问题。它绝对不是可移植的(big-endian与little-endian),如果在具有不同字节序的平台上读取文件,则可能会得到错误的int值。

std::string绝对不行。字符串是复杂的对象,它们通常在堆上分配存储(尽管有诸如小字符串优化之类的东西)。这意味着您将序列化一个指向动态分配对象的指针。这永远都行不通,因为向后读取指针将指向您绝对无法控制的内存中的某个位置。这是未定义行为的一个很好的例子。任何事情都会发生,并且程序可能会发生任何事情,包括尽管存在严重问题,但“看起来工作正确”。 在您的特定示例中,由于创建的Telephone对象仍在内存中,因此您获得的是指向同一动态分配内存的2个指针。当您的temp对象超出范围时,它将删除该内存。

返回主功能时,t1超出范围时,它将尝试再次删除相同的内存。

对任何类型的指针进行序列化是一个很大的禁忌。如果对象内部由指针组成,则需要针对这些指针将如何存储在流中做出自定义解决方案,然后读取以构造一个新对象。常见的解决方案是“好像”将它们存储为按值存储,然后,当从存储中读取对象时,动态分配内存并将对象的内容放在同一内存中。如果尝试序列化多个对象指向内存中同一地址的情况,这显然将不起作用:如果尝试应用此解决方案,最终将获得原始对象的多个副本。

幸运的是,对于std::string而言,此问题很容易解决,因为字符串已使operator<<operator>>重载,并且您无需执行任何操作即可使他们工作。

编辑:仅使用operator<<operator>>不能用于std::string,稍后再解释原因。

如何使其工作:

有许多可能的解决方案,我将在这里分享一个。 基本思想是,您应该分别序列化电话结构的每个成员,并依靠每个成员都知道如何序列化自身这一事实。我将忽略跨字节序兼容性的问题,以使答案更简短,但是如果您关心跨平台兼容性,则应考虑一下。

我的基本方法是为类电话改写operator<<operator>>

我声明了两个免费功能,它们是Telephone类的朋友。这将使他们可以戳入不同电话对象的内部,以序列化其成员。

class Telephone { 
   friend ostream& operator<<(ostream& os, const Telephone& telephone);
   friend istream& operator>>(istream& is, Telephone& telephone);
   // ... 
};

编辑:最初,我有错误地序列化字符串的代码,所以我的评论很简单,就是明显的错误

用于实现功能的代码令人惊讶。因为遇到空白时,用于字符串的operator>>会停止从流中读取数据,因此使用名称不是单个单词或带有特殊字符的名称将不起作用,并将流置于错误状态,从而导致无法读取电话号码。为了解决这个问题,我在@Michael Veksler后面跟随了这个示例,并明确存储了字符串的长度。我的实现如下所示:

ostream& operator<<(ostream& os, const Telephone& telephone)
{
    const size_t nameSize = telephone.name.size();
    os << nameSize;
    os.write(telephone.name.data(), nameSize);
    os << telephone.phno;
    return os;
}

istream& operator>>(istream& is, Telephone& telephone)
{
    size_t nameSize = 0;
    is >> nameSize;
    telephone.name.resize(nameSize);
    is.read(&telephone.name[0], nameSize);
    is >> telephone.phno;
    return is;
}

请注意,您必须确保所写入的数据与稍后将要尝试读取的数据匹配。如果存储的信息量不同,或者参数的顺序错误,则最终不会得到有效的对象。如果以后要对Telephone类进行任何形式的修改,则通过添加要保存的新字段,您将需要修改两者函数。

要支持名称中包含空格的名称,还应修改从cin读取名称的方式。一种方法是使用std::getline(std::cin, name);代替cin >> name

最后,如何从这些流中进行序列化和反序列化: 不要使用ostream::write()istream::read()函数-改用我们已覆盖的operator<<operator>>

void getData() {
    Telephone temp;
    ifstream ifs("Sample.txt",ios::in|ios::binary);
    ifs >> temp;
    temp.displayData();
} 

void storeData(const Telephone& telephone) {
    ofstream ofs("Sample.txt",ios::out|ios::binary);
    ofs << telephone;
}

答案 1 :(得分:1)

问题

您不能简单地将std::string个对象转储到文件中。请注意,std::string被定义为

std::basic_string<char, std::char_traits<char>, std::allocator<char>>

std::string无法避免时,它使用std::allocator<char>为字符串分配堆内存。通过将Telephone对象与ofs.write((char*)&t1,sizeof(t1))一起写入,您还在写std::string它包含为一组位的对象。这些std::string位中的某些位可以是从std::allocator获得的指针。这些指针指向包含字符串字符的堆内存。

通过调用ofs.write(),程序将写入指针,但不会写入字符。然后,当使用ifs.read()读取字符串时,它具有指向未分配堆的指针,该指针不包含字符。即使它确实指向了一个有效的堆,从某种程度上讲,它仍然不会包含它应该具有的字符。有时您可能很幸运,并且程序不会因为字符串太短而避免堆分配而崩溃,但这是完全不可靠的。

解决方案

您必须为此类编写自己的序列化代码,而不要依赖ofs.write()。有几种方法可以做到这一点。首先,您可以使用boost serialization。您只需按照linked tutorial中的示例进行操作,即可进行序列化。

另一种选择是从头开始做所有事情。当然,最好使用现有的代码(例如boost),但是亲自实现它可能是一种很好的学习经验。通过实施自己,您可以更好地了解如何在后台进行提升:

void writeData(std::ostream & out) const {
    unsigned size = name.size();
    out.write((char*)&size, sizeof(size));
    out.write(name.data(), size);

    out.write((char*)&phno, sizeof(phno));
}   

然后在getData中以相同的顺序读取它。当然,您必须动态地将字符串分配给正确的大小,然后用ifs.read()填充它。

operator<<用于字符串不同,此技术适用于任何类型的字符串。它适用于包含任何字符的字符串,包括空格和空字符(\0)。 operator>>技术不适用于带有空格的字符串,例如名字的姓氏组合,因为它停在空白处。


注意,有一些专门的分配器使序列化/反序列化变得微不足道。这样的分配器可以从预分配的缓冲区内部分配字符串,并在该缓冲区中使用奇特的指针(例如boost::interprocess::offset_ptr)。这样就可以简单地转储整个缓冲区,并在以后轻松地重新读取它。由于某种原因,这种方法并不常用。


严重:

安全性是一个问题。如果数据不受您的控制,则可以使用它来入侵您的系统。在我的序列化示例中,更糟的是耗尽内存。内存不足可能是拒绝服务的攻击媒介,或更糟的是。也许您应该限制字符串的最大大小并管理错误。

要考虑的另一件事是跨系统的互操作性。并非所有系统都以相同的方式表示intlong。例如,在64位linux上,long是8字节,而在MS-Windows上是4字节。最简单的解决方案是使用out<<size<<' '来写出大小,但是请确保使用C语言环境,否则四位数的长度中可能包含逗号或点,这会破坏解析。