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

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

本文关键字:错误 分段 读取 C++ 对象 文件      更新时间:2023-10-16

在我的代码中,我首先将名称和手机号码存储在一个对象中,然后使用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<<"NamettPhone no"<<endl;
cout<<name<<"tt"<<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();
}

请帮我哪里错了。提前感谢。。。!

所以,在我给你一个解决方案之前,让我们简要谈谈这里发生了什么:

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

您正在做的是将t1强制转换为指向char的指针,并说"按原样将t1的内存表示写入ofs"。所以我们必须问问自己:t1的记忆表示是什么?

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

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

std::string肯定不好。字符串是复杂的对象,它们通常在堆上分配存储(尽管有小字符串优化之类的事情)。这意味着您将序列化指向动态分配对象的指针。这永远不会起作用,因为读回指针会指向内存中您完全无法控制的某个位置。这是未定义行为的一个很好的例子。任何事情都会发生,你的程序可能会发生任何事情,包括尽管存在深层次的问题,但"看起来工作正常"。在您的特定示例中,因为创建的Telephone对象仍在内存中,所以您得到的是指向相同动态分配内存的2个指针。当temp对象超出范围时,它会删除该内存。

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

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

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

edit:仅仅使用operator<<operator>>std::string不起作用,稍后解释了原因

如何使其发挥作用:

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

我的基本方法是覆盖类电话的operator<<operator>>

我声明两个免费函数,它们是电话类的朋友。这将允许他们探查不同电话对象的内部,以序列化其成员。

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

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

实现这些函数的代码有一个令人惊讶的转折。由于字符串的operator>>在遇到空白时会停止从流中读取,因此名称不是一个单词或带有特殊字符将不起作用,并使流处于错误状态,无法读取电话号码。为了解决这个问题,我以@Michael Vecsler的例子为例,显式地存储了字符串的长度。我的实现如下:

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;
}

问题

不能简单地将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编写为位的集合。这些CCD_ 27比特中的一些可以是从CCD_ 28获得的指针。这些指针指向包含字符串字符的堆内存。

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

解决方案

您必须为此类编写自己的序列化代码,而不是依赖于ofs.write()。有几种方法可以做到这一点。首先,可以使用boost序列化。您可以简单地按照链接教程中的示例进行序列化。

另一种选择是自己从头开始做每件事。当然,最好使用现有的代码(比如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<<不同,此技术适用于任何类型的字符串。它可以很好地处理包含任何字符的字符串,包括空格和空字符()。operator>>技术不适用于有空格的字符串,例如名字和姓氏的组合,因为它在空格处停止。


请注意,有一些专门的分配器使序列化/反序列化变得微不足道。这样的分配器可以从预先分配的缓冲区内分配字符串,并使用高级指针(如boost::interprocess::offset_ptr)进入该缓冲区。这样就可以简单地转储整个缓冲区,然后再重新读取它,而不会有任何麻烦。出于某种原因,这种方法并不常用。


关键:

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

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