如何从矢量中删除对象指针而不导致内存错误

How do I delete an object pointer from a vector without causing a memory error?

本文关键字:错误 内存 指针 对象 删除      更新时间:2023-10-16

>我有一个对象指针向量,我在循环更新对象时正在添加和删除它。我似乎无法在不导致内存错误的情况下从矢量中删除已"死亡"的对象。我真的不确定我做错了什么。下面列出的是我的更新方法,它是子方法。

void Engine::update(string command){
    if(getGameOver()==false){
        for(p=objects.begin();p!=objects.end();p++){
        spawnUpdate(command);
        //cout<<"Spawn"<<endl;
        objectUpdate(command);
        //cout<<"object"<<endl;
        scrollUpdate(command);
    //  cout<<"scroll"<<endl;
        killUpdate(command);
        //cout<<"kill"<<endl;
}
}
}
void Engine::killUpdate(std::string command){
    if((*p)->getIsDead()==true){delete *p;}
}
void Engine::objectUpdate(string command){
    (*p)->update(command,getNumObjects(),getObjects());
    if(((*p)->getType() == PLAYER)&&((*p)->getPosX()>=getFinishLine())){setGameOver(true);}
}
void Engine::scrollUpdate(string command){
            //Check player position relative to finishLine
            if(((*p)->getType() == PLAYER)&&((*p)->getPosX()>(SCREEN_WIDTH/2))){
                (*p)->setPosX((*p)->getPosX()-RUN_SPEED);
                setFinishLine(getFinishLine()-RUN_SPEED);
                for(q=objects.begin();q!=objects.end();q++){
                    //Shift objects to pan the screen
                    if((*q)->getType() == OPPONENT){(*q)->setPosX((*q)->getPosX()-RUN_SPEED);}
                    if((*q)->getType() == BLOCK){(*q)->setPosX((*q)->getPosX()-RUN_SPEED);}
                }
            }
}
void Engine::spawnUpdate(string command){
    if(command.compare("shoot")==0){
        cout<<"Bang!"<<endl;
            if((*p)->getType() == PLAYER){objects.push_back(new Bullet((*p)->getPosX(),(*p)->getPosY(),(*p)->getState()));cout<<"Bullet success "<<endl;}
        }
}

一些假设/定义:

  • objects成员变量,类似于vector<Object*> objects;
  • p也是一个成员变量,类似于vector<Object*>::iterator p;

因此,p是一个迭代器,*p是一个 Object 指针,**p 是一个对象。

问题是这种方法:

void Engine::killUpdate(std::string command) {
  if ((*p)->getIsDead() == true) {
    delete *p;
  }
}

解除分配由 *p 指向的对象,该指针位于p迭代器引用的位置处的向量中。然而,指针*p本身仍在向量中,现在它只指向不再分配的内存。下次尝试使用此指针时,将导致未定义的行为,并且很可能崩溃。

因此,删除指向的对象后,您需要从矢量中删除此指针。这可以像这样简单:

void Engine::killUpdate(std::string command) {
  if ((*p)->getIsDead() == true) {
    delete *p;
    objects.erase(p);
  }
}

但是,您将在循环objects向量中从update调用killUpdate。如果使用上面的代码,您将遇到另一个问题:一旦从objects向量中删除p,在 for 循环语句中执行 p++ 就不再安全,因为p不再是有效的迭代器。

幸运的是,STL提供了一个非常好的方法来解决这个问题。 vector::erase 返回您擦除的迭代器之后的下一个有效迭代器!因此,您可以让killUpdate方法更新p而不是 for 循环语句,例如

void Engine::update(string command) {
  if (getGameOver() == false) {
    for (p = objects.begin(); p != objects.end(); /* NOTHING HERE */) {
      // ...
      killUpdate(command);
    }
  }
}
void Engine::killUpdate(std::string command) {
  if ((*p)->getIsDead() == true) {
    delete *p;
    p = objects.erase(p);
  } else {
    p++;
  }
}

这当然是假设你总是在循环中调用killUpdate,但我相信如果你不这样做,你可以看到解决这个问题的方法——只要在 for 循环主体的末尾执行 p++ 在你没有调用 killUpdate

的情况下。

