为什么使用“ new”会导致内存泄漏?


Answers:


464

怎么了

在编写时,T t;您正在创建T具有自动存储持续时间的类型的对象。超出范围时,它将自动清除。

在编写时,new T()您正在创建T具有动态存储持续时间的类型的对象。它不会自动清理。

没有清理的新

您需要传递一个指向它的指针以delete进行清理:

删除新

但是,您的第二个示例更糟:您要取消引用指针,并复制对象。这样,您将丢失指向由创建的对象的指针new,因此即使您愿意也永远无法删除它!

Deref更新

你应该怎么做

您应该更喜欢自动存储时间。需要一个新对象,只需编写:

A a; // a new object of type A
B b; // a new object of type B

如果确实需要动态存储期限,则将指向已分配对象的指针存储在自动存储期限对象中,该对象会自动删除它。

template <typename T>
class automatic_pointer {
public:
    automatic_pointer(T* pointer) : pointer(pointer) {}

    // destructor: gets called upon cleanup
    // in this case, we want to use delete
    ~automatic_pointer() { delete pointer; }

    // emulate pointers!
    // with this we can write *p
    T& operator*() const { return *pointer; }
    // and with this we can write p->f()
    T* operator->() const { return pointer; }

private:
    T* pointer;

    // for this example, I'll just forbid copies
    // a smarter class could deal with this some other way
    automatic_pointer(automatic_pointer const&);
    automatic_pointer& operator=(automatic_pointer const&);
};

automatic_pointer<A> a(new A()); // acts like a pointer, but deletes automatically
automatic_pointer<B> b(new B()); // acts like a pointer, but deletes automatically

使用auto_pointer进行更新

这是一个通用的成语,其名称不是非常描述性的RAII(资源获取是初始化)。当您获取需要清理的资源时,将其放置在一个具有自动存储期限的对象中,因此您不必担心清理它。这适用于任何资源,无论是内存,打开文件,网络连接还是您喜欢的任何资源。

这个automatic_pointer东西已经以各种形式存在,我只是提供了一个例子。标准库中存在一个非常相似的类,称为std::unique_ptr

还有一个旧的名称(C ++ 11之前的版本),auto_ptr但由于它具有奇怪的复制行为,因此现在已弃用。

然后有一些甚至更聪明的示例,例如std::shared_ptr,它允许多个指针指向同一个对象,并且仅在销毁最后一个指针时才将其清除。


4
@ user1131997:很高兴您提出了另一个问题。如您所见,要在评论中进行解释不是很容易:)
R. Martinho Fernandes 2012年

@ R.MartinhoFernandes:很好的答案。只是一个问题。为什么在operator *()函数中使用了按引用返回?
破坏者

@Destructor晚回复:D。通过引用返回可以修改指针,因此可以*p += 2像使用普通指针那样进行操作。如果未通过引用返回,则不会模仿普通指针的行为,这是这里的意图。
R. Martinho Fernandes

非常感谢您建议“将指向已分配对象的指针存储在自动删除该对象的自动存储期限对象中”。如果只有一种方法可以要求编码人员在编译任何C ++之前就学习这种模式!
安迪

35

逐步说明:

// creates a new object on the heap:
new B()
// dereferences the object
*(new B())
// calls the copy constructor of B on the object
B object2 = *(new B());

因此,到此为止,您在堆上有了一个没有指针的对象,因此无法删除它。

另一个示例:

A *object1 = new A();

仅当您忘记delete分配的内存时,内存泄漏:

delete object1;

在C ++中,有一些具有自动存储的对象,这些对象是在堆栈上创建的,这些对象会被自动丢弃,而有动态存储的对象是在堆上,您可以使用它们分配它们,new并释放它们delete。(这全都粗放了)

认为您应该delete为分配给的每个对象都有一个new

编辑

想一想,object2不一定是内存泄漏。

