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

Segmentation fault occur while reading content from file in object C++

在我的代码中,首先我将姓名和手机号码存储在一个对象中,然后我使用 fstream.write() 方法将该对象写入一个文本文件。它成功运行,但是当我将写入的内容读入另一个对象并调用显示方法时,它会正确显示数据,但在打印数据后会出现分段错误。 这是我的代码 -

#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();
}

哪里不对请大家帮帮我。 提前致谢...!

问题

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

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

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

通过调用ofs.write(),程序写入指针,但不写入字符。然后当使用 ifs.read() 读取字符串时,它有指向未分配堆的指针,其中不包含字符。即使它确实指向一个有效的堆,奇迹般地,它仍然不会包含它应该具有的字符。有时你可能幸运,程序不会崩溃,因为字符串足够短,可以避免堆分配,但它是完全不可靠的。

解决方案

您必须为此 class 编写自己的序列化代码,而不是依赖 ofs.write()。有几种方法可以做到这一点。首先,您可以使用 boost serialization. You can simply follow the examples in the linked tutorial 并为您进行序列化。

另一种选择是自己从头开始做所有事情。当然,最好使用现有代码(如 boost),但自己实现它可能是一个很好的学习经验。通过自己实施,您可以更好地了解 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<< 不同,此技术适用于任何类型的字符串。它适用于包含任何字符的字符串,包括空格和空字符 ([=27=])。 operator>> 技术不适用于包含空格的字符串,例如名字和姓氏的组合,因为它会在空格处停止。


请注意,有专门的分配器使 serialization/deserialization 变得微不足道。这样的分配器可以从预先分配的缓冲区中分配字符串,并使用奇特的指针(例如 boost::interprocess::offset_ptr)进入此缓冲区。然后可以简单地转储整个缓冲区并在以后重新读取它而不会遇到麻烦。由于某些原因,这种方法并不常用。


严重:

安全是个问题。如果数据不在您的控制之下,那么它可以用来入侵您的系统。在我的序列化示例中,最糟糕的情况是 运行 内存不足。 运行 内存不足可能是拒绝服务或更糟的攻击媒介。也许你应该限制字符串的最大大小,并管理错误。

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

所以,在我给你解决方案之前,让我们简单谈谈这里是怎么回事:

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

你正在做的是将 t1 转换为指向 char 的指针,然后说 'write to ofs the memory representation of t1, as is'。所以我们要问自己:t1的这个内存表示是什么?

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

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

std::string肯定不行。字符串是复杂的对象,它们最常在堆上分配存储空间(尽管有小字符串优化之类的东西)。这意味着您将序列化一个指向动态分配对象的指针。这永远行不通,因为读回指针会指向内存中您完全无法控制的某个位置。这是未定义行为的一个很好的例子。任何事情都会发生,并且您的程序可能会发生任何事情,包括 'appearing to work correct' 尽管存在根深蒂固的问题。 在您的具体示例中,因为创建的 Telephone 对象仍在内存中,所以您得到的是 2 个指向同一动态分配内存的指针。当您的 temp 对象超出范围时,它会删除该内存。

当您 return 到您的主函数时,当 t1 超出范围时,它会尝试再次删除相同的内存。

序列化任何类型的指针都是一个很大的禁忌。如果您的对象内部由指针组成,您需要针对如何将这些指针存储在流中制定自定义解决方案,然后再读取以构建新对象。一个常见的解决方案是存储它们 'as if' 它们是按值存储的,以后从存储中读取对象时,动态分配内存并将对象的内容放在同一块内存中。如果您尝试序列化多个对象指向内存中相同地址的情况,这显然不起作用:如果您尝试应用此解决方案,您最终会得到原始对象的多个副本。

幸运的是,对于 std::string 这个问题很容易解决,因为字符串已经重载了 operator<<operator>>,你不需要实施任何使它们工作的东西。

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

如何让它工作:

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

我的基本方法是为 class telephone 覆盖 operator<<operator>>

我声明了两个免费功能,那是Telephone的朋友class。这将允许他们戳穿不同 telephone 对象的内部结构以序列化他们的成员。

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

编辑:我最初的字符串序列化代码是错误的,所以我认为它相当简单的评论显然是错误的

实现这些功能的代码有一个令人惊讶的转折。因为 operator>> for strings 在遇到空格时停止从流中读取,具有非单个单词或特殊字符的名称将不起作用,并将流置于错误状态,无法读取phone 号。为了解决这个问题,我按照@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 class 进行任何类型的修改,您将需要修改 both 函数.

要支持名称中有空格,您从 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;
}