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

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

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

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


A *object1 = new A();
B object2 = *(new B());
28156 次浏览

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

正是这条线路立即泄漏了:

B object2 = *(new B());

在这里,您将在堆上创建一个新的 B对象,然后在堆栈上创建一个副本。已经在堆上分配的内存不能再被访问,因此会发生泄漏。

这句话并没有立即泄露出去:

A *object1 = new A();

如果你从来没有 deleted 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释放。(这些都是粗略的说法)

我们认为,对于用 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获取其地址并删除指针将释放内存。

但我要强调的是,不要这样做,这只是为了表明一个观点。

在 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 的技术,在创建对象的时候,通过将对象放入自动对象中来处理对象的删除,然后等待自动对象的析构函数生效。

其中一个对象是 share _ ptr,它将调用“ deleter”逻辑,但只有在共享该对象的 share _ ptr 的所有实例都被销毁时才会调用该逻辑。

一般来说,虽然您的代码可能有许多 new 调用,但是您应该有有限的调用可以删除,并且应该始终确保这些调用来自放入智能指针的析构函数或“ deleter”对象。

析构函数也不应该引发异常。

如果这样做,您将很少有内存泄漏。

发生了什么

在编写 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 (RAII) ,这个名字描述性不强。当您获取一个需要清理的资源时,您可以将它放在一个自动存储持续时间的对象中,这样就不需要担心清理它。这适用于任何资源,无论是内存、打开的文件、网络连接,还是您喜欢的任何资源。

这个 automatic_pointer已经以各种形式存在,我只是提供了一个例子。在标准库 std::unique_ptr中存在一个非常类似的类。

还有一个旧的(在 C + + 11之前)命名为 auto_ptr,但是现在已经废弃了,因为它有一种奇怪的复制行为。

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

给出两个“对象”:

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()),所以是无效的。

B object2 = *(new B());

这条线是漏水的原因,我们把它拆开来看看。

Object2是一个 B 类型的变量,存储在比如地址1(是的,我在这里选择任意数字)。在右边,您要求一个新的 B,或者一个指向 B 类型对象的指针。程序很高兴地把这个给你,并把你的新 B 分配给地址2,还在地址3中创建了一个指针。现在,访问地址2中的数据的唯一方法是通过地址3中的指针。接下来,使用 *取消对指针的引用,以获取指针所指向的数据(地址2中的数据)。这将有效地创建该数据的一个副本,并将其分配给 object2,分配到地址1中。记住,这是复制品,不是原件。

现在,问题来了:

实际上,您从未将该指针存储在任何可以使用它的地方!一旦这个任务完成,指针(address3中的内存,您用来访问 address2)就超出了作用域,超出了您的范围!您不能再对它调用 delete,因此无法清理 address2中的内存。剩下的是 address1中 address2的数据副本。记忆中有两样一模一样的东西。一个你可以访问,另一个你不能(因为你失去了它的路径)。这就是内存泄漏的原因。

我建议从你的 C # 背景中读到很多关于 C + + 中的指针是如何工作的。它们是一个高级话题,可能需要一些时间来理解,但是它们的使用对你来说是无价的。

如果计算机内存能让你更容易使用,那么把它想象成一个酒店,程序就是那些在需要的时候租用房间的客户。

这家旅馆的运作方式是你预订一个房间,告诉行李员你什么时候离开。

如果你编程预定了一个房间,却没有告诉门房就离开了,门房会认为这个房间还在使用,不会让任何人使用它。在这种情况下,有一个房间泄漏。

如果你的程序分配内存而不删除它(它只是停止使用它) ,那么计算机认为内存仍然在使用,不会允许任何人使用它。这是内存泄漏。

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