【问题标题】:C++ Destructor is being called when I don't expect it to be当我不希望它被调用时,正在调用 C++ 析构函数
【发布时间】:2019-11-06 18:11:58
【问题描述】:

我在一些代码中添加了一个析构函数,它似乎提前调用并导致了问题。我添加了一个调试语句来查看它在哪里被调用,这让我更加困惑。我知道管理自己的记忆不是最佳做法,但我想自己尝试一下。

这基本上是我的 GameObject 类:

class GameObject
{
public:
int             xCoord = 0, yCoord = 0, prevX, prevY;
int             status = 0, weight = -1;
int             id = -1;

GameObject(CommandComponent* commands,
    PhysicsComponent* physics,
    GraphicsComponent* graphics)
    : commandComponent(commands),
    physicsComponent(physics),
    graphicsComponent(graphics)
{};

~GameObject()
{
    std::cout << "Destructor called " << id << std::endl;
    delete commandComponent;
    delete physicsComponent;
    delete graphicsComponent;
};

void update(World& world, int command, sf::Time dt)
{
    commandComponent->update(*this, world, command);
    physicsComponent->update(*this, world);
    graphicsComponent->update(*this, dt);
};

void update(World& world, int command)
{
    commandComponent->update(*this, world, command);
    physicsComponent->update(*this, world);
};

sf::Sprite draw()
{
    return *(graphicsComponent->draw());
};

void setCoords(int x, int y)
{
    prevX = xCoord;
    xCoord = x;
    prevY = yCoord;
    yCoord = y;
};

void setId(int newId)
{
    id = newId;
}


private:
    CommandComponent*           commandComponent    = NULL;
    GraphicsComponent*          graphicsComponent   = NULL;
    PhysicsComponent*           physicsComponent    = NULL;

};

这是 createPlayer 方法:

    GameObject* createPlayer(sf::Texture* text)
{
    return new GameObject(new PlayerCommandComponent(), new PlayerPhysicsComponent(), new PlayerGraphicsComponent(text));
};

这是我调用的一种方法,根据它是活动对象还是非活动对象,将新对象添加到向量中,我还将它添加到数组中:

void World::addObject(GameObject object, int id, int type){
object.setId(id);

if (type == 0)
{
    inactiveObjects.push_back(object);
}
else if (type == 1)
{
    activeObjects.push_back(object);
}
}

最后,这是我创建游戏对象并调用上述函数的测试代码,以及我看到从中调用析构函数的位置:

void World::test()
{
// Player
std::cout << "Starting to create id 0\n";
addObject((*createPlayer(&(mTextures.get(Textures::PlayerCharacter)))), 0, 1);
activeObjects.at(0).setCoords(3, 3);
activeObjects.at(0).weight = 10;
std::cout << "Created id 0\n";

// Test Objects
std::cout << "Starting to create id 1\n";
addObject((*createPlayer(&(mTextures.get(Textures::PlayerCharacter)))), 1, 1);
activeObjects.at(1).setCoords(3, 4);
activeObjects.at(1).weight = 7;
std::cout << "Created id 1\n";

addObject((*createPlayer(&(mTextures.get(Textures::PlayerCharacter)))), 2, 1);
activeObjects.at(2).setCoords(5, 4);
activeObjects.at(2).weight = 2;

addObject((*createPlayer(&(mTextures.get(Textures::Enemy)))), 3, 1);
activeObjects.at(3).setCoords(6, 6);
activeObjects.at(3).weight = -1;

addObject((*createPlayer(&(mTextures.get(Textures::Enemy)))), 4, 1);
activeObjects.at(4).setCoords(1, 1);
activeObjects.at(4).weight = 0;

std::cout << "Done Creating Test Objects\n";

我想我的主要问题是如何调用析构函数?我假设它与我在 createPlayer 方法中构造对象的方式有关,也许在我返回它后它超出了范围,但我认为使用 new 关键字可以防止这种情况发生?我在这里感到困惑。

【问题讨论】:

  • 将此添加到您的班级:GameObject(GameObject const&amp;) = delete;
  • 上述内容的附录:@Eljay 向您展示了如何找到您的课程的意外副本。你可以认为这是对What is the Rule of Three?的介绍
  • 考虑使用nullptr而不是C的NULL
  • 请不要发布文本的截图 - 发布文本本身。此外,您确实需要了解和使用智能指针,而不是手动释放内存。
  • 这就是为什么在您知道所有特殊成员函数的作用以及调用它们的时间之前,您不会开始进行手动管理。

标签: c++ destructor


【解决方案1】:

这里有几件事在起作用。

GameObject* createPlayer(sf::Texture* text)

返回一个动态分配的GameObjectThis could be done better,请继续阅读 std::unique_ptr,但这里没有任何错误。我提到它主要是为了指出std::unique_ptr并设置

addObject((*createPlayer(&(mTextures.get(Textures::PlayerCharacter)))), 0, 1);

因为这是事情开始出错的地方。当您发现使用new 并取消引用并丢弃结果的代码时,您正在查看内存泄漏。您丢失了指向动态分配对象的指针,并且没有指针几乎不可能再次找到分配,因此您可以delete 它。

存储取消引用的对象将调用复制构造函数或赋值运算符,此时您需要考虑The Rule of Three:如果您需要定义自定义析构函数,您可能需要定义自定义赋值运算符和复制构造函数。这是您何时需要观察Rule of Three 的标准示例。 Rule of Three 链接中已经很好地解释了哪里出了问题,所以在继续之前停下来阅读并理解它。不这样做意味着这个答案的其余部分对你来说几乎是无用的。

如果不牢牢抓住the Rule of Three and all of its friends,您就无法编写出色的、重要的 C++ 代码。

你可以通过改变来绕过三法则

void World::addObject(GameObject object, int id, int type)

void World::addObject(GameObject * object, int id, int type)

并通过引用传递object。这没有多大帮助,因为

inactiveObjects.push_back(object);

期待一个对象,而不是一个指针。

你也可以改变它,但你应该吗? std::vector 在直接包含对象时处于绝对最佳状态。指针会导致指针追逐、糟糕的缓存行为,最终导致suuuhhhfering。除非您有令人信服的理由,否则不要存储指针。

如果你这样做了,用 std::unique_ptr 从头到尾管理指针。

我会做什么:

直接跳过三法则并转到The Rule of Five

  1. 尽可能多地消除动态分配的变量,这样我就不需要对第 2 点做太多工作(如果有的话)。这意味着没有指向(或指向)commandComponentphysicsComponentgraphicsComponent 的指针如果可能的话。
  2. 添加移动构造函数和移动赋值运算符到GameObject 以及CommandComponentPhysicsComponentGraphicsComponent。使所有资源管理尽可能靠近资源。这使您可以使更高级别的课程尽可能无知。如果GraphicsComponent 知道如何复制和移动自己,GameObject 不需要知道如何移动它。这使您可以利用The Rule of Zero,并且零规则应该是您在所有课程中努力的目标。
  3. 使用移动语义来获取GameObject,而不是从createPlayeractiveObjectsinactiveObjectsvectors 的GameObject*
  4. 享受减少的内存管理负载。

【讨论】:

  • 感谢您的所有帮助我想我明白现在发生了什么。我在创建新游戏对象的调用中分配内存,当它返回新游戏对象时,它调用复制构造函数,但由于我没有指定复制构造函数,它本质上只是执行数据的浅拷贝,包括一个拷贝指向已分配内存的指针。但是指针的新实例指向创建的对象的内存,然后在析构函数中取消分配,因为它超出了范围。
  • @CodySavage 这不是从createPlayer 返回的GameObject,因为那是一个指针。当您获取该指针、取消引用它并将其按值传递给addObject 时,(第一个)副本就完成了。更多的副本可以跟随将副本放入vector 并且任何时候vector 调整大小、随机播放、删除......与复制问题相比,这是次要的,但不要忘记丢弃没有deleteing 的指针。每当任何副本超出范围时,都会发生意外破坏。
猜你喜欢
  • 2023-03-10
  • 1970-01-01
  • 1970-01-01
  • 2017-11-18
  • 1970-01-01
  • 2012-09-02
  • 2013-08-08
  • 1970-01-01
相关资源
最近更新 更多