为啥使用“新”会导致内存泄漏?

Posted

技术标签:

【中文标题】为啥使用“新”会导致内存泄漏?【英文标题】:Why does the use of 'new' cause memory leaks?为什么使用“新”会导致内存泄漏? 【发布时间】:2012-02-09 00:41:57 【问题描述】:

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

你能解释一下这个示例代码中内存泄漏的原因吗?

class A  ... ;
struct B  ... ;

A *object1 = new A();
B object2 = *(new B());

【问题讨论】:

几乎重复:Is garbage collection automatic in standard C++? 【参考方案1】:

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

【讨论】:

获取引用地址来删除对象是非常糟糕的做法。使用智能指针。 非常糟糕的做法,嗯?您认为智能指针在幕后使用什么? @Blindy 智能指针(至少实现得体)直接使用指针。 好吧,老实说,整个想法并没有那么好,不是吗?实际上,我什至不确定在 OP 中尝试的模式在哪里真正有用。【参考方案2】:

正是这一行立即泄漏:

B object2 = *(new B());

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

这条线不会立即泄漏:

A *object1 = new A();

如果你从不deleted object1,就会有泄漏。

【讨论】:

在解释动态/自动存储时请不要使用堆/栈。 @Pubby 为什么不用呢?因为动态/自动存储总是堆而不是堆栈?这就是为什么不需要详细说明堆栈/堆的原因,对吗? @user1131997 堆/堆栈是实现细节。它们很重要,但与这个问题无关。 嗯,我想要一个单独的答案,即和我的一样,但用你认为最好的替换堆/堆栈。我很想知道您希望如何解释它。【参考方案3】:

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

在上述两种情况下:

A *object1 = new A();

这里你没有使用delete来释放内存,所以如果你的object1指针超出范围,你就会有内存泄漏,因为你会丢失指针,所以可以不要在上面使用delete 运算符。

这里

B object2 = *(new B());

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

【讨论】:

【参考方案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++ 中,有自动存储的对象,在堆栈​​上创建的对象会自动释放,而在堆上具有动态存储的对象,您使用 new 分配并需要使用 @987654326 释放自己@。 (这都是粗略的)

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

编辑

想想看,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 获取其地址并删除指针将释放内存。

但我不能强调这一点,不要这样做。这只是为了说明一点。

【讨论】:

我也有同样的想法:我们可以破解它以防止泄漏,但您不想这样做。 object1 也不必泄漏,因为它的构造函数可以将自己附加到某种数据结构上,该结构会在某个时候将其删除。 写出那些“可以这样做但不可以”的答案总是很诱人! :-) 我知道那种感觉【参考方案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 进行有限调用,并且应始终确保这些调用是从放入智能指针的析构函数或“删除”对象中调用的。

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

如果你这样做,你将很少有内存泄漏。

【讨论】:

不止automaticdynamic。还有static【参考方案6】:

发生了什么

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

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

您需要将指向它的指针传递给delete 以便清理它:

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

你应该做什么

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

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

这是一个常见的习惯用法,名称不是很具描述性的 RAII(资源获取即初始化)。当您获得需要清理的资源时,将其粘贴在自动存储期限的对象中,因此您无需担心清理它。这适用于任何资源,无论是内存、打开的文件、网络连接还是您喜欢的任何资源。

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

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

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

【讨论】:

@user1131997:很高兴你提出了另一个问题。如您所见,在 cmets 中解释起来并不容易 :) @R.MartinhoFernandes:很好的答案。就一个问题。为什么你在 operator* () 函数中使用了引用返回? @Destructor 延迟回复:D。通过引用返回可让您修改指针,因此您可以像使用普通指针一样执行例如*p += 2。如果它没有通过引用返回,它就不会模仿正常指针的行为,这就是这里的意图。 非常感谢您建议“将指向已分配对象的指针存储在自动存储持续时间对象中,该对象会自动删除它。”如果有办法要求编码人员在能够编译任何 C++ 之前学习这种模式!【参考方案7】:

给定两个“对象”:

obj a;
obj b;

它们不会在内存中占据相同的位置。换句话说,&amp;a != &amp;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(),但是&amp;object1 != &amp;(new A())。 (注意这个例子不是有效代码,仅供说明)

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


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

B object2 = *(new B());

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

【讨论】:

【参考方案8】:
B object2 = *(new B());

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

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

现在,问题来了:

您实际上从未将该指针存储在任何可以使用它的地方!分配完成后,指针(地址 3 中的内存,用于访问地址 2)超出范围,超出您的范围!您不能再对其调用 delete,因此无法清理 address2 中的内存。剩下的是地址 1 中地址 2 的数据副本。两个相同的东西留在记忆中。一个您可以访问,另一个您不能访问(因为您失去了通往它的路径)。这就是内存泄漏的原因。

我建议您具有 C# 背景,阅读大量有关 C++ 中指针如何工作的内容。它们是一个高级主题,可能需要一些时间才能掌握,但它们的使用对您来说非常宝贵。

【讨论】:

【参考方案9】:

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

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

如果您编程预订房间并在没有告诉搬运工的情况下离开,搬运工会认为该房间仍在使用中,并且不会让其他任何人使用它。在这种情况下,房间漏水。

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

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

【讨论】:

我很喜欢这个类比,它并不完美,但它绝对是向不熟悉它的人解释内存泄漏的好方法! 我在伦敦彭博社的一位高级工程师的采访中用这个来向一位 HR 女孩解释内存泄漏。我通过了那次面试,因为我能够以她理解的方式向非程序员解释内存泄漏(和线程问题)。

以上是关于为啥使用“新”会导致内存泄漏?的主要内容,如果未能解决你的问题,请参考以下文章

为啥使用 Activity 上下文会导致 Context 内存泄漏

为啥这会导致内存泄漏?

为啥这个 INotifyCollectionChanged 会导致内存泄漏?

为啥并行读取数组会导致内存泄漏?

为啥“[[UIDevice currentDevice] identifierForVendor]”会导致内存泄漏?

为啥 pthread 会导致内存泄漏