复制堆栈变量时出现异常的析构函数行为

Unusual destructor behaviour when copying over stack variables

提问人:BinarySplit 提问时间:10/7/2009 最后编辑:BinarySplit 更新时间:10/8/2009 访问量:306

问:

我编写了一个测试来检查在对堆栈变量进行覆盖赋值之前是否调用了析构函数,但我找不到任何合理的结果解释......

这是我的测试(在Visual C++ 2008发布模式下):

#include <iostream>
class C {
public:
 char* ptr;
 C(char p) { ptr = new char[100]; ptr[0] = p;}
 ~C() { std::cout << ptr[0] << ' '; delete [] ptr; }
};

int _tmain(int argc, _TCHAR* argv[])
{
 {
  C s('a');
  s = C('b');
  s = C('c');
  s = C('d');
 }
 std::cin.get();
 return 0;
}

如果我的假设为真,我期望得到“a b c d”,如果假设为真,我期望得到“d”。 取而代之的是,我得到“b c d x”。“x”根据分配给 ptr 的内存量而变化,表明它正在读取随机堆值。

我相信正在发生的事情(如果我错了,请纠正我)是每个构造函数调用都会创建一个新的堆栈值(让我们称它们为 s1、s2、s3、s4),然后赋值让 s1.ptr 被 s4.ptr 覆盖。然后,S4 在复制后立即销毁,但 S1(带有悬空的 PTR)在离开示波器时被销毁,导致 S4.ptr 被双重删除,而原始 S1.PTR 没有删除。

有没有办法解决这种不涉及使用shared_ptrs的无益行为?

编辑:将“删除”替换为“删除[]”

C Visual-C++ 析构函数 三法则

评论

3赞 Matthieu M. 10/7/2009
有一个错误,你使用 new[] 但删除,你应该使用 delete[]。
0赞 Calyth 10/7/2009
仅供参考,如果您在代码周围设置断点,并查看反汇编,它将具有注释程序集,可以指示是否调用析构函数。有时非常方便。

答:

1赞 Pete Kirkham 10/7/2009 #1

您可以创建一个复制构造函数和赋值运算符,就像对拥有原始指针的任何类型所做的那样。

1赞 Raphaël Saint-Pierre 10/7/2009 #2

s 只有在超出范围时才会被销毁 - 并且,正如您提到的,在程序过程中被覆盖,因此初始分配被泄露,最后一个被双重删除。

解决方案是重载赋值运算符(并且,正如 Pete 建议的那样,在它们齐头并进时提供一个复制构造函数),您将在其中清理您拥有的数组和副本。

11赞 Matthieu M. 10/7/2009 #3

三法则

您的应用程序行为是未定义的,因为如前所述,多个对象将共享对公共指针的访问,并尝试读取它...

三法则规定,每次定义以下一项时:

  • Copy 构造函数
  • 赋值运算符
  • 破坏者

然后,您应该定义另一个,因为您的对象具有默认生成的方法不知道的特定行为。

编辑特殊异常
有时你定义析构函数只是因为你希望它是虚拟的,或者因为它记录了一些东西,而不是因为你的属性有一些特殊的处理;)

评论

