我正在尝试将结构写入文件并读回。这样做的代码在这里:
#include <fstream>
#include <iostream>
#include <cstring>
using namespace std;
struct info {
int id;
string name;
};
int main(void) {
info adam;
adam.id = 50;
adam.name = "adam";
ofstream file("student_info.dat", ios::binary);
file.write((char*)&adam, sizeof(info));
file.close();
info student;
ifstream file2("student_info.dat", ios::binary);
file2.read((char*)&student, sizeof(student));
cout << "ID =" << student.id << " Name = " << student.name << endl;
file2.close();
return 0;
}
但是最后我遇到了奇怪的分割错误。
输出为:
ID =50 Name = adam
Segmentation fault (core dumped)
查看核心转储时,我发现在破坏结构信息时发生了一些奇怪的事情。
(gdb) bt
#0 0x00007f035330595c in ?? ()
#1 0x00000000004014d8 in info::~info() () at binio.cc:7
#2 0x00000000004013c9 in main () at binio.cc:21
我怀疑字符串破坏中发生了一些奇怪的事情,但是我无法找出确切的问题。任何帮助都会很棒。
我正在使用gcc 8.2.0。
答案 0 :(得分:3)
您不能像这样序列化/反序列化。在此行上:
file2.read((char*)&student, sizeof(student));
您只是在info
实例(其中包含std::string
)上写1:1。这些不仅仅是字符数组-它们在堆上动态分配其存储并使用指针进行管理。因此,如果您这样覆盖字符串,则该字符串将变为无效,这是未定义的行为,因为其指针不再指向有效位置。
相反,您应该保存实际字符,而不是字符串对象,并在加载时创建一个包含该内容的新字符串。
通常,您可以使用琐碎的对象进行类似的复制。您可以像这样测试它:
std::cout << std::is_trivially_copyable<std::string>::value << '\n';
答案 1 :(得分:3)
要添加到已接受的答案中,因为询问者仍对“为什么在删除第一个对象时崩溃而感到困惑?”:
让我们看一下汇编,因为即使在显示UB的错误程序面前,它也不会说谎(不同于调试器)。
(请注意,rsp
-我们的堆栈指针-除了在main
的开头和结尾处进行调整之外,永远不会更改。)
这是adam
的初始化:
lea rax, [rsp+24]
// ...
mov QWORD PTR [rsp+16], 0
mov QWORD PTR [rsp+8], rax
mov BYTE PTR [rsp+24], 0
似乎[rsp+16]
和[rsp+24]
拥有字符串的大小和容量,而[rsp+8]
拥有指向内部缓冲区的指针。该指针被设置为指向字符串对象本身。
然后adam.name
被"adam"
覆盖:
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long)
由于优化了较小的字符串,[rsp+8]
处的缓冲区指针可能仍指向同一位置(rsp+24
),以指示该字符串我们的缓冲区较小且没有内存分配(这是我的猜测)要清楚)。
稍后,我们以相同的方式初始化student
:
lea rax, [rsp+72]
// ...
mov QWORD PTR [rsp+64], 0
// ...
mov QWORD PTR [rsp+56], rax
mov BYTE PTR [rsp+72], 0
请注意student
的缓冲区指针如何将指向student
以表示一个小的缓冲区。
现在,您将student
的内部残酷地替换为adam
的内部。突然之间,student
的缓冲区指针不再指向预期位置。有问题吗?
mov rdi, QWORD PTR [rsp+56]
lea rax, [rsp+72]
cmp rdi, rax
je .L90
call operator delete(void*)
是的!如果student
的内部缓冲区指向其他地方,而不是我们最初将其设置为(rsp+72
)的地方,则它将delete
指向该指针。此时,我们不知道adam
的缓冲区指针(您复制到student
中)的确切指向,但这肯定是错误的地方。如上所述,"adam"
可能仍被小型字符串优化所覆盖,因此adam
的缓冲区指针可能与之前的位置完全相同:rsp+24
。由于我们将其复制到student
中,并且与rsp+72
不同,因此我们将调用delete(rsp+24)
-位于我们自己的堆栈中间。环境并不觉得很有趣,您会在第一个释放位置中遇到段错误(第二个位置甚至不会delete
,因为那里的世界仍然很好-adam
没有受到您的伤害)。
最重要的是:不要试图超越编译器(“它不能进行段错误,因为它将在同一堆上!”)。你会输的。遵循语言规则,没有人受伤。 ;)
旁注:gcc
中的这种设计甚至可能是故意的。我相信他们可以轻松地存储nullptr
而不是指向字符串对象来表示一个小的字符串缓冲区。但是在那种情况下,您不会因这种不当行为而遭受打击。
答案 2 :(得分:0)
简要地并从概念上进行思考,当完成adam.name = "adam";
时,会在内部为adam.name
分配适当的内存。
完成file2.read((char*)&student, sizeof(student));
时,您正在存储器位置即地址&student
上写,该地址尚未正确分配以容纳正在读取的数据。 student.adam
没有分配足够的有效内存。在read
对象的位置上进行这样的student
处理实际上会导致内存损坏。