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

我首先学习了C#,现在我从C ++开始。 据我所知,C ++中的new操作符与C#中的不一样。

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

 class A { ... }; struct B { ... }; A *object1 = new A(); B object2 = *(new B()); 

发生什么事

当你写T t; 你正在创build一个typesT的对象, 自动存储持续时间 。 超出范围时会自动清除。

当你写new T()你正在创build一个dynamic存储持续时间types为T的对象。 它不会自动清理。

新的没有清理

你需要传递一个指针来delete它来清理它:

新的删除

然而,你的第二个例子更糟糕:你正在取消引用指针,并复制对象。 这样你就失去了用new创build的对象的指针,所以即使你想要,也不能删除它!

新的与deref

你应该做什么

你应该更喜欢自动存储的时间。 需要一个新的对象,只写:

 A a; // a new object of type A B b; // a new object of type B 

如果您确实需要dynamic存储持续时间,请将指针存储在自动存储持续时间对象中,该对象将自动删除该对象。

 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( Resource Acquisition Is Initialization )。 当您获取需要清理的资源时,将其粘贴在自动存储期限的对象中,因此您不必担心清理它。 这适用于任何资源,无论是内存,打开文件,networking连接,或任何你喜欢的。

这个automatic_pointer指针的东西已经以各种forms存在了,我刚刚提供了一个例子。 标准库中存在一个名为std::unique_ptr非常类似的类。

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

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

一步一步的解释:

 // 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 ++中,存在自动存储的对象,堆栈中自动处理的对象,以及具有dynamic存储的对象,这些对象将分配给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获取它的地址并删除指针将释放内存。

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

给定两个“对象”:

 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()的值object1 。 该值是一个指针,意思是object1 == new A() ,但是&object1 != &(new A()) 。 (注意这个例子不是有效的代码,只是为了解释)

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


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

 B object2 = *(new B()); 

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

在C#和Java中,你使用new来创build任何类的实例,然后你不用担心以后销毁它。

C ++也有一个关键字“new”来创build一个对象,但是与Java或C#不同,它不是创build对象的唯一方法。

C ++有两种机制来创build一个对象:

  • 自动
  • dynamic

通过自动创build,您可以在范围化的环境中创build对象: – 在函数中或 – 作为类(或结构)的成员。

在一个函数中你可以这样创build它:

 int func() { A a; B b( 1, 2 ); } 

在课堂上,你通常会这样做:

 class A { B b; public: A(); }; A::A() : b( 1, 2 ) { } 

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

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

如果需要控制对象的生命周期,则需要使用新的对象进行分配,然后需要使用删除来销毁对象。 使用称为RAII的技术,通过将对象放在自动对象中,您可以在创build对象时删除该对象,并等待自动对象的析构函数生效。

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

一般来说,虽然你的代码可能有很多调用新的,你应该有有限的调用来删除,并应始终确保这些调用从析构函数或“删除”对象被放入智能指针。

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

如果你这样做,你会有很less的内存泄漏。

 B object2 = *(new B()); 

这条线是泄漏的原因。 让我们分开来看看

object2是typesB的variables,存储在say地址1(是的,我在这里select任意数字)。 在右边,你要求一个新的B,或者一个指向Btypes的对象的指针。程序很高兴地给你这个,并将你的新的B赋给地址2,同时在地址3中创build一个指针。现在,访问地址2中的数据的唯一方式是通过地址3中的指针。接下来,使用*取消指针指针以获取指针指向的数据(地址2中的数据)。 这有效地创build了该数据的一个副本,并将其分配给地址1中指定的object2。请记住,这是一个COPY,而不是原始的。

现在,这是问题:

你从来没有真正将该指针存储在任何可以使用的地方! 一旦这个任务完成,指针(你用来访问地址2的地址3中的内存)超出了范围,超出了你的范围! 您不能再调用它的删除,因此无法清除地址2中的内存。 你留下的是地址1中地址2的数据的副本。 记忆中有两件相同的事情。 一个你可以访问,另一个你不能(因为你失去了通往它的道路)。 这就是为什么这是内存泄漏。

我build议从你的C#背景来看,你阅读了很多C ++指针的工作原理。 他们是一个高级的话题,需要一些时间来掌握,但他们的使用将是非常宝贵的。

当创buildobject2你正在用new创build一个你创build的对象的副本,但是你也失去了(从来没有指定过的)指针(所以以后无法删除它)。 为了避免这种情况,你必须使object2成为一个参考。

那么,如果你在某些时候没有释放你使用new运算符分配的内存,就会创build一个内存泄漏,方法是将指向该内存的指针传递给delete运算符。

在你上面的两个例子中:

 A *object1 = new A(); 

这里你没有使用delete来释放内存,所以如果你的object1指针超出了范围,你将会发生内存泄漏,因为你将丢失指针,所以不能使用delete操作符它。

和这里

 B object2 = *(new B()); 

你放弃了new B()返回的指针,所以永远不能通过该指针来delete内存被释放。 因此另一个内存泄漏。

这是直接泄漏的线路:

 B object2 = *(new B()); 

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

这条线不是立即泄漏:

 A *object1 = new A(); 

如果你永远不delete d object1会有泄漏。

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

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

如果你在没有告诉搬运工的情况下编了一个房间的书并离开,搬运工会认为房间还在使用,不会让其他人使用它。 在这种情况下,有一个房间泄漏。

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

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