【问题标题】:Deleting a pointer which is used by multiple variables删除多个变量使用的指针
【发布时间】:2013-11-05 22:31:38
【问题描述】:

我有一个关于在 C++ 中正确删除/释放内存的问题。

假设我有以下...(假设 A、B、C、D 是类。B 和 C 有实例变量 A* a。D 有两个实例变量,B* bC* c

A* a = new A();
B* b = new B(a);
C* c = new C(a);
D* d = new D(b, c);

B 和 C 的析构函数:

B::~B() { delete a; }
C::~C() { delete a; }

D 的析构函数:

D::~D() { delete b; delete c; }

现在当我打电话时

delete d;

我收到“访问冲突读取位置 0xfeeefeee”(我在 Visual Studio 2010 中)。我想这是因为

-D 的析构函数试图在 a 已被释放时两次“删除”相同的内存 (a)。

-我有两个指针(一个在B,另一个在C)都指向同一个地址(a),当D的析构函数删除b时(又调用delete a ),此内存现在设置为已释放。

-现在,当D 的析构函数删除c 时,c 尝试自行调用delete a,但由于a 已被释放而失败。

我对 C++ 比较陌生,但对编程并不陌生。我查了一下,发现智能指针(如 shared_ptr)可以解决这个问题,但在这种情况下,最佳实践是什么?我应该只创建两个单独的A 对象吗?

【问题讨论】:

  • 不要使用指针。您很可能不需要它们,即使您需要它们,它们也是一个非常高级的主题,极难理解和正确使用,所以暂时不要这样做。

标签: c++ memory constructor destructor


【解决方案1】:

如果您的设计计划与bc共享相同的a,则应将指针封装在共享指针中。 p>

但是,仅在某些情况下才需要使用指针。在大多数情况下,引用也可以完成这项工作。在您的情况下,如果您定义这四个实例的上下文也应该限制它们的生命周期,只需将它们定义为值:

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。
  • @LightnessRacesinOrbit 是的。只是提到它,这也适用于原始代码(delete a 不好)。
  • @ZacHowland 对于来自不同但语法相似的语言(尤其是 Java)的初学者来说,这奇怪。初学者经常想知道为什么不在构造函数中简单地使用赋值语句,如果他们认为它看起来更好,我完全同意。
  • @leemes 公平点,虽然“奇怪”仍然不是我会使用的词。 “不同”或“特定于 C++”......但它实际上是一种非常优雅的初始化数据成员的方式(与 inferior 托管语言相比)。
  • @leemes 好吧,对于原始代码来说,这有点相反,其A 从未面临自发破坏的风险,在delete 恶作剧之前。跨度>
【解决方案2】:

这里的问题在于确定谁拥有A 对象。似乎BC 都认为自己是所有者,因为他们delete 是指向a 的指针。但是他们没有分配A,所以一般他们也不应该删除它。

这个问题有几种解决方案:

  1. 使用对象而不是指针 - 这大大简化了内存管理,因为编译器会为您做正确的事情。
  2. BC 的构造函数中创建一个新的A 对象 - 您现在可以delete a,因为您拥有它。执行此操作时,还必须实现复制构造函数和赋值运算符。
  3. 隐式转移所有权 - 这是调用者最棘手的问题:一旦调用者将A 传递给BC 的构造函数,调用者必须停止使用对象指针。 BC 承担其 A 的所有权,在析构函数中将其删除。
  4. 传递拥有的对象 - 如果您控制BC 的生命周期,则可以确保A 在这些对象的生命周期内存在。在这种情况下,BC 可能会引用 A,但不会触及析构函数中的指针。

如您所见,该语言在控制所有权方面为您提供了很大的灵活性。一般而言,您应该从最简单的模型开始,当更简单的模型不再适合您的需求时,再转向更复杂的模型。

【讨论】:

    【解决方案3】:

    使用指针时,您需要确保指针拥有适当的所有权:最简单的方法是创建单独的所有者,例如

    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&lt;T&gt;s 将拥有对象并释放它们,所以没有一个析构函数会做任何事情。也就是说,指针用作链接而不是资源句柄。

    当然,您甚至可以完全不在堆上分配任何内存就逃脱:

    A a;
    B b(&a);
    C c(&c);
    D d(&b, &c);
    

    如果您想确保只要有任何用户,分配对象就一直存在,使用std::shared_ptr&lt;T&gt; 是合适的方法:指针用作共享资源句柄,使用引用计数是正确的方法。

    【讨论】:

      【解决方案4】:

      您面临的问题是所有权问题。目前,B 类和 C 类都假设它们拥有在构造时传递给它们的 A 对象(我们知道这一点,因为在它们的析构函数中破坏了对象)。如果这些对象应该/继续拥有一个 A 对象,那么您必须创建第二个 A。但是,如果它们可以共享该对象,则 B 和 C 都不应负责删除该对象(因为它是共享的)。解决此问题的一种方法是将 B 和 C 更改为仅知道 A 对象。这意味着 A 对象将需要被其他人删除。一种方法是使用 shared_ptr。

      【讨论】:

        【解决方案5】:

        大多数情况下,您无需在堆上分配内存(也就是说,您无需调用new)即可摆脱困境。如果您来自托管语言(Java、C# 等),这是一个很难改掉的习惯。根据你的设计目标,你的代码可以写成

        A a;
        B b(a);
        C c(a);
        D d(b, c);
        

        如果您的设计确实需要指针和动态内存使用,您应该使用智能指针包装器(std::unique_ptrstd::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);
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-08-09
          • 2016-07-22
          • 1970-01-01
          • 2016-02-24
          • 2013-09-24
          • 2012-08-12
          相关资源
          最近更新 更多