下面的代码只是为了说明这一点,这是一个坏主意,永远不要喜欢这样的代码:

class B
{
public:
    B() {};   //default constructor
    B(const B& other) //copy constructor, this will be called
                      //on the line B object2 = *(new B())
    {
        delete &other;
    }
}

在这种情况下,由于other是通过引用传递的,因此它将是所指向的确切对象new B()。因此,通过&other删除其指针来获取其地址将释放内存。

但我不能对此施加太大压力,不要这样做。只是在这里提出要点。


2
我的想法也一样:我们可以破解它以免泄漏,但是您不想这样做。object1也不必泄漏,因为其构造函数可以将自身附加到某种数据结构上,该结构会在某个时候将其删除。
CashCow 2012年

2
总是写这些“可以这样做但不这样做”的答案很诱人!:-)我知道这种感觉
Kos 2012年

11

给定两个“对象”:

obj a;
obj b;

它们不会在内存中占据相同的位置。换一种说法,&a != &b

将一个值赋给另一个不会更改其位置,但会更改其内容:

obj a;
obj b = a;
//a == b, but &a != &b

直观地,指针“对象”的工作方式相同:

obj *a;
obj *b = a;
//a == b, but &a != &b

现在,让我们看一下您的示例:

A *object1 = new A();

这是将的值分配new A()object1。该值是一个指针,表示object1 == new A(),但&object1 != &(new A())。(请注意,此示例不是有效的代码,仅用于说明)

因为保留了指针的值,所以我们可以释放它指向的内存:delete object1;根据我们的规则,此行为与delete (new A());没有泄漏的行为相同。


对于第二个示例,您正在复制指向的对象。该值是该对象的内容,而不是实际的指针。与其他情况一样,&object2 != &*(new A())

B object2 = *(new B());

我们丢失了指向已分配内存的指针,因此无法释放它。delete &object2;可能看起来会起作用,但是因为&object2 != &*(new A()),它并不等效delete (new A()),因此无效。


9

在C#和Java中,您可以使用new创建任何类的实例,然后您不必担心以后将其销毁。

C ++还有一个关键字“ new”可以创建对象,但是与Java或C#不同,它不是创建对象的唯一方法。

C ++具有两种创建对象的机制:

  • 自动
  • 动态

通过自动创建,您可以在范围内的环境中创建对象:-在函数中-在类(或结构)的成员中。

在函数中,您可以通过以下方式创建它:

int func()
{
   A a;
   B b( 1, 2 );
}

在一个类中,通常可以这样创建它:

class A
{
  B b;
public:
  A();
};    

A::A() :
 b( 1, 2 )
{
}

在第一种情况下,退出范围块会自动销毁对象。这可以是一个函数或一个函数中的作用域块。

在后一种情况下,对象b与其作为成员的实例A一起被销毁。

当您需要控制对象的生存期,然后需要删除它来销毁它时,将为对象分配新的对象。使用称为RAII的技术,您可以在创建对象时将其删除,方法是将其放入自动对象中,然后等待该自动对象的析构函数生效。

一个这样的对象是shared_ptr,它将仅在共享对象的shared_ptr的所有实例都被破坏时调用“删除程序”逻辑。

通常,虽然您的代码可能有许多对new的调用,但您对delete的调用应受到限制,并且应始终确保这些调用是从置于智能指针中的析构函数或“ deleter”对象进行的。

您的析构函数也不应抛出异常。

如果这样做,则几乎没有内存泄漏。


4
还有超过automaticdynamic。也有static
o鸭

9
B object2 = *(new B());

这条线是造成泄漏的原因。让我们将其分开。

object2是类型B的变量,存储在地址1中(是的,我在这里选择任意数字)。在右侧,您要求一个新的B或指向类型B的对象的指针。程序很乐意将其提供给您,并将您的新B分配给地址2,并在地址3中创建一个指针。现在,访问地址2中数据的唯一方法是通过地址3中的指针。接下来,使用*以获取该指针指向的数据(地址2中的数据)。这有效地创建了该数据的副本,并将其分配给在地址1中分配的object2。请记住,它是COPY,而不是原始副本。

