【问题标题】:Why is object copy constructed and destructed twice?为什么对象副本构造和销毁两次?
【发布时间】:2026-02-13 04:50:02
【问题描述】:

为什么在下面的代码中会出现一对“额外的”复制构造函数和析构函数?

当叮咚的构造函数将 STL 容器作为参数时会发生这种情况(我尝试过 std::vector 和 std::list)。它可能发生在其他事情上吗?如果构造函数取而代之的是指针,则不会发生。如果我在堆上分配 ding 也不会发生( Dingledong* ding = new Dingledong(v) )。

#include <list>
#include <iostream>

class Dingledong{
public:
    Dingledong(std::list<int> numbers)
    {
        std::cout << "construction\n";
        numbers_ = numbers;
    }
    Dingledong(Dingledong const& other)
    {
        std::cout << "COPY construction\n";
    }
    ~Dingledong()
    {
        std::cout << "destructed\n";
        // I would do some cleanup here.. unsubscribe from events, whatever..
        // but the destructor is called sooner than I would expect and thus
        // the cleanup is done prematurely.
    }
    std::list<int> numbers_;
};

void diller()
{
    std::cout << "diller started.. " << std::endl;
    std::list<int> v = std::list<int>(34);
    // Having an STL container as parameter in constructor causes Dingledong's copy constructor to 
    // be used to create a temporary Dingledong which is immediately destructed again. Why?
    Dingledong ding = Dingledong(v);
    std::cout << "Has ding been destructed?\n";
}

int main()
{
    diller();
    system("pause");
}

输出:

diller started...
construction
COPY construction     // I didn't expect this to happen
destructed            // I didn't expect this to happen
Has ding been destructed?
destructed

提前谢谢你!

【问题讨论】:

  • 你用的是什么编译器?我本来希望该副本被省略。另外,是什么让您认为它与“STL”容器有关?
  • 我的编译器是 Visual Studio 2013 附带的。STL 容器只是我迄今为止发现的唯一模式。我希望它会发生在其他事情上。
  • 您是否在关闭优化的情况下进行编译?如果副本没有在 Release 模式下被删除,我会感到非常惊讶。
  • 好吧,在做出此类声明并添加各种可能不相关的信息和标签之前,您应该检查一下。
  • @LokiAstari 我可以确认 VS2013 中的发布版本仍然具有相同的行为。

标签: c++ destructor copy-constructor copy-elision


【解决方案1】:

这段代码:

Dingledong ding = Dingledong(v);

意思是:

  1. 创建Dingledong类型的临时对象,初始化为v
  2. 创建对象ding,用临时对象初始化。

复制构造函数出现在此处的第 2 步。如果您不想要副本,请不要编写指定副本的代码。例如:

Dingledong ding(v);   // no copy

编译器可以实现一个称为 copy elision 的功能,其中优化了这个临时对象(即使复制构造函数有副作用),但在这种情况下他们不必这样做没有理由依赖它。


您可以通过添加移动构造函数来改进代码,在这种情况下(如果编译器不执行复制省略)操作将是移动而不是复制,这样成本会更低。

在你的构造函数中也有一个浪费的副本(numbers 是从 v 复制的,然后 numbers_ 是从 numbers 复制的,还有 numbers_ 被初始化然后分配,而不是仅仅被初始化)。这将是一个更好的构造函数:

Dingledong(std::list<int> numbers): numbers_( std::move(numbers) )
{
    std::cout << "construction\n";
}

【讨论】:

  • 谢谢!这就解释了。我从来没有意识到这两者是不等价的。
【解决方案2】:

你的输出:

diller started...
construction                //(1)
COPY construction           //(2)
destructed                  //(3)
Has ding been destructed?
destructed                  //(4)

以及相关的代码:

Dingledong ding = Dingledong(v);

叮咚(v) 创建一个临时对象。 (1) 被打印出来。

您将此临时对象分配给 ding(实际上,这是调用复制构造函数的语法糖)。 (2) 被打印出来。

一旦语句完成,临时对象就会被销毁。 (3) 印刷。 你的代码运行,一旦 ding 超出范围,它就会被销毁,(4) 被打印出来。

【讨论】: