复制堆栈变量时不寻常的析构函数行为

时间:2009-10-07 12:20:04

标签: c++ visual-c++ destructor rule-of-three

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

这是我的测试(在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?

编辑:将'delete'替换为'delete []'

7 个答案:

答案 0 :(得分:11)

Rule of Three

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

三种规则的规则,每次你定义其中一个:

  • 复制构造函数
  • 赋值运算符

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

编辑特殊例外
有时您只定义析构函数,因为您希望它是虚拟的,或者因为它记录了某些内容,而不是因为对属性进行了一些特殊处理;)

答案 1 :(得分:3)

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

分配完成后,其他实例将被删除。这解释了bcdx。

接下来使用

delete [] ptr; 

而不是删除

答案 2 :(得分:2)

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

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;
}

现在应该打印出来:

  

b c d d

每个临时表达在表达式结束时被销毁。然后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指针(拥有,则表示您的对象确定生命周期)。然后,您必须覆盖所有4个编译器生成的方法。

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

答案 3 :(得分:1)

您可以为拥有原始指针的任何类型创建复制构造函数和赋值运算符。

答案 4 :(得分:1)

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

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

答案 5 :(得分:1)

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

答案 6 :(得分:0)

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

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'并使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;
  }
};