std::list::remove 方法是否调用每个已删除元素的析构函数?

Does std::list::remove method call destructor of each removed element?

提问人:Siarhei Fedartsou 提问时间:11/24/2010 最后编辑:KevinSiarhei Fedartsou 更新时间:9/23/2019 访问量:25758

问:

我有代码:

std::list<Node *> lst;
//....
Node * node = /* get from somewhere pointer on my node */;
lst.remove(node);

该方法是否调用每个已删除元素的析构函数(和释放内存)?如果是这样,我该如何避免它?std::list::remove

C 列表 STL 析构函数 C++-FAQ

评论

2赞 GManNickG 11/24/2010
为什么要首先存储指针?

答:

0赞 wkl 11/24/2010 #1

由于您要将指针放入 中,因此不会在指向对象上调用析构函数。std::listNode

如果要将堆分配的对象存储在 STL 容器中,并在删除时销毁它们,请将它们包装在智能指针中,例如boost::shared_ptr

12赞 John Dibling 11/24/2010 #2

它调用 -- 中每个项的析构函数,但这不是一个对象。它是一个.listNodeNode*

因此,它不会删除指针。Node

这有意义吗?

评论

0赞 Siarhei Fedartsou 11/24/2010
但是如果我在删除调用后尝试删除节点,我会遇到“隔离错误”。:(为什么会这样?
0赞 PHcoDer 4/11/2017
@JohnDibling 很明显,当对象的原始指针从容器中删除时,该对象不会被删除。但是它会调用对象的破坏或吗?我认为情况不应该是这样。但是文档说,对于要删除的 map 元素,会调用析构函数。
7赞 jpalecek 11/24/2010 #3

它确实调用列表中数据的析构函数。这意味着,将调用析构函数(当类似 时是必要的)。std::list<T>::removeTTstd::vector

在您的例子中,它将调用 的析构函数,这是一个空操作。它不调用 的析构函数。Node*node

评论

1赞 aschepler 11/24/2010
是的。为了更加清楚起见,显示的代码示例既不调用指针,也不调用指针。(尽管如果您在列表中有一个 to 那个位置,它现在无效。Node::~Nodedeletenodeiterator
3赞 Edward Strange 11/24/2010 #4

是的,尽管在本例中,Node* 没有析构函数。但是,根据其内部结构,各种 Node* 值会被范围规则删除或销毁。如果 Node* 属于某种非基本类型,则将调用析构函数。

是否在节点上调用析构函数?否,但“节点”不是列表中的元素类型。

至于你的另一个问题,你不能。标准列表容器(实际上是所有标准容器)采用其内容的所有权并对其进行清理。如果您不希望这种情况发生,标准容器不是一个好的选择。

48赞 fredoverflow 11/24/2010 #5

是的,从容器中删除 会破坏 ,但不会释放 .销毁原始指针始终是空操作。它不可能是任何其他方式!让我给你几个原因。Foo*Foo*Foo

存储类

只有当指针实际上是动态分配的时,删除指针才有意义,但是当指针变量被销毁时,运行时怎么可能知道是否是这种情况呢?指针还可以指向静态变量和自动变量,删除其中一个变量会产生未定义的行为

{
    Foo x;
    Foo* p = &x;

    Foo* q = new Foo;

    // Has *q been allocated dynamically?
    // (The answer is YES, but the runtime doesn't know that.)

    // Has *p been allocated dynamically?
    // (The answer is NO, but the runtime doesn't know that.)
}

悬空指针

无法确定指针过去是否已经发布过。删除同一指针两次会产生未定义的行为。(第一次删除后,它将成为悬空指针

{
    Foo* p = new Foo;

    Foo* q = p;

    // Has *q already been released?
    // (The answer is NO, but the runtime doesn't know that.)

    // (...suppose that pointees WOULD be automatically released...)

    // Has *p already been released?
    // (The answer WOULD now be YES, but the runtime doesn't know that.)
}

未初始化的指针

也无法检测指针变量是否已初始化。猜猜当您尝试删除此类指针时会发生什么?再一次,答案是未定义的行为

    {
        Foo* p;

        // Has p been properly initialized?
        // (The answer is NO, but the runtime doesn't know that.)
    }

动态数组

类型系统不区分指向单个对象 () 的指针和指向对象数组的第一个元素的指针 (also )。当指针变量被销毁时,运行时无法确定是通过还是通过 释放指针。通过错误的形式发布会调用未定义的行为Foo*Foo*deletedelete[]

{
    Foo* p = new Foo;

    Foo* q = new Foo[100];

    // What should I do, delete q or delete[] q?
    // (The answer is delete[] q, but the runtime doesn't know that.)

    // What should I do, delete p or delete[] p?
    // (The answer is delete p, but the runtime doesn't know that.)
}

总结

由于运行时无法对指针执行任何有意义的操作,因此销毁指针变量始终是空操作。什么都不做绝对比由于不知情的猜测而导致未定义的行为要好:-)

建议

请考虑使用智能指针作为容器的值类型,而不是原始指针,因为它们负责在不再需要指针时释放指针。根据您的需要,使用或 .如果您的编译器尚不支持 C++0x,请使用 .std::shared_ptr<Foo>std::unique_ptr<Foo>boost::shared_ptr<Foo>

再说一遍,永远不要用作容器的值类型。std::auto_ptr<Foo>

0赞 kmcguire 8/18/2016 #6

最好的理解方法是测试每个表单并观察结果。若要巧妙地将容器对象与自己的自定义对象一起使用,需要对行为有很好的理解。

简而言之,对于该类型,既不调用解构函数,也不调用 delete/free;但是,对于将调用解构函数的类型,当考虑 delete/free 是 list 的实现细节时,将调用解构函数。这意味着,这取决于列表实现是否使用了 new/malloc。Node*Node

在 的情况下,将调用解构函数,并且将调用 delete/free,因为您必须为其提供由 分配的东西。unique_ptr<Node>new

#include <iostream>
#include <list>
#include <memory>

using namespace std;

void* operator new(size_t size) {
    cout << "new operator with size " << size << endl;
    return malloc(size);
}

void operator delete(void *ptr) {
    cout << "delete operator for " << ptr << endl;
    free(ptr);
}

class Apple {
public:
    int id;

    Apple() : id(0) { cout << "apple " << this << ":" << this->id << " constructed" << endl; } 
    Apple(int id) : id(id) { cout << "apple " << this << ":" << this->id << " constructed" << endl; }
    ~Apple() { cout << "apple " << this << ":" << this->id << " deconstructed" << endl; }

    bool operator==(const Apple &right) {
        return this->id == right.id;
    }

    static void* operator new(size_t size) {
        cout << "new was called for Apple" << endl;
        return malloc(size);
    }

    static void operator delete(void *ptr) {
        cout << "delete was called for Apple" << endl;
        free(ptr);
    }
    /*
        The compiler generates one of these and simply assignments
        member variable. Think memcpy. It can be disabled by uncommenting
        the below requiring the usage of std::move or one can be implemented.
    */
    //Apple& operator=(const Apple &from) = delete;
};

int main() {
    list<Apple*> a = list<Apple*>();

    /* deconstructor not called */
    /* memory not released using delete */
    cout << "test 1" << endl;
    a.push_back(new Apple());
    a.pop_back();

    /* deconstructor not called */
    /* memory not released using delete */
    cout << "test 2" << endl;
    Apple *b = new Apple();
    a.push_back(b);
    a.remove(b);
    cout << "list size is now " << a.size() << endl;

    list<Apple> c = list<Apple>();      
    cout << "test 3" << endl;
    c.push_back(Apple(1)); /* deconstructed after copy by value (memcpy like) */
    c.push_back(Apple(2)); /* deconstructed after copy by value (memcpy like) */

    /*
       the list implementation will call new... but not
       call constructor when Apple(2) is pushed; however,
       delete will be called; since it was copied by value
       in the last push_back call

       double deconstructor on object with same data
    */
    c.pop_back();

    Apple z(10);

    /* will remove nothing */
    c.remove(z);

    cout << "test 4" << endl;

    /* Apple(5) will never deconstruct. It was literally overwritten by Apple(1). */
    /* Think memcpy... but not exactly. */
    z = Apple(1);

    /* will remove by matching using the operator== of Apple or default operator== */
    c.remove(z);

    cout << "test 5" << endl;
    list<unique_ptr<Apple>> d = list<unique_ptr<Apple>>();
    d.push_back(unique_ptr<Apple>(new Apple()));
    d.pop_back();

    /* z deconstructs */
    return 0;
}

请特别注意内存地址。您可以通过范围来判断哪些指向堆栈,哪些指向堆。