当重新分配变量时,析构函数不会被调用。(C++)

11

当一个变量被重新赋值时,析构函数不会被调用:

Object foo = Object(a,b);
foo = Object(c,d);

因此,在 Object(c,d) 的作用域结束时,析构函数只会被调用一次,这显然可能会导致问题。 现在,在这种特殊情况下,它并不会给我带来太多麻烦:声明2个不同的对象就足够了:

Object foo1 = Object(a,b);
Object foo2 = Object(c,d);

这样,两个对象的析构函数将在最后被调用。

然而,在某些情况下,我必须重新分配变量,例如在对象构造函数中:

SuperObject(Point point1, Point point2) : delay_object_(DelayObject(0)) {
  double distance = distance(point1, point2);
  double delay = distance / speed;

  delay_object_ = DelayObject(delay);
}

实际上,DelayObject参数不容易计算(在这个示例中我还省略了几个其他的部分),我想避免在初始化列表中进行计算。

我认为可以通过将对象放入堆中并显式调用析构函数来强制删除:

SuperObject(Point point1, Point point2) : p_delay_object_(new DelayObject(0)) {
  double distance = distance(point1, point2);
  double delay = distance / speed;

  delete p_delay_object_;
  p_delay_object_ = new DelayObject(delay);
}

但是我觉得这样真的很丑,因为我只在严格必要时才喜欢使用动态分配。我有什么遗漏吗?

干杯!


你为什么不输入 Object foo(a,b) 呢?然后再将 foo 的值更改为 c 和 d?你为什么觉得正确的做法是创建并复制整个对象呢?我认为你没有考虑堆栈语义。 - Kate Gregory
谢谢Kate。没有什么特别的原因:我已经很久没有使用C++了。 - Enzo
4个回答

10

“析构函数只会在 Object(c,d) 所在作用域的结尾被调用”

不正确。Object(c,d) 是一个临时对象,它的析构函数会在创建它的完整表达式结尾处被调用。在这种情况下,就是在 foo = Object(c,d); 末尾的分号。foo 的析构函数会在作用域结尾处被调用。

Object 的赋值运算符应该释放或重用 foo 已持有的资源,并复制临时对象所持有的资源。不一定按照这个顺序(参见 copy-and-swap)。

编辑:响应评论。

Object foo = Object(a,b);

要么

  1. 使用与匹配(a,b)的任何二参数构造函数来构造临时对象。
  2. 使用复制构造函数,将临时对象作为参数传递给foo
  3. 销毁临时对象。

或者

  1. 使用与匹配(a,b)的任何二参数构造函数来构造foo

实现可以自由选择执行哪种方式 - 这是通过“复制构造函数省略”进行允许的。

foo = Object(c,d);
  1. 临时对象使用与 (c,d) 匹配的任何二元构造函数构造。
  2. 调用类 Object 的赋值运算符,将临时对象作为参数传递给 foo
  3. 销毁临时对象。

一段时间后,在作用域的末尾,foo 被销毁。

在 C++0x 中,如果该类存在移动赋值运算符,则会使用移动赋值运算符。


谢谢Steve,还有大家。我没想到会得到这么多专业的回答。你能否详细解释一下每个步骤具体发生了什么? - Enzo
我的猜测是:(1) Object foo = Object(a,b); // 创建了Object(a,b),创建了foo并将Object(a,b)复制到foo中。(2) foo = Object(c,d); // 创建了Object(c,d)并将其复制到foo中 (3) 按照顺序销毁Object(a,b),Object(c,d)和foo。 - Enzo
我理解得对吗,在 foo = Object(c,d) 这个过程中,析构函数从未被显式调用,而是由复制/移动赋值运算符负责清理被复制或移动对象的内存? - Dragonsheep
@Dragonsheep:不确定我是否理解了问题。被复制或移动的对象是 foo,它在作用域结束时被销毁。被复制或移动的对象是临时对象,即使移动操作符不会留下任何资源与该临时对象一起,它仍然会被销毁。因此,析构函数很可能没有任何工作要做,也许一旦所有内容都被内联,编译器将看到它不需要发出任何代码来进行名义上的销毁(例如,如果编译器可以跟踪指针被置空,则可以省略调用删除它的语句)。 - Steve Jessop
你说得没错,析构函数确实没有被显式地调用。这些销毁操作都不会在用户的代码中显式出现,而是由标准保证并由编译器实现的。 - Steve Jessop

