为啥使用“新”会导致内存泄漏?
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();
如果你从不delete
d 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 进行有限调用,并且应始终确保这些调用是从放入智能指针的析构函数或“删除”对象中调用的。
你的析构函数也不应该抛出异常。
如果你这样做,你将很少有内存泄漏。
【讨论】:
不止automatic
和dynamic
。还有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;
它们不会在内存中占据相同的位置。换句话说,&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());
的行为相同,没有泄漏。
对于第二个示例,您正在复制指向的对象。该值是该对象的内容,而不是实际的指针。与其他所有情况一样,&object2 != &*(new A())
。
B object2 = *(new B());
我们丢失了指向分配内存的指针,因此我们无法释放它。 delete &object2;
可能看起来会起作用,但因为&object2 != &*(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 会导致内存泄漏?