【问题标题】:C++: Why is the destructor being called here?C++:为什么在这里调用析构函数?
【发布时间】:2010-07-10 01:27:25
【问题描述】:

我想我并不完全理解析构函数在 C++ 中是如何工作的。这是我为重现问题而编写的示例程序:

#include <iostream>
#include <memory>
#include <vector>

using namespace std;

struct Odp
{
    int id;

    Odp(int id)
    {
        this->id = id;
    }

    ~Odp()
    {
        cout << "Destructing Odp " << id << endl;
    }
};

typedef vector<shared_ptr<Odp>> OdpVec;

bool findOdpWithID(int id, shared_ptr<Odp> shpoutOdp, OdpVec& vec)
{
    shpoutOdp.reset();

    for (OdpVec::iterator iter = vec.begin(); iter < vec.end(); iter++)
    {
        Odp& odp = *(iter->get());
        if (odp.id == id)
        {
            shpoutOdp.reset(iter->get());
            return true;
        }
    }

    return false;
}

int main()
{
    OdpVec vec;

    vec.push_back(shared_ptr<Odp>(new Odp(0)));
    vec.push_back(shared_ptr<Odp>(new Odp(1)));
    vec.push_back(shared_ptr<Odp>(new Odp(2)));

    shared_ptr<Odp> shOdp;
    bool found = findOdpWithID(0, shOdp, vec);
    found = findOdpWithID(1, shOdp, vec);
}

就在main()结束之前,这个程序的输出是:

Destructing Odp 0
Destructing Odp 1

为什么会这样?我保留了对向量中每个 Odp 实例的引用。它与通过引用传递shared_ptr 有关吗?

更新我认为shared_ptr::reset 减少了引用计数,基于MSDN

运算符都减 资源的引用计数 当前由 *this 拥有

但也许我误解了它?

更新 2:看起来这个版本的 findOdpWithID() 不会导致调用析构函数:

bool findOdpWithID(int id, shared_ptr<Odp> shpoutOdp, OdpVec& vec)
{
    for (OdpVec::iterator iter = vec.begin(); iter < vec.end(); iter++)
    {
        Odp& odp = *(iter->get());
        if (odp.id == id)
        {
            shpoutOdp = *iter;
            return true;
        }
    }

    return false;
}

【问题讨论】:

  • 您在下面的评论中提到您正在尝试减少引用计数。你不应该担心这一点。共享指针的工作是担心引用计数等实现细节。你应该使用它。您可以将 shared_ptr 想象成 java 指针,它在幕后完成您需要的所有工作。
  • 这不是一个好问题。你提到了一个“问题”,但没有告诉我们它是什么,并抱怨你的程序的输出,但没有告诉我们你的期望。我们无法预测您在脑海中所期望的行为,尤其是实际行为——本质上是正确的 C++ 行为——是我们所期望的!

标签: c++ destructor


【解决方案1】:

这里的这条线可能是让你绊倒的原因。

shpoutOdp.reset(iter->get());

您在这里所做的是(通过get())从智能指针中获取裸指针,智能指针上不会有任何引用跟踪信息,然后告诉shpoutOdp 重置自身以指向裸指针指针。当shpoutOdp 被破坏时,它不知道还有另一个shared_ptr 指向同一事物,而shpoutOdp 继续破坏它所指向的事物。

你应该这样做

shpoutOdp = *iter;

这将正确地维护引用计数。顺便说一句,reset() 确实会减少引用计数器(并且只有在计数达到 0 时才会销毁)。

【讨论】:

  • 但是如果我在做shpoutOdp = *iter,我就不需要使用reset(),因为我的引用计数会自动递减,对吧?
【解决方案2】:

这么多东西几乎都被正确使用了:

bool findOdpWithID(int id, shared_ptr<Odp> shpoutOdp, OdpVec& vec)

这里的参数 shpoutOdp 是输入参数的一个副本。考虑到它是一个共享指针,这没什么大不了的,但这可能不是您想要的。您可能想通过引用传递,否则为什么首先将其传递给函数。

shpoutOdp.reset();

在传入参数时重置参数。
这是否意味着它可能是脏的(那么为什么将它作为输入参数)它会使函数返回一个共享指针作为结果,如果你想传递一些东西。

Odp& odp = *(iter->get());

除非你真的需要,否则不要在共享指针上使用 get(如果你真的需要的话)。提取指针对于获取指针指向的内容不是必需的,并且由于您正在处理指针,因此您更容易出错。等效的 safe(r) 行是:

Odp& odp = *(*iter); // The first * gets a reference to the shared pointer.
                     // The second star gets a reference to what the shared 
                     //pointer is pointing at

