提问人: 提问时间:1/13/2012 最后编辑:Xeo 更新时间:7/20/2012 访问量:30008
为什么使用“new”会导致内存泄漏?
Why does the use of 'new' cause memory leaks?
问:
我首先学习了 C#,现在我开始学习 C++。据我了解,C++ 中的运算符与 C# 中的运算符不同。new
您能解释一下此示例代码中内存泄漏的原因吗?
class A { ... };
struct B { ... };
A *object1 = new A();
B object2 = *(new B());
答:
创建时,您正在创建使用 new 创建的对象的副本,但您也会丢失(从未分配的)指针(因此以后无法删除它)。为了避免这种情况,你必须做一个参考。object2
object2
评论
正是这条线立即泄漏:
B object2 = *(new B());
在这里,您将在堆上创建一个新对象,然后在堆栈上创建一个副本。无法再访问已在堆上分配的那个,因此会泄漏。B
这条线路不会立即泄漏:
A *object1 = new A();
如果你从不这样做,就会有泄漏。delete
object1
评论
好吧,如果你在某个时候没有通过将指向该内存的指针传递给运算符来释放使用该内存分配的内存,则会造成内存泄漏。new
delete
在上述两种情况下:
A *object1 = new A();
在这里,你不用于释放内存,所以当你的指针超出范围时,你将发生内存泄漏,因为你将丢失指针,因此不能在其上使用运算符。delete
object1
delete
在这里
B object2 = *(new B());
您正在丢弃 返回的指针,因此永远无法将该指针传递给以释放内存。因此,另一个内存泄漏。new B()
delete
分步说明:
// 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
但我怎么强调都不为过,不要这样做。它只是为了说明一个观点。
评论
在 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 的调用应该有限,并且应始终确保这些调用是从放入智能指针中的析构函数或“deleter”对象调用的。
析构函数也不应抛出异常。
如果这样做,则很少发生内存泄漏。
评论
automatic
dynamic
static
发生了什么事情
当您编写时,您将创建一个具有自动存储持续时间的类型的对象。当它超出范围时,它会自动清理。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
还有一个旧的(C++ 之前的)命名,但它现在已被弃用,因为它有一个奇怪的复制行为。auto_ptr
还有一些更聪明的例子,比如 ,它允许多个指针指向同一个对象,并且只有在最后一个指针被销毁时才清理它。std::shared_ptr
评论
*p += 2
给定两个“对象”:
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())
因为指针的值被保留,我们可以释放它指向的内存: 由于我们的规则,this 的行为与 which 没有泄漏的行为相同。delete object1;
delete (new A());
对于第二个示例,您正在复制指向对象。该值是该对象的内容,而不是实际的指针。与所有其他情况一样,.&object2 != &*(new A())
B object2 = *(new B());
我们丢失了指向已分配内存的指针,因此无法释放它。 它可能看起来会起作用,但因为 ,它不等同于 和 所以无效。delete &object2;
&object2 != &*(new A())
delete (new A())
B object2 = *(new B());
这条线是泄漏的原因。让我们把它分开一点..
object2 是 B 类型的变量,存储在地址 1 中(是的,我在这里选择任意数字)。在右侧,您请求了一个新的 B,或指向 B 类型对象的指针。该程序很乐意为您提供此内容,并将您的新 B 分配给地址 2,并在地址 3 中创建指针。现在,访问地址 2 中数据的唯一方法是通过地址 3 中的指针。接下来,取消引用指针,用于获取指针指向的数据(地址 2 中的数据)。这有效地创建了该数据的副本,并将其分配给在地址 1 中分配的对象 2。请记住,这是副本,而不是原件。*
现在,问题来了:
您从未真正将该指针存储在任何可以使用它的地方!完成此分配后,指针(address3 中的内存,用于访问 address2)将超出范围,超出您的范围!您不能再对它调用 delete,因此无法清理 address2 中的内存。剩下的是 address1 中 address2 的数据副本。记忆中的两件相同的事情。一个你可以访问,另一个你不能(因为你失去了通往它的路径)。这就是为什么这是内存泄漏的原因。
我建议从你的 C# 背景来看,你阅读了很多关于 C++ 中的指针是如何工作的。它们是一个高级主题,可能需要一些时间才能掌握,但它们的使用对您来说将是无价的。
如果它更容易,可以把计算机内存想象成一个酒店,程序是客户在需要时租用房间。
这家酒店的运作方式是你预订一个房间,并在你离开时告诉门房。
如果你对一个房间进行编程,在没有告诉搬运工的情况下离开,搬运工会认为这个房间仍在使用中,不会让其他人使用它。在这种情况下,存在房间漏水。
如果程序分配内存并且不删除它(它只是停止使用它),则计算机认为内存仍在使用中,并且不允许其他人使用它。这是内存泄漏。
这不是一个确切的类比,但它可能会有所帮助。
评论
上一个:默认函数参数的有效表达式
评论