提问人:BinarySplit 提问时间:10/7/2009 最后编辑:BinarySplit 更新时间:10/8/2009 访问量:306
复制堆栈变量时出现异常的析构函数行为
Unusual destructor behaviour when copying over stack variables
问:
我编写了一个测试来检查在对堆栈变量进行覆盖赋值之前是否调用了析构函数,但我找不到任何合理的结果解释......
这是我的测试(在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的无益行为?
编辑:将“删除”替换为“删除[]”
答:
您可以创建一个复制构造函数和赋值运算符,就像对拥有原始指针的任何类型所做的那样。
s 只有在超出范围时才会被销毁 - 并且,正如您提到的,在程序过程中被覆盖,因此初始分配被泄露,最后一个被双重删除。
解决方案是重载赋值运算符(并且,正如 Pete 建议的那样,在它们齐头并进时提供一个复制构造函数),您将在其中清理您拥有的数组和副本。
您的应用程序行为是未定义的,因为如前所述,多个对象将共享对公共指针的访问,并尝试读取它...
三法则规定,每次定义以下一项时:
- Copy 构造函数
- 赋值运算符
- 破坏者
然后,您应该定义另一个,因为您的对象具有默认生成的方法不知道的特定行为。
编辑特殊异常:
有时你定义析构函数只是因为你希望它是虚拟的,或者因为它记录了一些东西,而不是因为你的属性有一些特殊的处理;)
评论
由于您在析构函数中打印,因此 a 实例将在作用域末尾(您看到的 x)被删除。
分配完成后,将立即删除其他实例。这解释了 BCDX。
接下来使用
delete [] ptr;
而不是删除
评论
问题是您需要复制构造函数和赋值运算符。由于将一个类分配给另一个类的行,因此会制作一个浅拷贝。这将导致两个类具有相同的 ptr 指针。如果其中一个被删除,则另一个指向已释放的内存顶部
您尚未定义分配或复制运算符。所以发生的事情是这样的:
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;
}
};
添加其他编译器定义的方法:
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。因此,必须定义所有四种方法。
评论