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

时间:2012-01-12 18:01:58

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

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

您可以在此示例代码中解释内存泄漏的原因吗?

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

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

9 个答案:

答案 0 :(得分:460)

发生了什么

当您编写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_pointer已经以各种形式存在,我刚刚提供了一个例子。标准库中存在一个非常相似的类,名为std::unique_ptr

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

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

答案 1 :(得分: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());

因此,到最后,你在堆上有一个没有指针的对象,因此无法删除。

另一个样本:

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 :(得分:11)

给出两个“对象”:

obj a;
obj b;

它们不会占用内存中的相同位置。换句话说,&a != &b

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

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()),因此无效。

答案 3 :(得分: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实例一起被销毁。

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

一个这样的对象是一个shared_ptr,它将调用“删除”逻辑,但只有当共享该对象的shared_ptr的所有实例都被销毁时才会被调用。

一般情况下,虽然你的代码可能有很多新的调用,但你应该有限制的删除调用,并且应该始终确保从析构函数或放入智能指针的“删除器”对象中调用它们。

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

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

答案 4 :(得分:9)

B object2 = *(new B());

此行是泄漏的原因。让我们分开一点..

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

现在,问题在于:

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

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

答案 5 :(得分:8)

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

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

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

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

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

答案 6 :(得分:7)

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

答案 7 :(得分:7)

这条线立即泄漏:

B object2 = *(new B());

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

此行不会立即泄漏:

A *object1 = new A();

如果你从来没有delete d object1,那么会有泄漏。

答案 8 :(得分:7)

好吧,如果你没有通过将指向该内存的指针传递给new运算符来释放你使用delete运算符分配的内存,则会产生内存泄漏。

在上述两种情况中:

A *object1 = new A();

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

在这里

B object2 = *(new B());

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