【问题标题】:C++ How to know when an object has been destroyedC ++如何知道对象何时被销毁
【发布时间】:2020-08-14 15:36:02
【问题描述】:

我不明白当死对象是从不同的范围创建并放置到另一个范围的某种容器中时,如何防止访问它。示例

#include <iostream>
#include <string>
#include <vector>

class Foo {
    public:
        void Speak(){
            std::cout << "ruff";    
        }
};

int main()
{
    std::vector<Foo*> foos;
    
    {
        Foo foo;
        foos.push_back(&foo);
        // foo is now dead 
    }
    
    for(size_t i = 0; i < foos.size(); i++){
        // uh oh
        foos[i]->Speak();   
    }
}

我已经尝试了大约 2 天来找出某种“共享”指针系统,它将Foo* 与另一个对象包装起来,但无论如何,它总是归结为这样一个事实,即使是“共享”指针不知道Foo 何时死亡。就好像我在寻找Foo 析构函数回调一样。

注意事项:C++ 98,无提升。

如果这个问题已经解决了 1000 次,我很想知道它背后的想法,以便我可以做出解释。


编辑:添加更多上下文

基本上我的设计中有这个反复出现的问题。我真的很喜欢“松散耦合”并将模块分开。但为了实现这一点,必须至少有一个地方可以连接它们。所以我选择发布/订阅或基于事件的系统。因此必须有发射器和处理程序。不过,这实际上只是重新包装了问题。如果ModuleA 可以发出事件并且ModuleB 可以监听事件,那么ModuleA 将不得不对ModuleB 有某种引用。这还不错,但现在我们必须考虑范围、复制 ctor、= 运算符等的所有时髦。我们别无选择,只能全力以赴。

例子

#include <iostream>
#include <string>

class Handler {
    void HandleEvent();
};

class Emitter {
public:
    // add handler to some kind of container
    void AttachHandler(Handler *handler);
    // loop through all handlers and call HandleEvent
    void EmitEvent();
};

int main()
{
    // scope A
    Emitter emitter;
    
    // scope B
    {
        Handler handler;
        emitter.AttachHandler(handler);
    }
    
    // rats..
    emitter.EmitEvent();
}

如果我们有一个名为MyObject 的东西包含一个我们想要监听的带有自己的EventEmitter 的组件(可能是一个套接字),事情会变得更糟。如果 MyObject 被复制,现在我们有这个内部 EventEmitter 和引用 MyObject 的处理程序可能不再存在!

所以也许我把它全部转储并切换到回调。但即便如此,某些对象仍然拥有 ptrs 或对可能不再存在的其他对象的引用!我们可以尽可能小心,但我们永远不知道会发生什么......

你知道,我想我需要说的是这个

  1. 任何对象都不应该有对另一个对象的引用或指针,除非它自己创建了那个对象。

现在必须通过管理这两个对象的更高级别的对象来将对象链接在一起......

...我应该坚持图形设计。

【问题讨论】:

标签: c++ c++98


【解决方案1】:

如果您希望对象的生命周期超出其创建范围,则必须使用动态生命周期创建该对象。您使用new 来执行此操作:

std::vector<Foo*> foos;

{
    Foo* foo = new Foo;
    foos.push_back(foo);
}

new 返回指向的指针的Foo 对象将一直存在,直到它显式为deleted。请注意,std::vector不会为您执行此操作。您必须显式删除存储在向量中的指针所指向的对象:

for (std::size_t i = 0; i < foos.size(); ++i) {
    delete foos[i];
}

理想情况下,您会使用某种智能指针来管理动态生命周期对象,但标准智能指针 std::unique_ptrstd::shared_ptr 不在 C++98 中。 std::auto_ptr 可用,但该类非常容易误用。可能值得编写自己的简单 shared_ptr 类来为您执行此操作。如果您不需要支持弱指针和原子操作之类的东西,这并不太复杂。这是一个非常基本的实现:

template <typename T>
class shared_ptr
{
private:
    struct control_block
    {
        control_block(T* ptr)
            : ref_count_(1),
              ptr_(ptr)
        {}
        
        ~control_block()
        {
            delete ptr_;
        }
        
        size_t ref_count_;
        T* ptr_;
    };

    control_block* control_block_;
    
public:
    shared_ptr()
        : control_block_(NULL)
    {}

    shared_ptr(T* ptr)
        : control_block_(new control_block(ptr))
    {}
    
    shared_ptr(const shared_ptr& other)
        : control_block_(other.control_block_)
    {
        ++control_block_->ref_count_;
    }
    
    shared_ptr& operator=(shared_ptr other)
    {
        std::swap(control_block_, other.control_block_);
        return *this;
    }
    
    ~shared_ptr()
    {
        if (control_block_) {
            --control_block_->ref_count_;
            if (control_block_->ref_count_ == 0) {
                delete control_block_;
            }
        }
    }
    
    T& operator*() { return *control_block_->ptr_; }
    T* operator->() { return control_block_->ptr_; }
    
    bool operator==(const shared_ptr& other)
    {
        return control_block_ == other.control_block_;
    }
};