0赞 sbi 10/7/2009
免责声明:这是经验法则,因此您可能会偏离它。然而,我很少觉得有必要。+1
1赞 10/7/2009
好点子。澄清一下,在 “s = C('d');” 和类似的行上,复制构造函数构造了一个临时的,然后使用默认赋值运算符复制它。assign 复制 ptr 指针,而不是其指向的数组,并覆盖上一个 ptr 而不删除它,从而导致内存泄漏。然后,临时指针将被丢弃,因此析构函数使指针无效,从而在副本中留下一个悬空的指针。
2赞 mmmmmmmm 10/8/2009
确切地说:在多个对象中共享指向公共内存片段的指针是没有问题的,只要您管理该公共部分的生命周期、所有权等以及正确指向它的指针(这在 OP 的示例中并非如此!
3赞 decasteljau 10/7/2009 #4

由于您在析构函数中打印,因此 a 实例将在作用域末尾(您看到的 x)被删除。

分配完成后,将立即删除其他实例。这解释了 BCDX。

接下来使用

delete [] ptr; 

而不是删除

评论

0赞 BinarySplit 10/7/2009
实际上,cout 将其读取为单个字符并忽略其余字符。谢谢你提到删除[]。我一辈子都在写错那个 D:
0赞 10/7/2009
尽管 BinarySplits 半有效点,但还是投了赞成票 - 即在这种情况下,不匹配的删除不会导致问题。无论您使用多少个项目,都需要 delete[],因为 new[] 会构建它们,因此它们都需要销毁,但在这种情况下,char 是 POD,因此不需要销毁。无论哪种方式,不匹配的 new/delete 都是一个非常糟糕的习惯 - 原则上,IIRC、new 和 new[] 甚至可以使用不同的内存池,无论析构函数问题如何,都会导致不匹配的删除损坏。
1赞 Toad 10/7/2009 #5

问题是您需要复制构造函数和赋值运算符。由于将一个类分配给另一个类的行,因此会制作一个浅拷贝。这将导致两个类具有相同的 ptr 指针。如果其中一个被删除,则另一个指向已释放的内存顶部

0赞 Chris Becke 10/7/2009 #6

您尚未定义分配或复制运算符。所以发生的事情是这样的:

C s('a');

创建“s”实例并使用“a”进行初始化。

s = C('b');

这将创建一个临时对象,用“b”初始化它,然后默认赋值运算符启动,它执行所有变量的按位复制,覆盖 s 的 ptr。临时对象被销毁。发出“b”并删除“ptr”(使 s 中的 ptr 无效)。

s = C('c');
s = C('d');

又一样。创建临时对象,用 'c' 初始化,s 中的 'ptr' 被临时对象中分配的 ptr 覆盖。临时对象被销毁,发出“c”,并使 s 中的 ptr 无效。对 d 重复上述步骤。

  return 0;
}

最后 s 离开范围,它的析构函数尝试发出 ptr 的第一个字符,但这是垃圾,因为 ptr 是由最后一个 ('d') 临时对象分配和删除的。删除 ptr 的尝试应该会失败,因为该内存已被删除。

为了解决这个问题?定义显式复制构造函数和赋值运算符。

class C {
  // other stuff
  C(const C&rhs); // copy constructor
  C& operator=(const c& rhs){ // assignment operator
    a[0] = rhs.a[0];
    return *this;
  }
};
3赞 Martin York 10/7/2009 #7

添加其他编译器定义的方法:

class C
{
    public:
      char* ptr;
      C(char p)                { ptr = new char[100]; ptr[0] = p;}
     ~C()                      { std::cout << ptr[0] << ' '; delete [] ptr; }
      C(C const& c)            { ptr = new char[100]; ptr[0] = c.ptr[0];}
      C& operator=(C const& c) { ptr[0] = c.ptr[0]; return *this;}
};

int _tmain(int argc, _TCHAR* argv[])
{
  {
      C s('a');
      s = C('b');
      s = C('c');
      s = C('d');
  }
  std::cin.get();
  return 0;
}

它现在应该打印出来:

乙、丙、丁、

每个临时值在表达式的末尾都会被销毁。然后 s 最后被销毁(在将 'd' 复制到 ptr[0] 之后)。如果在每种方法中都粘贴一个 print 语句,则更容易看到正在发生的事情:

>>           C s('a');
Construct 'a'

>>           s = C('b');
Construct 'b'  
Assign 'b' onto 'a'  
Destroy 'b'         (Temporary destroyed at ';')  

>>          s = C('c');
Construct 'c'  
Assign 'c' onto 'b' (was 'a' but has already been assigned over)  
Destroy 'c'         (Temporary destroyed at ';')

>>          s = C('d');  
Construct 'd'  
Assign 'd' onto 'c'  
Destroy 'd'         (Temporary destroyed at ';')  

>> End of scope.
Destroy 'd'         (Object s destroyed at '}')  

由于编译器定义了 4 种方法,因此适用“四法则”。
如果类包含类拥有的 RAW 指针(owned 表示对象决定生存期)。然后,必须重写所有 4 个编译器生成的方法。

由于您创建并销毁了成员“ptr”,因此这是一个拥有的 ptr。因此,必须定义所有四种方法。