为什么使用'new'导致内存泄漏?

130 c++ pointers memory-leaks c++-faq new-operator

我首先学习了C#,现在我开始使用C++.据我所知,newC++中的operator 与C#中的operator 不相似.

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

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

A *object1 = new A();
B object2 = *(new B());
Run Code Online (Sandbox Code Playgroud)

R. *_*des 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
Run Code Online (Sandbox Code Playgroud)

如果确实需要动态存储持续时间,请将指针存储在自动存储持续时间对象中的已分配对象中,该对象会自动删除它.

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
Run Code Online (Sandbox Code Playgroud)

使用automatic_pointer进行修改

这是一个常见的习惯用法,它由不太具描述性的名称RAII(资源获取是初始化)组成.当您获得需要清理的资源时,将其粘贴在自动存储持续时间的对象中,这样您就不必担心清理它了.这适用于任何资源,无论是内存,打开文件,网络连接,还是您喜欢的任何资源.

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

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

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

  • @ user1131997:很高兴你提出了另一个问题.正如你所看到的,在评论中解释并不容易:) (4认同)

Luc*_*ore 34

一步一步解释:

// 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());
Run Code Online (Sandbox Code Playgroud)

所以到最后,你在堆上有一个没有指针的对象,所以删除是不可能的.

另一个样本:

A *object1 = new A();
Run Code Online (Sandbox Code Playgroud)

只有忘记delete分配的内存时才会出现内存泄漏:

delete object1;
Run Code Online (Sandbox Code Playgroud)

在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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 我的想法是一样的:我们可以破解它不泄漏,但你不想这样做.object1也不必泄漏,因为它的构造函数可以将自己附加到某种数据结构上,这种数据结构会在某些时候删除它. (2认同)
  • 写这些"有可能做到这一点但却没有"答案总是那么诱人!:-) 我明白这感受 (2认同)

Pub*_*bby 11

鉴于两个"对象":

obj a;
obj b;
Run Code Online (Sandbox Code Playgroud)

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

将one的值分配给另一个不会改变它们的位置,但会改变它们的内容:

obj a;
obj b = a;
//a == b, but &a != &b
Run Code Online (Sandbox Code Playgroud)

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

obj *a;
obj *b = a;
//a == b, but &a != &b
Run Code Online (Sandbox Code Playgroud)

现在,让我们来看看你的例子:

A *object1 = new A();
Run Code Online (Sandbox Code Playgroud)

这是指定new A()to 的值object1.值是一个指针,意思是object1 == new A(),但是&object1 != &(new A()).(注意,此示例不是有效代码,仅用于解释)

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


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

B object2 = *(new B());
Run Code Online (Sandbox Code Playgroud)

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


Cas*_*Cow 9

在C#和Java中,您使用new来创建任何类的实例,然后您不必担心以后会破坏它.

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

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

  • 自动
  • 动态

通过自动创建,您可以在作用域环境中创建对象: - 在函数中或 - 作为类(或结构)的成员.

在函数中,您将以这种方式创建它:

int func()
{
   A a;
   B b( 1, 2 );
}
Run Code Online (Sandbox Code Playgroud)

在一个类中,您通常会以这种方式创建它:

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

A::A() :
 b( 1, 2 )
{
}
Run Code Online (Sandbox Code Playgroud)

在第一种情况下,退出范围块时会自动销毁对象.这可以是函数内的函数或范围块.

在后一种情况下,对象b与其所属的A实例一起被销毁.

当您需要控制对象的生命周期时,对象将使用new进行分配,然后需要删除才能销毁对象.使用称为RAII的技术,您可以通过将对象置于自动对象中来处理在创建对象时删除对象,并等待该自动对象的析构函数生效.

一个这样的对象是shared_ptr,它将调用"删除器"逻辑,但仅当共享对象的shared_ptr的所有实例都被销毁时才会调用.

通常,虽然您的代码可能有很多调用new,但您应该有限制的删除调用,并且应始终确保从析构函数或放入智能指针的"删除器"对象调用它们.

你的析构函数也应该永远不会抛出异常.

如果这样做,您将几乎没有内存泄漏.

  • 不仅仅是"自动"和"动态".还有`静态'. (4认同)

MGZ*_*ero 9

B object2 = *(new B());
Run Code Online (Sandbox Code Playgroud)

这条线是泄漏的原因.让我们分一点吧..

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

现在,问题出在这里:

你从来没有将指针存储在你可以使用它的任何地方!完成此分配后,指针(用于访问address2的address3中的内存)超出范围且超出您的范围!您不能再在其上调用delete,因此无法清除address2中的内存.你剩下的是address1中address2的数据副本.两个相同的东西坐在记忆中.一个你可以访问,另一个你不能(因为你失去了它的路径).这就是为什么这是内存泄漏.

我建议你来自你的C#背景,你已经阅读了很多关于C++指针如何工作的内容.它们是一个高级主题,可能需要一些时间来掌握,但它们的使用对您来说是非常宝贵的.


Ste*_*fan 8

如果它更容易,可以将计算机内存视为酒店,程序是指在需要时租用房间的客户.

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

如果您在没有告诉搬运工的情况下将书籍编入房间并离开,搬运工会认为房间仍在使用,并且不会让其他人使用它.在这种情况下,有房间泄漏.

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

这不是一个完全类比,但它可能会有所帮助.

  • 我非常喜欢这个类比,它并不完美,但它绝对是向新手解释内存泄漏的好方法! (5认同)

Mar*_*rio 7

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

  • 获取引用的地址来删除对象是非常糟糕的做法.使用智能指针. (3认同)
  • 令人难以置信的糟糕做法,是吗?您认为智能指针在幕后使用的是什么? (3认同)
  • @Blindy智能指针(至少是体面的实现)直接使用指针. (3认同)
  • @Blindy:智能指针如何重置? (2认同)
  • 嗯,说实话,整个想法并不那么好,不是吗?实际上,我甚至不确定在OP中尝试的模式实际上是否有用. (2认同)
  • 无处不在,这绝对是错误的. (2认同)

mat*_*way 7

这条线立即泄漏:

B object2 = *(new B());
Run Code Online (Sandbox Code Playgroud)

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

这条线不会立即泄漏:

A *object1 = new A();
Run Code Online (Sandbox Code Playgroud)

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

  • 在解释动态/自动存储时请不要使用堆/堆栈. (4认同)
  • @ user1131997堆/堆栈是实现细节.知道它们很重要,但与这个问题无关. (4认同)
  • @Pubby为什么不用?因为动态/自动存储总是堆,而不是堆栈?这就是为什么没有必要详细说明堆栈/堆,我是对的吗? (2认同)
  • 嗯,我想要一个单独的答案,即与我的相同,但用您认为最好的方式替换堆/堆栈.我有兴趣了解你更愿意解释它. (2认同)

raz*_*ebe 7

好吧,如果你不new通过将指向该内存的指针传递给操作员来释放你使用操作符分配的内存,则会产生内存泄漏delete.

在你的两个案例中:

A *object1 = new A();
Run Code Online (Sandbox Code Playgroud)

在这里你不是delete用来释放内存,所以当你的object1指针超出范围时,你会有内存泄漏,因为你将丢失指针,所以不能delete在它上面使用运算符.

和这里

B object2 = *(new B());
Run Code Online (Sandbox Code Playgroud)

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