Live Demo

基本前提是给定对象的所有shared_ptrs 都持有指向同一个control_block 的指针。每当复制shared_ptr 时,其control_block 的引用计数就会增加,而每当shared_ptr 被销毁时,其control_block 的引用计数就会减少。如果引用计数达到零,它会删除控制块以及指向的对象。

【讨论】:

  • It's not too complicated - 好的,我觉得我几乎可以写任何东西。但是有没有什么地方我可以得到共享 ptrs 的概要?他们使用什么机制?
  • @ony_pox232 添加了一个基本示例以及对前提的解释。 std::shared_ptr 有一些额外的功能和优化,但它使用相同的基本前提。
  • 天啊。我写的几乎一样。但我决定让ref_count 成为“共享”内存,因为我在堆栈上创建对象,这些对象甚至没有动态内存,所以这根本没有任何意义。我想我将停止收听 SO 上的所有内容并开始更频繁地使用new
  • @ony_pox232 你真的应该尽量避免使用new,但它确实有合法用途。只要确保您没有不必要地使用它,并记住内存分配在可维护性和性能方面都有相关成本。
  • @ony_pox232:通常的“事件中心”解决方案是有一个事件中心,每个“事件生产者”都有一个公共标头,其中包含他们可以生产的事件。当构建一个想要监听事件的对象时,它会将一个指向自身的指针添加到该事件的事件中心。当对象被破坏时,它会删除它的指针。任何时候都不会有人调用new,模块之间也不会直接相互依赖。
【解决方案2】:

在 C++ 中,您负责对象的生命周期。如果你使用

Foo foo;

foo 将在超出范围时被销毁。 所以,你不应该使用局部变量。 使用动态对象(例如):

Foo* foo = new Foo;
foos.push_back(foo);

它会起作用的。 你负责对象销毁。

【讨论】:

  • @TedLyngmo • 在std::auto_ptr 的辩护中,它确实需要Howard Hinnant 将&amp;&amp; 引用和std::move 语义添加到核心语言中以正确支持它。
  • @Eljay 哦,我不知道那些是 Howard Hinnant 的。他确实做了一些好事!我通常在想“chrono”,但移动语义是金色的。
  • @user4581301 auto_ptr 已于 10 年前弃用。我们知道使用 std::unique_ptr 确实可以很好地处理向量。
  • 是的。但是问题限制限制了现代智能指针,人们忘记了auto_ptr 被扔进垃圾箱的原因。
【解决方案3】:

您可以使用将向量赋予 foo 本身并删除析构函数中的指针。基本上,对象本身会记录指针。

#include <iostream>
#include <string>
#include <vector>

class Foo {
    std::vector <Foo *> *v;
    public:
    Foo(std::vector <Foo *> *v):  v(v){}
    void Speak(){
        std::cout << "ruff";    
    }
    ~Foo() {
        auto it = std::find(v->begin(), v->end(), this);
        if (it != v->end())
        {
            v->erase(it);
        }
    }
};

int main()
{
    std::vector<Foo*> foos;
    
    {
        Foo foo(&foos);
        foos.push_back(&foo);
        // foo is now dead 
    }
    std::cout << "length is " << foos.size() << '\n';
    for(size_t i = 0; i < foos.size(); i++){
        // uh oh
        foos[i]->Speak();   
    }
}

输出

➜  test ./a.out
length is 0

【讨论】:

    【解决方案4】:

    我想把我自己的答案放在这里,以展示我在不知不觉中试图写的东西的实现:垃圾收集。更重要的是,在已经自动管理的内存上。

    花了大约 3 天时间才明​​白。我正在创建一个节点和引用系统以使对象保持活动状态。真正意义上的垃圾回收……

    哎呀。

    【讨论】:

      【解决方案5】:

      为什么不使用std::vector&lt;Foo&gt; 而不是std::vector&lt;Foo*&gt;?在前一种方式中,我认为没有办法防止 Foo 对象在超出范围时被销毁 - 但是当您推送到 vector 时,它会一直坐在那里直到程序终止或我们显式删除。

      【讨论】:

      • 但这不一样,因为对象被复制到向量中。 Foo 可能没有复制构造函数,或者复制它可能会产生不必要的副作用。 (是的,这段代码中没有,但这只是示例代码......)另外,在它被推送之后修改foo 不会改变它在向量中的新实例。它可以与std::move 一起工作,但这不是 C++98。
      • 如果Foo 是一个非常复杂的对象怎么办?如果复制它的行为有这么多我必须维持的副作用怎么办。我是否正确地说一个容器(如vector)应该是1。只有在堆上创建对象时才具有ptrs(新)2。否则,对其对象进行100%的管理。
      • @ony_pox232 -- 你的担心与vector 无关。 Foo 的简单数组会导致您提到的相同问题。
      猜你喜欢
      • 1970-01-01
      • 2016-03-19
      • 2012-04-24
      • 2014-02-07
      • 1970-01-01
      • 2014-01-02
      • 1970-01-01
      • 2015-01-10
      • 2012-12-27
      相关资源
      最近更新 更多