现在,这是问题所在:

您从未真正将指针存储在可以使用它的任何地方!分配完成后,指针(您用来访问地址2的地址3中的内存)超出范围,超出了范围!您无法再对其调用call,因此无法清理address2中的内存。剩下的只是地址1中地址2中数据的副本。内存中有两个相同的东西。一个您可以访问,另一个您不能访问(因为您失去了访问它的路径)。这就是为什么这是内存泄漏。

我建议您从C#的背景知识中学到很多有关C ++指针如何工作的知识。它们是高级主题,可能需要一些时间来掌握,但是它们的使用对您来说是无价的。


8

如果让它变得更容易,则可以将计算机内存视为旅馆,并且程序是在需要时出租房间的客户。

这家酒店的工作方式是您预订房间并在离开时告诉搬运工。

如果您编程房间而没有通知搬运工就离开了,则搬运工会认为该房间仍在使用中,不会让其他人使用它。在这种情况下,会有房间漏水。

如果您的程序分配了内存但没有删除(它只是停止使用它),则计算机会认为该内存仍在使用中,并且将不允许其他任何人使用它。这是内存泄漏。

这不是确切的类比,但可能会有所帮助。


5
我很喜欢这种类比,虽然它不是完美的,但是绝对是向新手解释内存泄漏的好方法!
AdamM 2012年

1
我在接受彭博社伦敦高级工程师采访时用它来向HR女孩解释内存泄漏。我通过了这次采访,因为我能够以她理解的方式向非程序员解释内存泄漏(和线程问题)。
Stefan 2014年

7

在创建时,object2您正在创建使用new创建的对象的副本,但同时也丢失了(从未分配的)指针(因此以后无法删除它)。为了避免这种情况,您必须object2提供参考。


3
引用的地址删除对象是非常糟糕的做法。使用智能指针。
Tom Whittock

3
令人难以置信的坏习惯,是吗?您认为智能指针在幕后使用什么?
布林迪2012年

3
@Blindy智能指针(至少是体面实现的指针)直接使用指针。
Luchian Grigore 2012年

2
好吧,说实话,整个主意不是那么好,不是吗?实际上,我什至不确定在OP中尝试过的模式在哪里实际有用。
马里奥(Mario)2012年

7

好吧,如果您在某个时候不new通过将指向该内存的指针传递给操作符来释放使用操作员分配的内存,则会造成内存泄漏。delete

在上述两种情况下:

A *object1 = new A();

在这里,您不是delete用来释放内存的,因此,当object1指针超出范围时,就会发生内存泄漏,因为您将丢失指针,因此无法使用delete运算符。

和这里

B object2 = *(new B());

您将舍弃由返回的指针new B(),因此永远不能将该指针传递delete给来释放内存。因此,另一个内存泄漏。


7

正是这一行立即泄漏:

B object2 = *(new B());

在这里,您要B在堆上创建一个新对象,然后在堆栈上创建一个副本。在堆上已分配的那个将无法再访问,因此将导致泄漏。

该行不是立即泄漏的:

A *object1 = new A();

会有泄漏,如果你从来没有deleteð object1虽然。


4
在解释动态/自动存储时,请不要使用堆/堆栈。
Pubby 2012年

2
@Pubby为什么不使用?由于动态/自动存储始终是堆,而不是堆栈?这就是为什么不需要详细介绍堆栈/堆的原因,对吗?

4
@ user1131997堆/堆栈是实现细节。他们很重要,但与这个问题无关。
Pubby

2
嗯,我想要一个单独的答案,即和我的相同,但是用您认为最好的方式替换堆/堆栈。我很想找出您希望如何解释它。
mattjgalloway 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.