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

Why does the use of 'new' cause memory leaks?

提问人: 提问时间:1/13/2012 最后编辑:Xeo 更新时间:7/20/2012 访问量:30008

问:

我首先学习了 C#,现在我开始学习 C++。据我了解,C++ 中的运算符与 C# 中的运算符不同。new

您能解释一下此示例代码中内存泄漏的原因吗?

class A { ... };
struct B { ... };

A *object1 = new A();
B object2 = *(new B());
C 指针 内存泄漏 新运算符 C++-FAQ

评论

0赞 Brent Bradburn 8/2/2012
近乎重复:标准 C++ 中的垃圾回收是自动的吗?

答:

5赞 Mario 1/13/2012 #1

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

评论

3赞 Tom Whittock 1/13/2012
使用引用的地址来删除对象是非常糟糕的做法。使用智能指针。
3赞 Blindy 1/13/2012
难以置信的糟糕做法,嗯?你认为智能指针在幕后使用什么?
3赞 Luchian Grigore 1/13/2012
@Blindy智能指针(至少是体面实现的指针)直接使用指针。
2赞 Mario 1/13/2012
好吧,老实说,整个想法并不是那么好,不是吗?实际上,我什至不确定在 OP 中尝试的模式实际上在哪里有用。
5赞 mattjgalloway 1/13/2012 #2

正是这条线立即泄漏:

B object2 = *(new B());

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

这条线路不会立即泄漏:

A *object1 = new A();

如果你从不这样做,就会有泄漏。deleteobject1

评论

4赞 Pubby 1/13/2012
在解释动态/自动存储时,请不要使用堆/堆栈。
2赞 1/13/2012
@Pubby为什么不使用?因为动态/自动存储总是堆的,而不是堆叠的?这就是为什么没有必要详细说明堆栈/堆,对吗?
4赞 Pubby 1/13/2012
@user1131997堆/堆栈是实现细节。了解它们很重要,但与这个问题无关。
2赞 mattjgalloway 1/13/2012
嗯,我想要一个单独的答案,即与我的相同,但用您认为最好的替换堆/堆栈。我很想知道你希望如何解释它。
6赞 razlebe 1/13/2012 #3

好吧,如果你在某个时候没有通过将指向该内存的指针传递给运算符来释放使用该内存分配的内存,则会造成内存泄漏。newdelete

在上述两种情况下:

A *object1 = new A();

在这里,你不用于释放内存,所以当你的指针超出范围时,你将发生内存泄漏,因为你将丢失指针,因此不能在其上使用运算符。deleteobject1delete

在这里

B object2 = *(new B());

您正在丢弃 返回的指针,因此永远无法将该指针传递给以释放内存。因此,另一个内存泄漏。new B()delete

34赞 Luchian Grigore 1/13/2012 #4

分步说明:

// 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++ 中,有具有自动存储的对象,在堆栈上创建的对象会自动释放,而具有动态存储的对象则位于堆上,您可以分配这些对象,并且需要释放自己。(这都是粗略的)newdelete

认为您应该为每个对象分配一个 。deletenew

编辑

想想看,不一定是内存泄漏。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;
    }
}

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

但我怎么强调都不为过,不要这样做。它只是为了说明一个观点。

评论

2赞 CashCow 1/13/2012
我也是这么想的:我们可以破解它以免泄漏,但你不会想这样做。Object1 也不必泄漏,因为它的构造函数可以将自身附加到某种数据结构上,该结构会在某个时候将其删除。
2赞 Kos 2/8/2012
写出那些“可以这样做但不要这样做”的答案总是很诱人!:-)我知道这种感觉
7赞 CashCow 1/13/2012 #5

在 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赞 Mooing Duck 1/13/2012
还有更多 和 .还有.automaticdynamicstatic
477赞 R. Martinho Fernandes 1/13/2012 #6

发生了什么事情

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

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

