重用分配给 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 代替,您不必定义析构函数或任何特殊的成员函数。 这也自动解决了长度不匹配问题。