在以下情况下手动调用析构函数是一个糟糕的设计决策吗?(涉及IT技术)

3

我目前正在学习面向对象编程/OOP和C++技能,并遇到了以下情况:

我正在制作一个游戏,游戏中有实体,这些实体需要进食才能生存。它们可以吃其他实体或食物。为了实现这种行为,我创建了一个 edible接口,强制每个实现该接口的类创建一个 getNutritionalInformation() 方法来计算饱食度。

因此,整个代码应该像这样工作:

std::unique_ptr<Entity> e1(new Entity);
std::unique_ptr<Entity> e2(new Entity);

std::cout << "Pre feasting energy: " << e1->getNutritionalInformation() << std::endl;
e1->eat(*e2);
std::cout << "After feasting energy: " << e1->getNutritionalInformation() << std::endl;

在此操作之后,c1的能量应该比以前高,这个值在实体创建时是随机分配的。但是为了模拟被吃掉的实体的“死亡”,我想在它被吃掉的过程中手动“杀死”它。我是通过以下方式实现的:

void Entity::eat(Edible& subject) {
   this->energy += subject.getNutritionalInformation();
   delete &subject;
}

但最终,这对我来说似乎有点不太干净。我真的希望以某种方式让智能指针知道它当前持有/指向的对象已不再有效。是否有更干净的方法实现这一点?我相信我尝试这样做的方式非常hacky,并且不被视为正确的面向对象编程。

提前感谢您的帮助。


3
为什么不对unique_ptr调用reset以清除指针并销毁它所指向的对象呢? - NathanOliver
这并不是与你的问题相关的内容,所以我将其作为评论放置在这里。但是请注意,虽然智能指针是处理堆分配的首选方式,但更好的方法是根本不使用堆。除非你有继承层次结构(这可能是个好主意,也可能不是),并且Entity是基类,否则最好创建堆栈上的局部变量而不是使用指针。 - Nir Friedman
5个回答

12

吃掉e1实际上是取得它的所有权,具体来说,必须取得它的所有权才能摧毁它。所以做法实际上是这样的:

void Entity::eat(std::unique_ptr<Edible> subject) {
    this->energy += subject->getNutritionalInformation();
}

就这样了。该主题将在作用域结束时自动销毁。要使用它,您需要显式调用std::move;这表示您正在将所有权从调用范围传递到e2中。

std::unique_ptr<Entity> e1(new Entity);
std::unique_ptr<Entity> e2(new Entity);

std::cout << "Pre feasting energy: " << e1->getNutritionalInformation() << std::endl;
e1->eat(std::move(e2));
std::cout << "After feasting energy: " << e1->getNutritionalInformation( << std::endl;

请注意,在对e2调用std::move后,就不再能假定e2实际上指向一个实体(因为所有权已经被转移)。


或者,将 std::unique_ptr 通过引用传递给 eat(),然后 eat() 可以调用 reset() 来销毁对象,而无需实际拥有它,也不需要调用者使用 std::move() 来转移所有权。void Entity::eat(std::unique_ptr<Edible> &subject) { energy += subject->getNutritionalInformation(); subject.reset(); } ... e1->eat(e2); - Remy Lebeau

1
std::unique_ptr超出作用域时,对象的析构函数会自动调用。您不需要担心它。
手动销毁对象可能会导致问题。在这种情况下,当eat()手动销毁subject对象时,拥有该对象的std::unique_ptr不知道对象已被销毁,并且在尝试第二次销毁对象时会崩溃。

1
你希望 e2 的所有者在后续操作中调用其变量的 delete 方法以避免问题。
有两种方法可以实现这一点:
e1->eat(std::move(e2));
void Entity::eat(std::unique_ptr<Edible> subject) {
    this->energy += subject->getNutritionalInformation();
}

或者,另一种选择是:
e1->eat(*e2);
e2.reset(nullptr);

无论哪种情况,您的调用者(第一个示例中的函数)都知道 e2 正在被操作销毁,在我给出的第一个实例中是因为您移出了它,在第二个实例中是因为它手动调用了 reset(这将销毁旧指针)。

0
另一种方法是有一些高级管理器来控制实体的生命周期,因此在进食期间,e2 只会执行以下操作:e1->kill(); e1->eatenEnergy(energy);(如果可能存在部分被吃掉的实体,则降低其营养价值)。
然后等待实体管理器获得轮次,以循环遍历所有实体(或响应内部 e1->kill() 实现中报告的信号,如死亡事件),并释放那些已经死亡和完全被吃掉/腐烂等的实体(杀死那些饥饿的实体等)。
这在很大程度上取决于您是否想保持简单的基于事件的世界,就像您当前的示例一样,或者您希望在世界中发生一些“生命周期”,而[内存/实例]管理集中在单个类中。
我认为集中式管理器会使事情变得有点复杂,但是使用它们可能会更容易实现某些可能性,例如更好地调试检查回合的初始/结束状态,优先处理某些实体,重用实体的内存(无动态分配)等。
在两种情况下,您都应该提前考虑一些边角情况(主要是竞态条件)将如何解决,例如:
  • e1想同时吃e2,而e2也想吃e1
  • 同时,e1和e3都想吃e2
  • e3试图在e2被e1吃掉后再吃它(e2已经“死亡”但已分配)

因此,实体的状态应该被设计成在这种情况下有效,并以期望的结果结束。


0

是的,请不要手动操作。

这是你实际上不需要关心的少数事情之一。你的程序会为你完成这个任务!

规则是:一旦变量离开作用域,它就被丢弃了(析构函数被调用)。除了动态分配的内存,你需要释放(删除)变量。

此外,如果你手动删除一个变量,你的程序会崩溃:

{
    Type variable;
    delete variable;
} //Scope end: deconstructor get's called. ERROR: already deleated, undefined behaviour, crash. everything destroyed.

很遗憾,您没有回答我的问题。我已经知道这是不好的,但我不想等待我的被吃实体超出范围。如果它被另一个实体消耗,它应该死亡并从内存中删除。否则,一个实体可能会被多次吃掉。 - user3325043

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