这就是问题所在:

shpoutOdp.reset(iter->get());

您正在从一个指针创建一个新的共享指针。不幸的是,该指针已经由另一个共享指针管理。因此,现在您有两个共享指针,它们认为它们拥有该指针,并在它们超出范围时将其删除(第一个在函数末尾超出范围,因为它是输入参数的副本(而不是比参考))。正确的做法是做一个作业。然后共享指针就知道它们共享一个指针:

shpoutOdp = *iter; // * converts the iterator into a shared pointer reference

下一行虽然不是完全错误,但确实假设使用的迭代器是随机访问的(对于向量来说也是如此)。

for (OdpVec::iterator iter = vec.begin(); iter < vec.end(); iter++)

但这会使代码更加脆弱,因为 typedef OdpVec 中的简单更改会在没有任何警告的情况下破坏代码。因此,为了使其与正常的迭代器使用更加一致,请在检查 end() 时使用 != 并且更喜欢预增量运算符:

for (OdpVec::iterator iter = vec.begin(); iter != vec.end(); ++iter)

【讨论】:

【解决方案3】:

shared_ptr::reset 会破坏 shared_ptr 中已有的内容。 如果您只想影响单个 shared_ptr 引用,只需分配给它即可。

编辑:作为对评论的回应,您可以通过将 for 循环的主体更改为:

if ((*iter)->id == id)
{
    shpoutOdp = *iter;
    return true;
}

EDIT2:说了这么多,你为什么不在这里使用 std::find_if 呢?

#include <iostream>
#include <memory>
#include <vector>
#include <algorithm> //for std::find_if
#include <functional> //for std::bind

struct Odp
{
    int id;

    int GetId()
    {
        return id;
    }

    Odp(int id)
    {
        this->id = id;
    }

    ~Odp()
    {
        std::cout << "Destructing Odp " << id << std::endl;
    }
};

typedef std::vector<shared_ptr<Odp> > OdpVec;

int main()
{
    OdpVec vec;

    vec.push_back(std::shared_ptr<Odp>(new Odp(0)));
    vec.push_back(std::shared_ptr<Odp>(new Odp(1)));
    vec.push_back(std::shared_ptr<Odp>(new Odp(2)));

    OdpVec::iterator foundOdp = std::find_if(vec.begin(), vec.end(), 
        std::bind(std::equal_to<int>(), 0, std::bind(&Odp::GetId,_1)));
    bool found = foundOdp != vec.end();
}

【讨论】:

  • 我只是想减少引用计数。我该怎么做?
  • @Rosarch:让旧的 shared_ptr 被销毁。如果你愿意,你可以给它分配一个空的 shared_ptr。
  • 说实话,我发现find_if 不太明显。任何人都可以查看 for 循环并弄清楚发生了什么,但不熟悉 STL 的人会想知道所有这些 std::bind 噪音是什么。
  • @Rosarch:也许吧。但是任何熟悉 STL 的人都会很高兴看到find,因为他们知道他们正在寻找某些东西,而不必花几分钟试图找出循环是什么正在做。当然,STL 具有很高的学习曲线,但是一旦你知道发生了什么,它就会比无数循环更容易阅读。更不用说可以从 STL 算法中获得的性能优势。一般来说,人们应该更喜欢算法调用而不是显式循环。
  • @Rosarch:很容易看出 for 循环 意味着 做什么。但要了解它实际做了什么可能会很棘手。它可能包含细微的错误。 std::find 具有我们知道它有效的优势。我们知道它会按照包装盒上的说明进行操作。
【解决方案4】:

shared_ptr 的好处在于它在内部处理引用计数。您不需要手动增加或减少它永远。 (这就是为什么shared_ptr 也不允许你这样做)

当您调用reset 时,它只是将当前shared_ptr 设置为指向另一个对象(或null)。这意味着现在对它在reset 之前指向的对象的引用减少了,所以从这个意义上说,引用计数器已经递减。但它不是你应该调用来减少 ref 计数器的函数。

您永远不需要这样做。只要让shared_ptr 超出范围,它就会负责递减引用计数。

这是RAII 的示例。

您需要管理的资源(在这种情况下是shared_ptr 指向的对象)绑定到堆栈分配的对象(shared_ptr 本身),因此它的生命周期是自动管理的。 shared_ptr 的析构函数确保指向的对象在适当的时候被释放。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-07-06
    • 2015-11-04
    • 2017-04-07
    • 1970-01-01
    • 1970-01-01
    • 2013-10-01
    • 1970-01-01
    • 2015-08-13
    相关资源
    最近更新 更多