另请注意,这不是特别有效,因为每次擦除矢量的元素时,其后的元素都必须移回以填充空白空间。因此,如果您的objects向量很大,这将很慢。如果您改用了std::list(或者如果您已经在使用它(,那么这不是问题,但列表还有其他缺点。

第二种方法是nullptr 覆盖指向已删除对象的每个指针,然后在循环结束时使用 std::remove_if 一次性删除它们。 例如:

void Engine::update(string command) {
  if (getGameOver() == false) {
    for (p = objects.begin(); p != objects.end(); p++) {
      // ...
      killUpdate(command);
    }
  }
  std::erase(std::remove_if(objects.begin(), objects.end(), 
                            [](const Object* o) { return o == nullptr; }), 
             objects.end());
}
void Engine::killUpdate(std::string command) {
  if ((*p)->getIsDead() == true) {
    delete *p;
    *p = nullptr;
  } 
}

这次的假设是,由于某种原因,您永远不会有想要保留的objects nullptr元素。

由于您似乎是初学者,因此我应该注意这一点:

  std::erase(std::remove_if(objects.begin(), objects.end(), 
                            [](const Object* o) { return o == nullptr; }),
             objects.end());

是擦除-删除成语,在维基百科上解释得很好。如果元素在调用给定函数对象时返回 true,它会从向量中删除元素。在这种情况下,函数对象为:

[](const Object* o) { return o == nullptr; }

这是一个 lambda 表达式,本质上是具有此类型的对象实例的简写:

class IsNull {
 public:
   bool operator() (const Object* o) const {
     return o == nullptr;
   }
};

第二种方法的最后一个警告,我刚刚注意到您在scrollUpdate中对objects另一个循环。如果选择第二种方法,请务必更新此循环以检查objects中的nullptr并跳过它们。

这是一个问题(为便于阅读而格式化(:

void Engine::update(string command)
{
    if (getGameOver()==false)
    {
        for (p=objects.begin();p!=objects.end();p++)
        {
            spawnUpdate(command);  // changes vector
//...
       }
    }
//...
}
void Engine::spawnUpdate(string command)
{
//...
    objects.push_back(new Bullet((*p)->getPosX(),(*p)->getPosY(),(*p)->getState())); // no
//...
}

您有一个带有迭代器p的循环,该循环指向object向量中的元素。 当你调用objects.push_back时,向量的迭代器可能会失效。 因此,循环迭代器p不再有任何好处。 在for()中递增它将导致未定义的行为。

解决此问题的一种方法是创建一个临时向量来保存您的更新。 然后,在处理结束时添加更新:

void Engine::update(string command)
{
    std::vector<Object*> subVector;
    if (getGameOver()==false)
    {
        for (p=objects.begin();p!=objects.end();p++)
        {
            spawnUpdate(command, subVector);
            //...
        }
    }
    // add elements to the vector
    object.insert(object.end(), subVector.begin(), subVector.end());
}
void Engine::spawnUpdate(string command, std::vector<Object*>& subV)
{
    if (command.compare("shoot")==0)
    {
        cout<<"Bang!"<<endl;
        if ((*p)->getType() == PLAYER)
            subV.push_back(new Bullet((*p)->getPosX(),(*p)->getPosY(),(*p)->getState()));
        cout<<"Bullet success "<<endl;
    }
}

您可以通过不使用原始指针来避免这些问题中的大多数。 显然,您的代码使用向量拥有指针的语义,因此您可以直接表达这一点:

std::vector< std::unique_ptr<Object> > objects;

然后您可以使用 objects.emplace_back(arguments,to,Object,constructor); 插入到向量中,当您从向量中删除时,它将自动delete对象。

您仍然需要注意erase使迭代器无效,因此请继续使用Tyler McHenry解释的擦除删除习惯用语。例如:

objects.erase( std::remove_if( begin(objects), end(objects),
    [](auto&& o) { return o->getIsDead(); }),  end(objects) );

注意 - 自 C++14 起,这里允许使用auto&&;在 C++11 中,您必须使用 std::unique_ptr<Object>& 。 必需的包括<algorithm><memory>

并且请停止使用全局迭代器,将p保留在函数的本地,并传递需要传递的任何参数。