C++ 析构函数调用了错误的对象?

C++ Destructor called for the wrong object?

我是 C++ 的新手,我编写了一个小程序来了解赋值如何处理对象。此页面 (http://www.cplusplus.com/doc/tutorial/classes2/) 的 cpp 文档提示我执行此操作。在此页面上,它指出:

The implicit version [of the copy assignment operator] performs a shallow copy which is suitable for many classes, but not for classes with pointers to objects they handle its storage. In this case, not only the class incurs the risk of deleting the pointed object twice, but the assignment creates memory leaks by not deleting the object pointed by the object before the assignment.

我以粗体显示的最后一部分是我决定进行测试的原因。我认为这个问题可以通过在析构函数(这是标准的?)中处理指向对象的删除来解决,而不是必须重载复制赋值运算符。如果不调用析构函数,那不是很不方便吗?假设我有多个引用对象,我必须将所有删除都放在析构函数(对于大多数重新分配的情况)和赋值重载中。

在这次测试中,我遇到了一个完全不同的问题。我最初的想法是创建一个简单的 class,它存储一个 int(作为用于测试目的的标识符)并重载构造函数和析构函数以查看何时以及是否调用了析构函数。

这是我的代码:

class Test{
public:
    int id;
    explicit Test(int id) : id(id) {
        cout << "Created " << id << endl;
    }
    ~Test() {
        cout << "Destroyed " << id << endl;
    }
};

int main() {
    Test x = Test(1);
    x = Test(2);

    cout << x.id << endl;
    return 0;
}

我预期的输出是:

1: Created 1
2:Destroyed 1? (这是我不确定的一个,因为网站暗示如果对象 'replaced' 与另一个对象而不是超出范围,则不会调用此析构函数)
3: Created 2 对象 2 'replaces' 对象 1 分配给 x
4:2对象2的id打印出来的值
5: Destroyed 2 对象 2 在超出范围时被销毁

相反,我得到了以下输出:

Created 1
Created 2
Destroyed 2
2
Destroyed 2

这对我来说真的没有意义。

使用调试器,Created 2Destroyed 2 都在调用行 x = Test(2); 时显示。如果我们刚刚将 x 分配给对象 2,为什么它的析构函数会立即被调用?接下来是下一部分。

其次,由于调用了对象 2 的析构函数,我们可以假设它已被销毁。 2 的下一个输出似乎与此相矛盾,因为它表明 x 仍然持有对象 2(预期,但与其析构函数的调用相矛盾)。

我不太清楚为什么会这样。

最后输出Destroyed 2。如果我们没有更早地看到这一点,这将是有道理的。对象 2 存储在 x 中,因此当它超出范围时,将调用析构函数。

由于某种原因,我们的析构函数被调用了两次,而对象 1,也就是对象 2 赋值给 x 的 'overridden' 从未调用过它的析构函数,而是对象的析构函数我们刚刚创建的对象调用了它的析构函数。

所以...这总结为一个由两部分组成的问题:

1:为什么会出现这种奇怪的行为,是否有任何合乎逻辑的原因?
2: 'overwriting' 一个对象(例如对象 1)与另一个对象(对象 2)通过赋值导致其析构函数(在本例中为对象 1 的析构函数)是否被调用?

提前致谢。

Using the debugger, Created 2 and Destroyed 2 both display when the line x = Test(2); is called. If we just assigned x to Object 2, why is its destructor being called immediately? This follows on to the next part.

x = Test(2);首先创建了一个Test,构造函数参数为2。这就是产生 Created 2 的原因。这个无名 Test 然后被分配给 x ,它给 x.id 值 2。那个无名 Test 然后在表达式的末尾被销毁,产生 "Destroyed 2" .

Secondly, since the destructor for Object 2 has been called, we can assume it has been destroyed. The next output of 2 seems to contradict this, as it suggests that x is still holding Object 2 (expected, but contradicted by the call of its destructor).

如本回答第一部分所述,被破坏的不是 x,而是临时 Tempx.id 仍然有效并将产生新值 2.

Finally, Destroyed 2 is outputted. This would make sense if we didn't see this earlier. Object 2 is stored in x, so when it goes out of scope the destructor is called.

x 在函数结束时被销毁时会发生这种情况。它的 id 值被之前的赋值更改为 2,因此它产生 "Destroyed 2".

1: Why is this weird behaviour occurring, and is there any logical reason to why it is?

这可能不是您预期的行为,但并不奇怪。我希望这个答案能帮助您理解它发生的原因。

2: Does 'overriding' an object (e.g. Object 1) with another object (Object 2) by assignment lead to its destructor (in this case the destructor of Object 1) to be called or not?

分配给一个对象并不会破坏它。它用一个新的值替换它的值,从这个意义上说 "destroys" value 它以前有帮助,但实际的对象实例没有被破坏并且不涉及析构函数。

编辑:看来您可能担心资源泄漏。由于 Test 没有管理任何资源,因此不会有泄漏,编译器生成的成员将表现良好。如果您的 class 确实 管理资源(通常以动态分配内存的形式),那么您将需要应用 rule of 3/5/0。值得注意的是,您将需要自己实现赋值运算符,以便它清理所有以前持有的资源。仅仅实现析构函数是不够的,因为它不参与赋值。

Test x = Test(1);

这将创建一个值为“1”的新对象。

x = Test(2);

这首先创建一个值为“2”的新对象,然后将其赋值 到第一个带有赋值运算符的对象,它是为您的 class 隐式创建的!此时此刻,你有两个对象,它们的值都是 2!

要获得更好的想法,您可以这样做:

class Test{
    public:
        static int instanceCount;
        int id;
        int count;

        explicit Test(int id) : id{id}, count{instanceCount++} {
            std::cout << "Created " << id << " " << count << std::endl;
        }

        ~Test() {
            std::cout << "Destroyed " << id << " " << count << std::endl;
        }

        //Test& operator=(const Test&) = delete;
        Test& operator=(const Test& ex) 
        {
            id=ex.id;
            return *this;
        }
};  


int Test::instanceCount = 0;

int main() {
    Test x = Test{1};
    x = Test{2};

    std::cout << x.id << std::endl; 
    return 0;
}  

现在您可以看到何时创建了新实例。如果删除 class 的赋值运算符,您将看到您编写的第一条指令 "Test x = Test{1};" 不是赋值而是构造。第二个 "x = Test{2};" 将失败,因为您现在已经删除了运算符。

输出结果如下:

Created 1 0
Created 2 1
Destroyed 2 1
2
Destroyed 2 0

如您所见,您首先获得一个计数为 0 且值为 1 的实例。然后创建第二个临时实例,计数为 1,您的值为 2。 然后这个将被分配给第一个,临时实例将在你的 std::cout 发生之前被删除!在您离开主函数作用域的那一刻,第一个实例将被删除!

你能学到什么:

  • X x=X(3); 创建一个对象与写 X x(3);
  • 是一样的
  • 如果您还没有手动编写赋值运算符,您可能会得到一个默认值,具体取决于更多规则(此处广泛)。
  • 你应该看到你在这里创建了临时对象,它将 可以创建和删除 "on the fly" 但在大多数情况下完全可以避免成本!
  • 你应该使用using namespace std!
  • 你应该写 X x{3} instead ofX x(3)`
  • X x=X(3); 完全令人困惑,因为看起来您构建了一个临时的,而不是将其分配给默认构建的。但这不会发生,因此您应该编写更简单的代码!