为什么使用“新”会导致内存泄漏?
我首先学习了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的对象的指针,所以即使你想要,也不能删除它!
你应该做什么
你应该更喜欢自动存储的时间。 需要一个新的对象,只写:
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
会有泄漏。
如果更容易,可以将计算机内存视为酒店,而程序则是在需要时租用房间的客户。
这家酒店的工作方式是,你预订一个房间,并告诉搬运工,当你离开。
如果你在没有告诉搬运工的情况下编了一个房间的书并离开,搬运工会认为房间还在使用,不会让其他人使用它。 在这种情况下,有一个房间泄漏。
如果您的程序分配内存并且不删除它(它只是停止使用它),那么计算机认为内存仍在使用中,并且不允许其他人使用它。 这是一个内存泄漏。
这不是一个确切的比喻,但它可能有帮助。