结构破坏中的奇怪行为
Weird behavior in struct destruction
我正在尝试将结构写入文件并将其读回。执行此操作的代码如下:
#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。
你不能像那样序列化/反序列化。在此行中:
file2.read((char*)&student, sizeof(student));
你只是在info
的实例上写 1:1,其中包括一个std::string
。这些不仅仅是字符数组 - 它们在堆上动态分配存储并使用指针进行管理。因此,如果您这样覆盖字符串,则字符串将变得无效,这是未定义的行为,因为它的指针不再指向有效位置。
相反,您应该保存实际字符,而不是字符串对象,并在加载时使用该内容创建一个新字符串。
通常,您可以使用琐碎的对象进行此类复制。您可以像这样测试它:
std::cout << std::is_trivially_copyable<std::string>::value << 'n';
为了补充接受的答案,因为提问者仍然对"为什么它在删除第一个对象时崩溃?"感到困惑:
让我们看一下双汇编,因为它不能撒谎,即使面对显示 UB 的错误程序(与调试器不同)。
https://godbolt.org/z/pstZu5
(请注意,除了main
开头和结尾的调整外,rsp
- 我们的堆栈指针 - 永远不会更改)。
以下是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
以表示小缓冲区。
现在你残酷地用adam
的内部替换student
的内部.突然,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
,而不是指向字符串对象来表示一个小的字符串缓冲区。但在这种情况下,你不会从这种渎职行为中分离出来。
简要地从概念上思考,当adam.name = "adam";
完成后,内部会为adam.name
分配适当的内存。
完成后file2.read((char*)&student, sizeof(student));
,您将在内存位置写入,即地址&student
尚未正确分配以容纳正在读取的数据。student.adam
没有足够的有效内存分配给它。在对对象的位置执行此类read
student
实际上会导致内存损坏。
- 如何循环打印顶点结构
- 通过方法访问结构
- 使用不带参数的函数访问结构元素
- 预处理器:插入结构名称中的前一个行号
- 为什么在没有显式默认构造函数的情况下,将另一个结构封装在联合中作为成员的结构不能编译
- 孤立代码块在结构中引发异常
- 有什么方法可以遍历结构吗
- 如何在 C# 中映射双 C 结构指针?
- 如何在C++中使用结构生成映射
- 无法将结构注册为增强几何体3D点
- 多成员Constexpr结构初始化
- C++将文本文件中的数据读取到结构数组中
- 如何重构类层次结构以避免菱形问题
- 如何在C++中序列化结构数据
- std::vector的包装器,使数组的结构看起来像结构的数组
- 没有为自己的结构调用列表推回方法
- 奇怪的结构&GCC&clang(void*返回类型)
- 在 c++ 中拥有一组结构的正确方法是什么?
- vscode g++链路故障:体系结构x86_64的未定义符号
- C++概念:如何使用'concept'检查模板化结构的属性?