当重用分配给类的变量时,为什么只有最后一个析构函数调用才会导致崩溃?
我有一个例子,我有一个类在构造函数中分配内存,并在析构函数中释放它——非常基本的东西。如果我为类的新实例重用类实例变量,就会出现问题。当最终(并且只有最终)实例超出范围时被销毁时,它将在调用 free/delete 时崩溃并出现 SIGABRT:
malloc: *** error for object 0xXXXXX:pointer being freed was未分配
我觉得我错过了一些基本的东西,我想了解我的错误,这样我就可以在将来避免它。
这是一个简单的重现:
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
都没有关系——这都是相同的行为方式——并且仅在最终释放时。如果我移动范围或其他任何东西也没关系 - 一旦最终范围离开,崩溃就会发生。
如果将变量“t”限制在其自己的范围内并且不尝试重用它,则不会重现这种情况 - 例如,如果我这样做,一切都很好:
for (int i = 0; i < 100; i++) {
AllocTest t(c);
}
解决这个问题很简单,但我更愿意理解为什么我'我一开始就遇到这个问题。
解决方案:
感谢@user17732522 的回答,我现在明白了问题所在。我没有意识到,当我重新分配 t
时,它实际上是在复制该类 - 我正在假设,就像我通常使用的其他语言一样,分配会覆盖它。当我意识到这一切都是有道理的时,我无意中陷入了一个经典的“双重释放”问题。有关 复制初始化 的文档以及有关 三模式规则帮助填补了这里的其余空白。
只需修改我的类来定义隐式复制语义就足以让代码按预期工作:
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;
}
谢谢,伙计们!
I have case where I have a class that allocates memory in the constructor, and frees it in the destructor -- pretty basic stuff. The problem happens if I reuse the class instance variable for a new instance of the class. When the final (and only the final) instance gets destroyed when it goes out of scope, it will crash with a SIGABRT on the call to free/delete:
malloc: *** error for object 0xXXXXX: pointer being freed was not allocated
I feel like I'm missing something fundamental and I'd like to understand my mistake so I can avoid it in the future.
Here's a simple repro:
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;
}
I can see in the debugger that the destructor being called each time t
gets reassigned against the previous instance, why does only the final destructor cause a crash? It doesn't matter if I use new
/delete
or malloc
/free
-- it's the same behavior either way -- and only on the final deallocation. It also doesn't matter if I move the scopes around or anything -- as soon as the final scope leaves, the crash happens.
This does not reproduce if confining the variable 't' to its own scope and not trying to reuse it -- for instance, everything's fine if I do this:
for (int i = 0; i < 100; i++) {
AllocTest t(c);
}
Working around the issue is simple enough but I'd much rather understand why I'm having this problem to start with.
SOLUTION:
Thanks to the answer from @user17732522, I now understand what the problem is here. I didn't realize that when I was reassigning t
that it was actually making a copy of the class -- I was working on an assumption that like other languages that I typically work with that the assignment overwrites it. Upon realizing this things all made sense as I was unwittingly stumbling into a classic "double free" problem. The documentation on copy initialization and the pointers to the documentation about the rule of three pattern helped fill in the rest of the gaps here.
Simply modifying my class to define the implicit copy semantic was enough to allow the code to work as expected:
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;
}
Thanks, folks!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
首先,构造函数本身具有未定义的行为,因为您只为字符串 (
strlen(c)
) 的长度分配了足够的空间,而缺少了空终止符所需的附加元素。假设您在下面修复了该问题。
对象/实例上的析构函数仅被调用一次。
在您的代码中
t
始终是同一个实例。它永远不会被新的替换。您只是分配给它,它使用隐式复制分配运算符将临时对象中的成员一一复制分配给t
的成员。t
上的析构函数仅在其作用域在return
语句之后结束时调用一次。此析构函数调用具有未定义的行为,因为最后一个
AllocTest(c)
临时对象的析构函数已经删除了此时t.data
指向的已分配数组(它已已通过t = AllocTest(c);
分配该值)。此外,
AllocTest t(c);
中的第一个分配已泄漏,因为您用第一个t = AllocTest(c) 覆盖了
。t.data
指针;仅使用
AllocTest t(c);
您不会复制任何指针,因此不会发生这种情况。这里的根本问题是你的类违反了 0/3/5 规则:如果您有析构函数,您还应该使用正确的语义定义复制/移动构造函数和赋值运算符。如果您需要自定义析构函数,则隐式析构函数(您在此处使用的)可能会做错误的事情。
或者更好的是,通过不手动分配内存来使零规则发挥作用。使用
std::string
代替,您不必定义析构函数或任何特殊成员函数。这也自动解决了长度不匹配的问题。
First of all the constructor itself has undefined behavior, because you allocate only enough space for the length of the string (
strlen(c)
) which misses the additional element required for the null-terminator.Assuming in the following that you fix that.
The destructor on an object/instance is only ever called once.
In your code
t
is always the same instance. It is never replaced with a new one. You are only assigning to it, which uses the implicit copy assignment operator to copy-assign the members from the temporary object tot
's members one-by-one.The destructor on
t
is called only once, when its scope ends after thereturn
statement.This destructor call has undefined behavior because the destructor of the last
AllocTest(c)
temporary object has already deleted the allocated array to whicht.data
points at this point (it had been assigned that value witht = AllocTest(c);
).Additionally, the first allocation in
AllocTest t(c);
has been leaked, since you overwrote thet.data
pointer with the firstt = AllocTest(c);
.With just
AllocTest t(c);
you are not copying any pointer and so this can't happen.The underlying issue here is that your class violates the rule of 0/3/5: If you have a destructor you should also define copy/move constructor and assignment operators with the correct semantics. The implicit ones (which you are using here) are likely to do the wrong thing if you need a custom destructor.
Or even better, make the rule-of-zero work by not manually allocating memory. Use
std::string
instead and you don't have to define either the destructor or any special member function.This also automatically solves the length mismatch issue.