析构函数被调用超过预期

时间:2015-10-23 16:57:15

标签: c++ c++11

我有以下代码,其中未使用new,但调用了std :: move()。

#include <iostream>
#include <vector>
#include <utility>

class Animal {
public:
    Animal() { std::cout << "Animal()" << std::endl; };
    ~Animal() { std::cout << "~Animal()" << std::endl; };
    virtual void eat() { std::cout << "Animal is eating" << std::endl; };
};

class Bear : public Animal {
public:
    Bear() { std::cout << "Bear()" << std::endl; };
    ~Bear() { std::cout << "~Bear()" << std::endl; };
    void eat() { std::cout << "Bear is eating" << std::endl; };
};

class Zebra : public Animal {
public:
    Zebra() { std::cout << "Zebra()" << std::endl; };
    ~Zebra() { std::cout << "~Zebra()" << std::endl; };
    void eat() { std::cout << "Zebra is eating" << std::endl; };
};

int main()
{
    Bear bear;
    Zebra zebra;

    std::vector<Animal*> animalsp;
    animalsp.push_back(&bear);
    animalsp.push_back(&zebra);
    for (auto i : animalsp) {
        i->eat();
    }

    std::vector<Animal> animals;
    animals.push_back(std::move(bear));
    animals.push_back(std::move(zebra));
    for (auto i : animals) {
        i.eat();
    }

    return 0;
}

我得到以下输出:

Animal()
Bear()
Animal()
Zebra()
Bear is eating
Zebra is eating
~Animal()
Animal is eating
~Animal()
Animal is eating
~Animal()
~Animal()
~Animal()
~Zebra()
~Animal()
~Bear()
~Animal()

我原本期望对~Animal()析构函数的调用次数减少。我还发现调用~Animal()的时间是出乎意料的。我希望std::move()调用可能会触发这个,但是在函数调用输出之前这样做。我在ideone和VS2015上得到了相同的输出。我错过了什么?

4 个答案:

答案 0 :(得分:4)

移动并不会破坏对象,它只会将其状态标记为可丢弃。所以这一举动不会造成破坏(除非通过推回重新分配向量!)

自动存储对象(比如在函数中声明为locals)在其作用域的末尾被销毁。这就是为什么你最终得到所有这些析构函数的原因。

当矢量被摧毁时,它会摧毁存储在其中的动物(这些动物不是熊或斑马,只是动物,因为你要求它们按值存储,这意味着熊被切掉了) 。这是析构函数的另一个来源。

for (auto i:animals)循环还会在向量中创建每个动物的副本,并将其销毁。更多的析构垃圾邮件。

Animal()
Bear() // Bear bear; line
Animal()
Zebra() // Zebra zebra; line
Bear is eating
Zebra is eating // first loop
~Animal() // reallocation of vector during push_backs
Animal is eating // second loop body
~Animal() // first i copy destroyed in second loop
Animal is eating // second loop body
~Animal() // second i copy destroyed in second loop 
~Animal()
~Animal() // vector destroyed 
~Zebra()
~Animal() // zerbra destroyed
~Bear()
~Animal() // bear destroyed

如果事情需要2行,我评论了第2行。

答案 1 :(得分:2)

for (auto i : animals)正在创建向量中的对象副本。这可能是您看到的许多析构函数调用的来源。在这种情况下,通常最好通过引用传递,如:

for (auto& i : animals)

答案 2 :(得分:1)

即使使用移动语义,您也有多个在该范围结束时被销毁的对象。移动语义的实现是这些对象的内部实现。

示例:

#include <iostream>
class Resource
{
    public:
    int data =  0;
    Resource()
    {
        std::cout << "Allocate resource\n";
        data = 1;
    }

    Resource(Resource&& other)
    :   data(other.data)
    {
        std::cout << "Transfer resource\n";
        other.data = 0;
    }

    Resource& operator = (Resource&& other)
    {
        std::cout << "Transfer resource\n";
        data = other.data;
        other.data = 0;
        return *this;
    }

    Resource(const Resource&) = delete;
    Resource& operator = (const Resource&) = delete;

    ~Resource()
    {
        if(data)
            std::cout << "Deallocate resource\n";
    }
};

int main(int, char**) {
    Resource a;
    Resource b(std::move(a));
    return 0;
    // Destruction of a and b follows.
}

注意:持有数据的对象不是移动语义的关注点,而是数据本身。

输出:

Allocate resource
Transfer resource
Deallocate resource

答案 3 :(得分:1)

很容易误解std :: move和rvalue引用的实际机制。使用右值引用构造对象时,预期(但绝不是契约)行为是接收构造函数将控制引用持有的任何动态内存,然后更改引用对象以反映它的事实不再控制任何动态内存。

这绝不意味着传入的右值将立即被丢弃。无论是否移动了析构函数,它仍然会被调用。关键的区别在于析构函数将能够检测到对象已被移动,因此没有调用释放内存。

当然,C ++是一种非常强大的语言,因此受到滥用。您可以在rvalue引用构造函数或析构函数中执行任何操作。这只是最常见的模式,因为malloc和free非常昂贵,所以尽可能少地调用它们就是好的设计。