在 C++ 中分配时,我们分配的对象是否会被破坏?

When assigning in C++, does the object we assigned over get destructed?

提问人:Tony Park 提问时间:12/3/2010 最后编辑:Tony Park 更新时间:12/3/2010 访问量:557

问:

以下代码片段是否泄漏?如果不是,在 foobar() 中构造的两个对象在哪里被破坏?

class B
{
   int* mpI;

public:
   B() { mpI = new int; }
   ~B() { delete mpI; }
};

void foobar()
{
   B b;

   b = B();  // causes construction
   b = B();  // causes construction
}
C++ 三法则

评论


答:

3赞 Matthew Flaschen 12/3/2010 #1

你正在构建三个对象,所有对象都将被破坏。问题在于,默认的 copy-assignment 运算符将执行浅层复制。这意味着指针被复制,这会导致它被多次删除。这会导致未定义的行为。

这就是 3 法则背后的原因。你有一个析构函数,但没有其他两个析构函数。您需要实现复制构造函数和复制赋值运算符,这两者都应该执行深度复制。这意味着分配一个新的 int,复制该值。

B(const B& other) : mpI(new int(*other.mpI)) {
}

B& operator = (const B &other) {
    if (this != &other)
    {
        int *temp = new int(*other.mpI);
        delete mpI;
        mpI = temp;
    }
    return *this;
}

评论

0赞 The Maniac 12/3/2010
是的,一旦对象离开范围,它们就会被自动销毁。您(Tony)在构造函数和析构函数中所做的工作可确保在使用B类型的对象时不会泄漏内存。现在,如果您创建了指向 B 类型的对象的指针,则必须显式调用 B 的析构函数以避免泄漏。delete bPtr
7赞 Alex Budovski 12/3/2010 #2

默认的复制分配运算符执行成员复制。

因此,就您而言:

{
  B b;      // default construction.
  b = B();  // temporary is default-contructed, allocating again
            // copy-assignment copies b.mpI = temp.mpI
            // b's original pointer is lost, memory is leaked.
            // temporary is destroyed, calling dtor on temp, which also frees
            // b's pointer, since they both pointed to the same place.

  // b now has an invalid pointer.

  b = B();  // same process as above

  // at end of scope, b's dtor is called on a deleted pointer, chaos ensues.
}

有关详细信息,请参阅有效 C++ 第 2 版中的第 11 项。

评论

1赞 seand 12/3/2010
您需要编写一个正确的深度复制 operator=,或者阻止调用 operator=。(除非您明确需要,否则这是很好的做法)
1赞 Tony Park 12/3/2010
好的,谢谢。我想这就是为什么他们说要定义一个私人赋值运算符的原因,除非你需要赋值......或者,如果我希望它以任何有用的方式工作,我需要定义一个赋值运算符。
4赞 SingleNegationElimination 12/3/2010 #3

是的,这确实会泄漏。编译器会自动提供额外的方法,因为您尚未定义它。它生成的代码等效于:

B & B::operator=(const B & other)
{
    mpI = other.mpI;
    return *this;
}

这意味着会发生以下情况:

B b; // b.mpI = heap_object_1

B temp1; // temporary object, temp1.mpI = heap_object_2

b = temp1; // b.mpI = temp1.mpI = heap_object_2;  heap_object_1 is leaked;

~temp1(); // delete heap_object_2; b.mpI = temp1.mpI = invalid heap pointer!

B temp2; // temporary object, temp1.mpI = heap_object_3

b = temp1; // b.mpI = temp2.mpI = heap_object_3; 

~temp1(); // delete heap_object_3; b.mpI = temp2.mpI = invalid heap pointer!

~b(); // delete b.mpI; but b.mpI is invalid, UNDEFINED BEHAVIOR!

这显然是不好的。在您违反三法则的任何情况下,都可能发生这种情况。您已经定义了一个重要的析构函数和一个复制构造函数。但是,您尚未定义复制分配。三法则是,如果您定义了上述任何一项,则应始终定义所有三项。

相反,请执行以下操作:

class B
{
   int* mpI;

public:
   B() { mpI = new int; }
   B(const B & other){ mpI = new int; *mpi = *(other.mpI); }
   ~B() { delete mpI; }
   B & operator=(const B & other) { *mpI = *(other.mpI); return *this; }
};

void foobar()
{
   B b;

   b = B();  // causes construction
   b = B();  // causes construction
}
0赞 pythonic metaphor 12/3/2010 #4

正如多次指出的那样,你违反了三法则。只是为了补充链接,在堆栈溢出上对此进行了很好的讨论:什么是三法则?

0赞 Tony Park 12/3/2010 #5

只是为了提供一种不同的方法来解决我最初发布的代码的问题,我想我可以将指针保留在类 B 中,但去掉内存管理。然后我不需要自定义析构函数,所以我不违反 3 规则......

class B
{
   int* mpI;

public:
   B() {}
   B(int* p) { mpI = p; }
   ~B() {}
};

void foobar()
{
   int* pI = new int;
   int* pJ = new int;

   B b;        // causes construction

   b = B(pI);  // causes construction
   b = B(pJ);  // causes construction

   delete pI;
   delete pJ;
}

评论

1赞 Tony Delroy 12/3/2010
好主意,但这里有一个很大的区别,因为 B 对象过去(试图)是独立的、独立的对象。他们“拥有”堆上的 int,并且知道只要他们存在,它就存在。使用此答案中的方法,B 将依赖于传递给构造函数的指针所指示的数据的生存期。B 无法保证这些数据的生命周期,程序员必须更仔细地考虑它。例如,如果你在堆上创建一个新的 B 并从你的函数中返回它,那么你的 pI 和 pJ 已被删除,B 对象被垃圾了。
1赞 Tony Delroy 12/3/2010
(顺便说一句,第三种方法是用共享指针替换原始版本中的指针,这将在内部提供一个安全工具,以便编译器生成的 B 和析构函数版本将“正常工作”。operator=()operator=()