【问题标题】: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());

【问题讨论】:

标签: c++ pointers memory-leaks new-operator c++-faq


【解决方案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 女孩解释内存泄漏。我通过了那次面试,因为我能够以她理解的方式向非程序员解释内存泄漏(和线程问题)。
        猜你喜欢
        • 2013-07-12
        • 1970-01-01
        • 2017-02-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多