我有一个关于在C ++中正确删除/取消分配内存的问题。
假设我有以下...(假设A,B,C,D是类.B和C有实例变量A* a
。D有两个实例变量B* b
和{{1 }})
C* c
B和C的析构函数:
A* a = new A();
B* b = new B(a);
C* c = new C(a);
D* d = new D(b, c);
D的析构函数:
B::~B() { delete a; }
C::~C() { delete a; }
现在我打电话
D::~D() { delete b; delete c; }
我收到“访问冲突读取位置0xfeeefeee”(我在Visual Studio 2010中)。我想这是因为
- delete d;
的析构函数在D
已被释放时尝试“删除”相同的内存(a
)两次。
- 我有两个指针(一个在a
,另一个在B
)都指向同一个地址(C
),当D的析构函数删除a
时(进而调用b
),此内存现在设置为已释放。
- 当delete a
的析构函数删除D
时,c
尝试自行调用c
失败,因为delete a
已被解除分配。
我对C ++比较陌生,但对编程并不陌生。我查了一下,看到智能指针(如shared_ptr)可以解决这个问题,但在这种情况下最好的做法是什么?我应该创建两个单独的a
对象吗?
答案 0 :(得分:5)
如果您的设计计划与a
和b
共享相同的c
,则应将指针封装在共享指针。
但是,在某些情况下,只需要使用指针。在大多数情况下,参考也可以完成这项工作。在您的情况下,如果您定义这四个实例的上下文也应该限制它们的生命周期,只需将它们定义为值:
A a;
B b(a);
C c(a);
D d(b, c);
要通过引用传递对象,请在构造函数和私有成员中使用引用类型。并且不要在析构函数中调用delete
。
class B {
A & m_a;
public:
B (A &a) : m_a(a) {
}
};
关于引用和指针的一个非常重要的区别(开头可能比你想象的要多)是你不能改变引用(只有引用的值)。此外(这是这个事实的含义),必须在定义变量时分配它。这就是为什么你必须在构造函数中使用奇怪的初始化语法(:m_a(a)
)而不是在它的主体中使用赋值(m_a = a
将不起作用)。
此外,从不删除在构造函数中传递的析构函数中的实例,除非您知道自己在做什么,这意味着您知道规则三。但是聪明的指针使obsolete成为了这一点。
答案 1 :(得分:4)
这里的问题在于决定谁拥有A
对象。似乎B
和C
都认为自己是所有者,因为他们delete
指向a
。但是,他们没有分配A
,所以通常他们也不应该删除它。
这个问题有几种解决方案:
A
和B
的构造函数中创建一个新的C
对象 - 您现在可以delete a
,因为您拥有它。执行此操作时,还必须实现复制构造函数和赋值运算符。A
传递给B
或{{{{}}的构造函数1}},调用者必须立即停止使用对象指针。 C
和B
承担其C
的所有权,并在析构函数中删除它。A
和B
的生命周期,则可以确保在这些对象的生命周期内存在C
。在这种情况下,A
和B
可能会引用C
,但不会触及析构函数中的指针。如您所见,该语言为您提供了控制所有权的灵活性。一般来说,你应该从最简单的模型开始,当更简单的模型不再适合你的需求时,你可以进展到更复杂的模型。
答案 2 :(得分:2)
使用指针时,您需要确保指针适当拥有:最简单的方法是创建个人所有者,例如
std::unique_ptr<A> a(new A());
std::unique_ptr<B> b(new B(a.get()));
std::unique_ptr<C> c(new C(a.get()));
std::unique_ptr<D> d(new D(b.get(), c.get());
所有析构函数都不会执行任何操作,因为std::unique_ptr<T>
将拥有对象并释放它们。也就是说,指针用作链接但不用作资源句柄。
当然,你甚至可以在没有在堆上分配任何内存的情况下离开:
A a;
B b(&a);
C c(&c);
D d(&b, &c);
如果要确保只要有任何用户就可以保留分配对象,那么使用std::shared_ptr<T>
是合适的方法:指针用作共享资源句柄,使用引用计数是正确的方法
答案 3 :(得分:2)
您遇到的问题是所有权的问题。目前,B类和C类都假设它们拥有在构造时传递给它们的A对象(我们知道这是因为它们在析构函数中销毁了对象)。如果这些对象应该/继续拥有一个A对象,那么你必须创建第二个A.但是,如果它们可以共享该对象,则B和C都不应该负责删除该对象(因为它是共享的)。解决此问题的一种方法是将B和C更改为仅知道 A对象。这意味着A对象需要被其他人删除。一种方法是使用shared_ptr。
答案 4 :(得分:2)
大多数情况下,您可以在不需要在堆上分配内存的情况下离开(也就是说,您不需要调用new
)。如果你来自托管语言(Java,C#等),那么打破它是一个很难的习惯。根据您的设计目标,您的代码可以写为
A a;
B b(a);
C c(a);
D d(b, c);
如果您的设计确实需要指针和动态内存使用,则应使用智能指针包装器(std::unique_ptr
和std::shared_ptr
)。在这种特殊情况下,我建议您使用std::shared_ptr
,因为您在多个地方使用(例如共享)指针:
std::shared_ptr<A> pA = std::make_shared<A>();
std::shared_ptr<B> pB = std::make_shared<B>(a);
std::shared_ptr<C> pC = std::make_shared<C>(a);
std::shared_ptr<D> pD = std::make_shared<D>(b, c);