new without cleanup

您需要传递指向它的指针才能清理它:delete

newing with delete

但是,第二个示例更糟:您正在取消引用指针,并创建对象的副本。这样一来,你就失去了指向用 创建的对象的指针,所以即使你想要,你也永远无法删除它!new

newing with 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

newing with automatic_pointer

这是一个常见的成语,其名称不是很描述性 RAII(资源获取即初始化)。当您获取需要清理的资源时,您可以将其粘贴到自动存储持续时间的对象中,这样您就无需担心清理它。这适用于任何资源,无论是内存、打开的文件、网络连接还是您喜欢的任何资源。

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

还有一个旧的(C++ 之前的)命名,但它现在已被弃用,因为它有一个奇怪的复制行为。auto_ptr

还有一些更聪明的例子,比如 ,它允许多个指针指向同一个对象,并且只有在最后一个指针被销毁时才清理它。std::shared_ptr

评论

5赞 R. Martinho Fernandes 1/13/2012
@user1131997:很高兴你又问了这个问题。如您所见,在评论中解释并不容易:)
1赞 Destructor 9/14/2015
@R.MartinhoFernandes:很好的答案。只有一个问题。为什么在运算符*()函数中使用引用返回?
1赞 R. Martinho Fernandes 7/8/2016
@Destructor迟到的回复:D。通过引用返回可以让你修改指针,这样你就可以像使用普通指针一样执行,例如,。如果它没有通过引用返回,它就不会模仿正常指针的行为,这就是这里的意图。*p += 2
1赞 Andy 2/20/2017
非常感谢您建议“将指向已分配对象的指针存储在自动存储持续时间对象中,该对象会自动删除它。要是有办法要求编码人员在编译任何 C++ 之前学习这种模式就好了!
10赞 Pubby 1/13/2012 #7

给定两个“对象”:

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()object1object1 == new A()&object1 != &(new A())

因为指针的值被保留,我们可以释放它指向的内存: 由于我们的规则,this 的行为与 which 没有泄漏的行为相同。delete object1;delete (new A());


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

B object2 = *(new B());

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

7赞 MGZero 1/13/2012 #8
B object2 = *(new B());

这条线是泄漏的原因。让我们把它分开一点..

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

现在,问题来了:

您从未真正将该指针存储在任何可以使用它的地方!完成此分配后,指针(address3 中的内存,用于访问 address2)将超出范围,超出您的范围!您不能再对它调用 delete,因此无法清理 address2 中的内存。剩下的是 address1 中 address2 的数据副本。记忆中的两件相同的事情。一个你可以访问,另一个你不能(因为你失去了通往它的路径)。这就是为什么这是内存泄漏的原因。

我建议从你的 C# 背景来看,你阅读了很多关于 C++ 中的指针是如何工作的。它们是一个高级主题,可能需要一些时间才能掌握,但它们的使用对您来说将是无价的。

6赞 Stefan 2/8/2012 #9

如果它更容易,可以把计算机内存想象成一个酒店,程序是客户在需要时租用房间。

这家酒店的运作方式是你预订一个房间,并在你离开时告诉门房。

如果你对一个房间进行编程,在没有告诉搬运工的情况下离开,搬运工会认为这个房间仍在使用中,不会让其他人使用它。在这种情况下,存在房间漏水。

如果程序分配内存并且不删除它(它只是停止使用它),则计算机认为内存仍在使用中,并且不允许其他人使用它。这是内存泄漏。

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

评论

5赞 AdamM 2/8/2012
我非常喜欢这个类比,它并不完美,但它绝对是向新手解释内存泄漏的好方法!
1赞 Stefan 8/27/2014
我在伦敦彭博社(Bloomberg)的一位高级工程师的采访中用这个来向一位人力资源女孩解释内存泄漏。我通过了那次面试,因为我能够以非程序员理解的方式向她解释内存泄漏(和线程问题)。