8

您应该重载赋值运算符,这将在概念上复制构造现有对象并销毁旧的“this”。

class Object {
  ...
  Object& operator= (const Object& other) {
     if (this != &other) {
       // copy 'other' into 'this'.
     }
     return *this;
  }
  ...
};

如果这样做:foo = Object(c,d);,你所期望的将会实现。

(正如@Steve Jessop所提到的那样,临时对象Object(c,d)也将在作用域结束之前被析构。)

请参见什么是三大法则?


拒绝这个,当动态类型的对象不等于其静态类型时,它会让你崩溃。另外,你如何处理从Object派生的类中的operator=?使用创建临时和交换惯用语。 - Tomek
也许值得一提的是,每当你发现必须编写自己的析构函数并因此提供复制和赋值构造函数时,你应该三思而行。通常使用现代库工具有更好的方法来完全避免这种情况。正如Stephan T Lavavej所说,“不要编写自己的复制构造函数,那是邪恶的”... - Kerrek SB
@Tomek:我不明白你的观点。创建临时并交换和子类型化是两个不同的问题。您需要在派生类中再次覆盖operator=,无论是否使用该习惯用法。您无法通过赋值将堆栈上的“Object”更改为“Derived”。 - kennytm
谢谢大家!我试图遵循Google c++风格指南,该指南建议不要重载运算符,特别是复制构造函数。@Tomek,"使用创建临时对象并交换惯用语"是什么意思? - Enzo
1
@Enzo:最好设计具有智能数据成员的类,这样您就不必提供自定义复制构造函数。但是您必须知道如何设计您的类(而且可能并非总是可行)。 - Kerrek SB
显示剩余3条评论

1

是的,你忘了你可以重载赋值运算符。编译器提供的默认运算符会盲目地复制对象的所有字段,但你可以自己提供一个运算符,它将执行任何你想要的操作,包括处理分配对象的资源。

请注意,容易犯错的是赋值运算符(忘记了边角情况、异常安全等),我建议你阅读我们的运算符重载 FAQ 的至少这一部分,该部分转向常见的复制和交换惯用语的解释,这几乎总是实现赋值运算符的唯一明智方式。


编辑

请参考@Steve Jessop的答案,他考虑了另一个基本误解。

在所有情况下,您写入的内容都应该是:

SomeClass sc = SomeClass(parameters);

你正在初始化一个新对象:你正在创建一个临时的 (SomeClass(parameters)),然后通过拷贝构造函数初始化 sc 的副本;这既不必要也是低效的,C++ 中在堆栈上创建一个对象的语法只需:

SomeClass sc(parameters);

复制构造函数省略处理了即使是半好的编译器中的低效率,使得这两种形式变得等价。 我稍微喜欢带有=的版本,因为它永远不会失败于最令人烦恼的解析,但这只是风格问题。 当然,如果分析工具表明复制构造函数的省略已经失败,那么就必须解决这个问题。 - Steve Jessop
1
对我来说,它仍然看起来像Java/C#,感觉不对。我也不喜欢它,因为我从来没有记住过它是概念上的临时+复制,还是一些特殊情况下的“正常”构造,类似于=调用1个参数构造函数。 - Matteo Italia
C++ 先行一步,如果 Java/C# 看起来像它,那也不会让我烦恼;-) 但正如我所说,这是一个风格问题,只要类 复制构造函数,我们应该写任何我们喜欢的东西。 - Steve Jessop
谁沉迷于Steve Jessop的回答?;-) - Kerrek SB
哦,是的,我想到了一个问题,如果类名很长,那么我会冒险使用最棘手的解析,而不是写成std::vector<int> vec = std::vector<int>(10);。是的,当我试图从一对istream_iterator初始化向量时,我确实会遇到可怕的解析错误。 - Steve Jessop

0
将 DelayObject 参数的委托计算交给私有(可能为静态)方法,并在初始化列表中构造 DelayObject 时使用它们。

嗯,那可能就可以了!干杯! - Enzo

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