在C++中,对象是否应该自行删除?

24

我过去四年一直在使用C#,因此我对C++中的最佳实践和常见设计模式感兴趣。考虑以下部分示例:

class World
{
public:
    void Add(Object *object);
    void Remove(Object *object);
    void Update();
}

class Fire : Object
{
public:
    virtual void Update()
    {
        if(age > burnTime)
        {
            world.Remove(this);
            delete this;
        }
    }
}

在这里,我们有一个负责管理一组对象并定期更新它们的世界。 火是可能在许多不同情况下添加到世界中的对象,但通常是由已经存在于世界中的另一个对象添加的。火是唯一知道自己何时烧尽的对象,因此我目前让它自行删除。创建火的对象很可能已不存在或不相关。

这样做是否明智,还是有更好的设计可以帮助清理这些对象?

11个回答

30
您将Update函数声明为虚函数,这意味着派生类可以重写Update的实现。这会带来两个很大的风险:
1)重写的实现可能记得进行World.Remove,但却忘记了delete this,导致内存泄漏。
2)重写的实现调用了基类的Update,该函数执行了delete this,但后续还进行了更多的操作,但是使用一个无效的this指针。
请考虑以下示例:
class WizardsFire: public Fire
{
public:
    virtual void Update()
    {
        Fire::Update();  // May delete the this-object!
        RegenerateMana(this.ManaCost); // this is now invaild!  GPF!
    }
}

我见过这种情况!尝试追踪它并不好玩。 - Adam Tegen
啊,这不好玩。感谢指出这些问题,这是我们在C#中没有(直接)遇到的两个问题。 - Generic Error

20
这样做的问题在于,在对象和 World 类之间实际上创建了一种隐式耦合。
如果我尝试在 World 类外调用 Update(),会发生什么?我可能会发现对象被删除了,但我不知道为什么。这似乎是职责混乱。当你在编写此代码时,如果在新情况下使用 Fire 类,会出现问题。如果对象应该从多个位置删除怎么办?也许它应该从世界、当前地图和玩家的库存中删除?你的 Update 函数将从世界中删除它,然后删除对象,下次地图或库存尝试访问对象时,就会发生糟糕的事情。
总的来说,我认为一个 Update() 函数删除它正在更新的对象非常不直观。我还会说,一个对象删除自身也不直观。更合理的做法是,对象应该有某种方式表示它已经完成了燃烧,并且任何感兴趣的人现在可以采取行动。例如,将其从世界中删除。对于删除操作,要考虑所有权。
谁拥有这个对象?世界吗?这意味着世界独自决定对象死亡的时间。只要世界对对象的引用将比其他引用长,这就没问题。你认为对象拥有自己吗?这是什么意思?对象应该在对象不存在时被删除?这没有意义。
但是,如果没有明确定义单个所有者,请使用共享所有权,例如使用实现引用计数的智能指针,如 boost::shared_ptr。
但是,对于对象本身具有的成员函数来说,硬编码以将对象从一个特定列表中删除(无论其是否存在于其中,以及是否同时存在于任何其他列表中),并且无论存在哪些引用,都会删除该对象本身,这是一种不好的想法。

1
我在这里选择一个被接受的答案真是费了好大劲。我选择这个答案是因为它描述了我最终遇到的问题(除了世界持有引用之外的其他事情),并且还建议使用shared_ptr,这最终成为了我的解决方案的一部分。谢谢。 - Generic Error
1
删除 self 的另一个不好的原因是它排除了在堆栈上创建对象的可能性。此外,与 World 耦合会使独立测试变得不可能。 - walrii

6

在C++中,对象自我删除并没有什么问题,大多数引用计数的实现都会使用类似的技术。然而,这被视为一种略微神秘的技巧,你需要非常注意所有权问题。


6

如果未使用new分配和构造对象,则删除操作将失败并可能导致程序崩溃。这种构造方法将防止您在堆栈或静态上实例化类。在您的情况下,您似乎正在使用某种分配方案。如果您坚持使用该方案,这可能是安全的。不过我个人不会这么做。


5
我更喜欢更严格的所有权模式。世界应该拥有其中的对象并负责清理它们。可以使用world remove函数或让update(或其他函数)返回一个值来指示应删除对象。
我还猜测,在这种类型的模式中,您将要引用计数您的对象以避免出现悬空指针。

