'When reusing a variable assigned to a class, why is only the last destructor call causing a crash?

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!



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source