0

可能的重复:
什么是三法则?

人们说如果你需要一个析构函数,那么你实际上需要一个重载的operator=

struct longlife{ };
class z
{
 z(){};
 ~z(){ for( auto it=hold.begin();it!=hold.end() ++it ) delete(*it); };
 vector<longlife*> hold;
};

假设所有插入的指针hold都是new堆分配的,为什么除了这个例子需要解构函数之外还有什么?

anything else我的意思是,

z& operator=( const z&ref )
{
 hold = ref.hold;
 return *this;
}

将:

z a;
a.hold.push_back( heap_item );
z a2;
a2 = a;

导致内存泄漏?有时很难理解为什么三规则是规则

4

5 回答 5

3

不仅需要赋值运算符,还需要实现复制构造函数。否则,编译器将提供默认实现,这将导致两个副本(在赋值/复制构造之后)包含指向相同longlife实例的指针。然后,两个副本的析构函数将delete导致这些实例导致未定义的行为。

z a;
a.hold.push_back( heap_item );
z a2;
a2 = a;

两者都a.hold[0]包含a2.hold[0]一个指向相同的指针heap_item;从而在销毁过程中造成双重删除。

避免必须实现赋值运算符和复制构造函数的简单方法是使用智能指针将longlife实例保存在vector.

std::vector<std::unique_ptr<longlife>> hold;

现在甚至不需要为你的类编写析构函数。


对于 C++03,您的选择是使用std::tr1::shared_ptr(or boost::shared_ptr) 代替unique_ptr或使用boost::ptr_vector(当然,这也是 C++11 的选项)代替std::vector.

于 2012-10-10T20:53:42.303 回答
2

因为如果没有赋值运算符和复制构造函数,您最终可能会得到多个hold指向同一个堆项的向量,从而在销毁时导致未定义的行为:

z firstZ;
if (somethingIsTrue) {
    z otherZ = firstZ;
    // play with otherZ...
    // now otherZ gets destructed, along with longlife's of the firstZ
}
// now it's time to destroy the firstZ, but its longlife's are long gone!

当然,如果您使用对象向量或“智能指针”向量,而不是“普通旧”指针向量,您当然不会遇到这个问题。

有关更多信息,请参阅三法则。

于 2012-10-10T20:52:08.250 回答
1

实际上这里会有双重释放,而不是内存泄漏。

STL 容器存储对象,而不是引用。在你的情况下object是一个指针。指针被简单地复制。您的行将a2 = a;在向量中复制指针。之后,每个析构函数都会释放指针。

双重释放比内存泄漏危险得多。它会导致令人讨厌的未定义行为:

MyStruct *p1 = new MyStruct();
delete p1;
.... do something, wait, etc.
delete p1;

同时在另一个线程上:

MyOptherStruct *p2 = new MyOtherStruct();
.... do something, wait, etc.
p2->function();

结果可能是内存分配器将分配给用于p2的完全相同的值p1,因为它在第一次调用 后是空闲的delete p1。稍后第二个delete p1也会正常,因为分配器认为这是一个为p2. 问题只会出现在p2->function();. 查看线程 2 的代码,绝对不可能理解出错的原因和原因。这非常难以调试,尤其是在系统很大的情况下。

于 2012-10-10T20:51:29.137 回答
1

这将导致aor的析构函数a2(以第二个被销毁的为准)上的双重删除(和崩溃),因为默认赋值构造函数将对hold. 所以每个对象a最终a2都会删除完全相同的内存。

于 2012-10-10T20:52:54.900 回答
1

从您的评论中:

@Xeo,我了解三规则是什么,问题主要是为什么它是规则

考虑一下这里发生了什么:

z& operator=( const z&ref )
{
 hold = ref.hold;
 return *this;
}

假设您有一个实例z

z myz;
myz.a.hold.push_back( new long_life );

...然后您创建一个副本myz

z my_other_z;
// ...
my_other_z = myz;

您在上面提供的operator=实现只是复制vector. 如果vector有指针,它不会复制所指向的任何内容——它只是复制指针本身的文字副本。

因此,在返回后,您将有 2 个具有指向同一事物的指针的operator=实例。z当这些zs 中的第一个被破坏时,它将删除指针:

~z(){ for( auto it=hold.begin();it!=hold.end() ++it ) delete(*it); };

当第二个z被销毁时,它会再次尝试delete指向同一个指针。这会导致未定义的行为。

此问题的解决方案是在分配或复制维护需要分配和删除的资源的对象时进行深拷贝。这意味着提供一个赋值运算符和一个复制构造函数。

这就是为什么三规则是规则的原因。

编辑:

正如其他人所提到的,通过使用值语义和 RAII 可以更好地避免这一切。正如其他人所说,重新设计您的对象以使用零规则是一种更好的方法。

于 2012-10-10T21:03:43.653 回答