5
它对使用“new”创建的对象有效,并且在“Update”的调用者正确了解该行为时。但我会避免这种情况。在你的情况下,拥有权明显在于世界,所以我会让世界删除它。对象不是自己创建的,我认为它也不应该自己删除。如果您调用对象上的函数“Update”,但突然之间对象不存在而没有任何世界做出的操作(除了将其从列表中删除 - 但是在另一个帧!调用对象上的Update代码将注意不到这一点)可能会非常令人惊讶。
关于此问题的一些想法:
1. 添加一个对象引用列表到世界中。列表中的每个对象都等待删除。这是一种常见技术,在wxWidgets中用于已关闭但仍可能接收消息的顶级窗口。在空闲时间内,当处理完所有消息后,将处理待处理列表并删除对象。我相信Qt遵循类似的技术。
2. 告诉世界对象想要被删除。世界将被适当地告知并将照顾需要完成的任何事情。例如,像“deleteObject(Object&)”这样的东西。
3. 为每个对象添加一个“shouldBeDeleted”函数,如果对象希望被其所有者删除,则返回true。
我更喜欢选项3。世界将调用Update。之后,它会查看对象是否应该被删除,并可以这样做 - 或者如果它希望,它通过手动将该对象添加到待删除列表中来记住该事实。
当您无法确定何时何时不可以访问对象的函数和数据时,这真是一件痛苦的事情。例如,在wxWidgets中,有一个wxThread类,它可以在两种模式下操作之一(称为“可分离”)。其中之一是,如果其主要功能返回(并且线程资源应该被释放),它将删除自身(以释放wxThread对象占用的内存),而不是等待线程对象的所有者调用wait或join函数。然而,这会引起严重的头痛。您永远不可以调用任何要素,因为它可能已经在任何情况下终止,并且您不能使用new创建它。相当多的人告诉过我,他们非常不喜欢它的行为。
在我看来,引用计数对象的自我删除有点臭味相似。让我们比较一下:
// bitmap owns the data. Bitmap keeps a pointer to BitmapData, which
// is shared by multiple Bitmap instances. 
class Bitmap {
    ~Bitmap() {
        if(bmp->dec_refcount() == 0) {
            // count drops to zero => remove 
            // ref-counted object. 
            delete bmp;
        }
    }
    BitmapData *bmp;
};

class BitmapData {
    int dec_refcount();
    int inc_refcount();

};

将其与自删除引用计数对象进行比较:

class Bitmap {
    ~Bitmap() {
        bmp->dec_refcount();
    }
    BitmapData *bmp;
};

class BitmapData {
    int dec_refcount() {
        int newCount = --count;
        if(newCount == 0) {
            delete this;
        }
        return newCount;
    }
    int inc_refcount();
};

我认为第一种方式更好,我相信设计良好的引用计数对象不会使用"delete this",因为这会增加耦合性:使用引用计数数据的类必须知道并记住数据在减少引用计数的副作用下删除自身。请注意,在~Bitmap的析构函数中,"bmp"可能成为一个悬空指针。可以说,在这里不执行"delete this"更好。

回答类似问题"delete this有什么作用"的问题"What is the use of delete this"


我喜欢这个答案,最终使用了其中的元素,但不幸的是我只能接受一个答案。谢谢! - Generic Error
我很高兴我们能帮到你 :) 我认为jalf在他的回答中提出了一些非常好的想法。玩得开心 :) - Johannes Schaub - litb

2

其他人已经提到了“删除这个”的问题。简而言之,由于“世界”管理着火的创建,因此它也应该管理其删除。

另一个问题是如果世界末日时仍有火在燃烧。如果这是摧毁火的唯一机制,那么你可能会得到一个孤儿或垃圾火。

为了获得您想要的语义,我会在您的“Fire”类(或适用的对象)中设置一个“active”或“alive”标志。然后,世界偶尔会检查其物品清单,并处理不再活跃的物品。

--

还有一点需要注意的是,您编写的代码使Fire从Object私有继承,因为这是默认值,尽管它比公共继承要少见得多。您应该明确指出继承的类型,我认为您真正想要的是公共继承。


我们没有点燃火焰 它一直在燃烧 自世界开始转动以来 我们没有点燃火焰 不,我们没有点燃它 但我们试图与之抗争 - abelenky
这个缺失的 public 是因为我的大脑还停留在 C# 模式中... 没考虑过如果世界先结束了怎么删除火焰... 听起来有点严重! - Generic Error

2

这是一个相当常见的引用计数实现,我以前成功地使用过。

然而,我也看到它崩溃了。我希望我能记得崩溃的情况。在@abelenky上,我已经看到了它崩溃过。

可能是你进一步子类化Fire,但未创建虚析构函数(请参见下文)。当你没有虚析构函数时,Update()函数将会调用~Fire()而不是适当的析构函数~Flame()

class Fire : Object
{
public:
    virtual void Update()
    {
        if(age > burnTime)
        {
            world.Remove(this);
            delete this; //Make sure this is a virtual destructor.
        }
    }
};

class Flame: public Fire
{
private: 
    int somevariable;
};

可能在调用删除函数后,您尝试访问了一个成员变量或调用了一个虚函数。 - 1800 INFORMATION
优秀的说明虚拟析构函数的必要性。升级你。 - abelenky

1
通常情况下,除非必要或以非常直接的方式使用,“删除此”并不是一个好主意。在您的情况下,看起来世界可以在对象被移除时直接删除它,除非我们没有看到其他依赖关系(在这种情况下,您的删除调用将导致错误,因为它们没有得到通知)。将所有权保留在对象之外可以使对象更好地封装和更稳定:对象不会因为选择删除自身而在某个时刻变得无效。

如果你的意思是 World::Remove(Object*o) {delete o},那么这是不好的,因为 Remove 是从它正在删除的对象中调用的。这意味着它将返回到一个不存在的对象的方法。 - KeithB

0

我认为对象自删除本身并没有什么本质上的问题,但另一种可能的方法是让世界在 ::Remove 的过程中负责删除对象(假设所有已移除的对象也都被删除了)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接