重用分配给 class 的变量时,为什么只有最后一次析构函数调用导致崩溃?
When reusing a variable assigned to a class, why is only the last destructor call causing a crash?
我有一个 class 在构造函数中分配内存并在析构函数中释放它的情况 - 非常基本的东西。如果我为 class 的新实例重用 class 实例变量,就会出现问题。当最后一个(并且只有最后一个)实例在超出范围时被销毁时,它将在调用 free/delete:
时崩溃并出现 SIGABRT
malloc: *** error for object 0xXXXXX: pointer being freed was not allocated
我觉得我遗漏了一些基本的东西,我想了解我的错误,以便将来避免它。
这是一个简单的重现:
class AllocTest {
public:
AllocTest(const char *c) {
this->data = new char[strlen(c) + 1];
strcpy(this->data, c);
}
~AllocTest() {
if (this->data != NULL) {
delete[] data;
}
}
private:
char* data;
};
int main(int argc, const char *argv[]) {
const char* c = "test test test";
AllocTest t(c);
t = AllocTest(c);
t = AllocTest(c);
return 0;
}
我可以在调试器中看到,每次 t
都会根据前一个实例重新分配调用的析构函数,为什么只有最终的析构函数会导致崩溃?我使用 new
/delete
还是 malloc
/free
都没有关系——无论哪种方式都是相同的行为——而且只在 上final 释放。如果我四处移动示波器或其他任何东西也没关系——一旦最终示波器离开,崩溃就会发生。
如果将变量 't' 限制在它自己的范围内并且不尝试重用它,这不会重现——例如,如果我这样做,一切都很好:
for (int i = 0; i < 100; i++) {
AllocTest t(c);
}
解决这个问题很简单,但我更愿意理解我为什么会遇到这个问题。
解决方案:
感谢@user17732522的回答,我现在明白问题出在哪里了。我没有意识到,当我重新分配 t
时,它实际上是在制作 class 的副本 - 我正在假设与我通常使用的其他语言一样,分配会覆盖它。当我无意中陷入 classic “double free” 问题时,意识到这一切都是有道理的。 copy initialization and the pointers to the documentation about the rule of three pattern 上的文档帮助填补了此处的其余空白。
只需修改我的 class 来定义隐式复制语义就足以让代码按预期工作:
AllocTest& operator=(const AllocTest& t) {
if (this == &t) {
return *this;
}
size_t newDataLen = strlen(t.data) + 1;
char* newData = new char[newDataLen];
strcpy(newData, t.data);
delete this->data;
this->data = newData;
return *this;
}
谢谢大家!
首先,构造函数本身具有未定义的行为,因为您只为字符串 (strlen(c)
) 的长度分配了足够的 space,而错过了 [=48= 所需的附加元素].
假设您在下文中修复了该问题。
object/instance 上的析构函数仅被调用 一次。
在您的代码中 t
始终是同一个实例。它永远不会被新的取代。您只分配给它,它使用隐式复制赋值运算符 copy-assign 从临时对象到 t
的成员 one-by-one.
t
上的析构函数仅在其范围在 return
语句之后结束时被调用一次。
此析构函数调用具有未定义的行为,因为最后一个 AllocTest(c)
临时对象的析构函数已经删除了此时 t.data
指向的已分配数组(它已被分配该值 t = AllocTest(c);
).
此外,AllocTest t(c);
中的第一个分配已泄露,因为您用第一个 t = AllocTest(c);
.
覆盖了 t.data
指针
只有 AllocTest t(c);
你没有复制任何指针,所以这不会发生。
这里的根本问题是您的 class 违反了 rule of 0/3/5:如果您有析构函数,您还应该使用正确的语义定义 copy/move 构造函数和赋值运算符。如果您需要自定义析构函数,隐式的(您在这里使用的)可能会做错事。
或者更好的是,通过不手动分配内存来使 rule-of-zero 工作。使用 std::string
代替,您不必定义析构函数或任何特殊的成员函数。
这也自动解决了长度不匹配问题。
我有一个 class 在构造函数中分配内存并在析构函数中释放它的情况 - 非常基本的东西。如果我为 class 的新实例重用 class 实例变量,就会出现问题。当最后一个(并且只有最后一个)实例在超出范围时被销毁时,它将在调用 free/delete:
时崩溃并出现 SIGABRTmalloc: *** error for object 0xXXXXX: pointer being freed was not allocated
我觉得我遗漏了一些基本的东西,我想了解我的错误,以便将来避免它。
这是一个简单的重现:
class AllocTest {
public:
AllocTest(const char *c) {
this->data = new char[strlen(c) + 1];
strcpy(this->data, c);
}
~AllocTest() {
if (this->data != NULL) {
delete[] data;
}
}
private:
char* data;
};
int main(int argc, const char *argv[]) {
const char* c = "test test test";
AllocTest t(c);
t = AllocTest(c);
t = AllocTest(c);
return 0;
}
我可以在调试器中看到,每次 t
都会根据前一个实例重新分配调用的析构函数,为什么只有最终的析构函数会导致崩溃?我使用 new
/delete
还是 malloc
/free
都没有关系——无论哪种方式都是相同的行为——而且只在 上final 释放。如果我四处移动示波器或其他任何东西也没关系——一旦最终示波器离开,崩溃就会发生。
如果将变量 't' 限制在它自己的范围内并且不尝试重用它,这不会重现——例如,如果我这样做,一切都很好:
for (int i = 0; i < 100; i++) {
AllocTest t(c);
}
解决这个问题很简单,但我更愿意理解我为什么会遇到这个问题。
解决方案:
感谢@user17732522的回答,我现在明白问题出在哪里了。我没有意识到,当我重新分配 t
时,它实际上是在制作 class 的副本 - 我正在假设与我通常使用的其他语言一样,分配会覆盖它。当我无意中陷入 classic “double free” 问题时,意识到这一切都是有道理的。 copy initialization and the pointers to the documentation about the rule of three pattern 上的文档帮助填补了此处的其余空白。
只需修改我的 class 来定义隐式复制语义就足以让代码按预期工作:
AllocTest& operator=(const AllocTest& t) {
if (this == &t) {
return *this;
}
size_t newDataLen = strlen(t.data) + 1;
char* newData = new char[newDataLen];
strcpy(newData, t.data);
delete this->data;
this->data = newData;
return *this;
}
谢谢大家!
首先,构造函数本身具有未定义的行为,因为您只为字符串 (strlen(c)
) 的长度分配了足够的 space,而错过了 [=48= 所需的附加元素].
假设您在下文中修复了该问题。
object/instance 上的析构函数仅被调用 一次。
在您的代码中 t
始终是同一个实例。它永远不会被新的取代。您只分配给它,它使用隐式复制赋值运算符 copy-assign 从临时对象到 t
的成员 one-by-one.
t
上的析构函数仅在其范围在 return
语句之后结束时被调用一次。
此析构函数调用具有未定义的行为,因为最后一个 AllocTest(c)
临时对象的析构函数已经删除了此时 t.data
指向的已分配数组(它已被分配该值 t = AllocTest(c);
).
此外,AllocTest t(c);
中的第一个分配已泄露,因为您用第一个 t = AllocTest(c);
.
t.data
指针
只有 AllocTest t(c);
你没有复制任何指针,所以这不会发生。
这里的根本问题是您的 class 违反了 rule of 0/3/5:如果您有析构函数,您还应该使用正确的语义定义 copy/move 构造函数和赋值运算符。如果您需要自定义析构函数,隐式的(您在这里使用的)可能会做错事。
或者更好的是,通过不手动分配内存来使 rule-of-zero 工作。使用 std::string
代替,您不必定义析构函数或任何特殊的成员函数。
这也自动解决了长度不匹配问题。