我先学了C#,现在开始学习C++。据我了解,C++中的new
操作符与C#中的不相似。
你能解释一下在这段示例代码中为什么会出现内存泄漏的原因吗?
class A { ... };
struct B { ... };
A *object1 = new A();
B object2 = *(new B());
我先学了C#,现在开始学习C++。据我了解,C++中的new
操作符与C#中的不相似。
你能解释一下在这段示例代码中为什么会出现内存泄漏的原因吗?
class A { ... };
struct B { ... };
A *object1 = new A();
B object2 = *(new B());
正在发生什么
当你写下 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(Resource Acquisition Is Initialization)。当您获取需要清理的资源时,将其放入自动存储期对象中,以便您无需担心清理。这适用于任何资源,无论是内存、打开的文件、网络连接还是其他任何您喜欢的东西。
automatic_pointer
类已经以各种形式存在,我只是提供了一个示例。标准库中有一个非常相似的类,称为std::unique_ptr
。
还有一个旧的类(C++11之前)叫做auto_ptr
,但它现在已被弃用,因为它具有奇怪的复制行为。
然后还有一些更智能的例子,比如std::shared_ptr
,它允许多个指针指向同一个对象,并且仅在销毁最后一个指针时才清理它。
*p += 2
等操作。如果不返回引用,则无法模拟普通指针的行为,而这正是本意所在。 - R. Martinho Fernandes逐步解释:
// 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
获取其地址并删除指针将释放内存。
但我无法强调这一点:不要这样做。这只是为了说明一个观点而已。
给定两个“对象”:
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())
,因此是无效的。int func()
{
A a;
B b( 1, 2 );
}
在一个类中,通常会这样创建它:
class A
{
B b;
public:
A();
};
A::A() :
b( 1, 2 )
{
}
automatic
和 dynamic
,还有 static
。 - Mooing DuckB object2 = *(new B());
这一行代码是内存泄漏的根本原因。让我们来详细分析一下...
object2是一个类型为B的变量,存储在地址1(是的,我在这里选择了任意数字)。在右边,你要求一个新的B,或者说是指向类型为B的对象的指针。程序很高兴地给了你,并将你的新B分配到地址2,并在地址3中创建了一个指针。现在,访问地址2中的数据的唯一方法是通过地址3中的指针。接下来,您使用*
解除引用指针,以获取指针指向的数据(地址2中的数据)。这实际上创建了该数据的副本,并将其分配给了存储在地址1中的object2。请记住,这是一个副本,不是原件。
现在,问题来了:
您实际上从未将该指针存储在可以使用它的任何位置!一旦此赋值完成,指针(用于访问地址2的地址3中的内存)就超出了作用域并且无法访问!您无法再对其调用delete,因此无法清理地址2中的内存。你所剩下的是地址1中地址2数据的副本。两个相同的东西坐落在内存中。其中一个可以访问,另一个则无法访问(因为你失去了它的路径)。这就是为什么这是内存泄漏的原因。
我建议从您的C#背景出发,多了解一下C++中指针的工作方式。它们是一个高级主题,可能需要一些时间来掌握,但它们的使用对您非常有价值。
如果您在某个时候没有通过将内存指针传递给 delete
操作符来释放使用 new
操作符分配的内存,那么就会创建一个内存泄漏。
在上述两种情况下:
A *object1 = new A();
delete
释放内存,所以如果你的object1
指针超出了范围,你将会有一个内存泄漏,因为你丢失了该指针,所以无法使用delete
操作符。B object2 = *(new B());
您正在丢弃new B()
返回的指针,因此永远无法将该指针传递给delete
以释放内存。因此会出现另一个内存泄漏。
这一行代码立即泄漏:
B object2 = *(new B());
在这里,您正在堆上创建一个新的B
对象,然后在堆栈上创建副本。已在堆上分配的对象不再可以访问,因此会出现泄漏。
这行代码不是立即泄漏的:
A *object1 = new A();
object1
,那么可能会发生泄漏。object2
时,您正在创建使用new创建的对象的副本,但您也丢失了(从未分配的)指针(因此无法以后删除它)。为避免这种情况,您需要将object2
设置为引用。