C++ - 赋值时析构函数被调用。

3

我还在学习C++的基础知识,可能没有掌握正确的术语来找到答案,但我无法在任何地方找到这个问题的提及。

如果我有一个带有构造函数和析构函数的类,在对该类进行赋值时为什么析构函数会被调用新数据上?

例如:

#include <iostream>

class TestClass {
    public:
    int* some_data;

    TestClass() {
        std::cout << "Creating" << std::endl;
        some_data = (int*)malloc(10*sizeof(int));
    }

    ~TestClass() {
        std::cout << "Deconstructing" << std::endl;
        free(some_data);
    }

    TestClass(const TestClass& t) : some_data{t.some_data} {
        std::cout << "Copy" << std::endl;
    }
};

int main() {
    TestClass foo;
    std::cout << "Created once" << std::endl;
    foo = TestClass();
    std::cout << "Created twice" << std::endl;
}

它将打印:

Creating
Created once
Creating
Deconstructing
Created twice
Deconstructing
free(): double free detected in tcache 2
Aborted (core dumped)

所以在调试器中跟踪后,看起来析构函数在新创建的数据上被调用,这让我感到困惑。原始数据不应该先被释放,然后在执行结束时再释放新数据吗?看起来像这样原始数据永远没有被释放。


析构函数 - 463035818_is_not_a_number
5
另一个非常重要的词汇是:3/5法则。rule of 3/5 - 463035818_is_not_a_number
你的复制构造函数有问题,最终会得到两个指向相同 some_data 的对象。此外,你还缺少一个赋值运算符重载(这就是 foo = TestClass(); 调用的内容)。 - UnholySheep
2个回答

1
您的对象拥有一个指向已分配内存的原始指针,但并未实现适当的复制构造函数来进行分配并复制指针后面的数据。因此,当您复制一个对象时,指针被复制,这样现在两个对象指向同一个地址(并且刚刚赋值给对象的旧对象泄漏了)。
当临时变量超出范围时,它会删除其指针,但复制品(foo)仍然指向它。当foo超出范围时,它再次删除相同的指针,导致您看到的双重释放错误。
如果您需要编写析构函数进行清理,几乎总是需要提供复制和赋值操作,或者禁用它们。
建议:
  • 使用 std::unique_ptr 持有指针,在尝试复制时将无法编译。这样可以强制你处理此问题。此外,malloc 和 free 主要用于 C 或低级别的 C++ 内存管理。考虑使用 new 和 delete 进行分配。(unique_ptr 默认使用 delete,而不是 free,不要混用)
  • 或者,删除复制构造函数和赋值运算符。
  • 同时,当想要从 xvalue(临时或移动 lvalue)移动时,请考虑从右侧窃取分配。 因此,这个类是移动构造函数和移动赋值的好选择。

不仅缺少赋值操作符,op确实有一个复制构造函数,但它没有做正确的事情。 - 463035818_is_not_a_number
我从未检查过复制构造函数,因为它从未被调用,输出也从未打印。但是赋值运算符问题似乎是正确的。 - Matthew
移动赋值正是我所需要的。 - Matthew

0

大多数注释以及代码中的一些细节:

#include <iostream>
#include <array>
#include <memory>

class TestClass 
{
// members of a class should not be public
private:

    // TestClass owns the data, this is best modeled 
    // with a unique_ptr. std::array is a nicer way of
    // working with arrays as objects (with size!)
    std::unique_ptr<std::array<int, 10>> some_data;

public:
    TestClass() :
        some_data{ std::make_unique<std::array<int,10>>() }
    {
        std::cout << "Creating" << std::endl;

        // set everything in the array to 0
        std::fill(some_data->begin(), some_data->end(), 0);
    }

    ~TestClass() 
    {
        std::cout << "Destructing" << std::endl;
        // no need to manually delete a std::unique_ptr
        // its destructor will free the memory
        // and that will be called as part of this destructor
    }

    TestClass(const TestClass& t) : 
        // when you copy a class the copy should have its
        // own copy of the data (to avoid deleting some data twice)
        // or you must chose shared ownership (lookup std::shared_ptr)
        some_data{ std::make_unique<std::array<int,10>>() }
    {
        std::cout << "Copy" << std::endl;

        // copy data from t to this instances array
        // (note this would not have been necessary
        // it your array was just a non-pointer member,
        // try that for yourself too.)
        std::copy(some_data->begin(), some_data->end(), t.some_data->begin());
    }

    TestClass(TestClass&& rhs) :
        some_data{ std::move(rhs.some_data) } // transfer ownership of the unique_pointer to this copy
    {
        std::cout << "Move" << std::endl;
    }

    // Important this assignement operator is used in your original code too
    // but you couldn't see it!
    TestClass& operator=(const TestClass& t)
    {
        some_data = std::make_unique<std::array<int, 10>>();
        std::copy(some_data->begin(), some_data->end(), t.some_data->begin());

        std::cout << "Assignment" << std::endl;
        return *this;
    }

    

};

int main() 
{
    TestClass foo;
    std::cout << "Created once" << std::endl;

    foo = TestClass();
    std::cout << "Created twice" << std::endl;

    TestClass bar{ std::move(foo) };
    std::cout << "Moved" << std::endl;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接