【问题标题】:Design: How to avoid calling destructor?设计:如何避免调用析构函数?
【发布时间】:2014-05-14 16:43:06
【问题描述】:

我有一个Window 类,它是一些 C 结构的包装器。
该类有一个静态vector<Window*> windows_,它是一个包含已创建窗口的列表。
Window 构造函数做了两件事:

  • handle_ = SDL_CreateWindow( ... ); 基本上分配了 C 结构并将指针存储在成员变量 handle_ 中;
  • 在列表中推送this

Window 析构函数做了三件事,但前提是handle_ 不是nullptr

  • SDL_DestroyWindow() 释放 C 结构体;
  • 从列表中删除this
  • handle_ = nullptr;

然后,在我的main 中,我将Window 声明为局部变量。
当窗口接收到CLOSE 事件时,我调用该窗口的析构函数。
然后,当窗口超出范围时,再次调用窗口的析构函数,我收到分段错误。

我知道明确调用析构函数很微妙,但我真的不知道为什么。
所以问题是双重的:

为什么会崩溃?

我可以使用什么设计来避免调用析构函数?

【问题讨论】:

  • 我认为如果您可以发布代码示例会更容易为您提供帮助。
  • 您推送的this 在离开创建代码调用方的范围时将被销毁。手动触发析构函数是一个的主意。
  • 您似乎已经明白为什么显式调用对象的析构函数很少是一个好主意:当对象结束其常规生命周期时,它很可能会再次被调用。
  • 如果您希望能够在作用域结束之前将其销毁,请不要使用自动变量!
  • 关闭窗口和销毁窗口实际上是同义词吗?如果是这样,则不应关闭窗口,应将其销毁并关闭。如果不是,则关闭时不应销毁窗口,而应将其关闭并在适当的时间进行销毁。

标签: c++ destructor


【解决方案1】:

您应该发布一些代码,以便我们可以确切地看到正在发生的事情,但您是对的,您不应该手动调用析构函数,因为这会导致未定义的行为(请参阅 AliciaBytes 的回答) .相反,向类添加一个方法来关闭窗口,并使用该类为 SDL 窗口函数提供安全接口。这是一个草图:

class Window {
private:
    SDL_Window *window_;

public:
    Window() : window_(nullptr) { }
    Window(const Window &) = delete;
    Window(Window &&w) : window_(w.window_) { w.window_ = nullptr; }
    Window(const char* title, int x, int y, int w, int h, Uint32 flags)
        : window(SDL_CreateWindow(title, x, y, w, h, flags))
    { }

    ~Window()
    {
        if (window_ != nullptr)
            SDL_DestroyWindow(window_);
    }

    Window &operator=(const Window &) = delete;
    Window &operator=(Window &&w)
    {
        if (window_) { SDL_DestroyWindow(window_); window_ = nullptr; }
        window_ = w.window_;
        w.window_ = nullptr;
    }

    operator bool() const
    {
        return window_ != nullptr;
    }

    void close()
    {
        if (window_ != nullptr) {
            SDL_DestroyWindow(window_);
            window_ = nullptr;
        }
    }
};

【讨论】:

  • 所以基本上close方法和析构函数应该做同样的事情。我在 close 方法中调用 ~Window 但析构函数应该是调用 close 方法的那个。
  • 好吧,如果窗口打开的话,他们都会关闭窗口。但是close()方法将窗口指针设置为null,所以多次调用是安全的。析构函数不应该被多次调用,所以它不会费心将窗口指针设置为空。
【解决方案2】:

调用析构函数很微妙,因为它会使对象处于未定义状态。从此对象的任何读取都是未定义的。实际上,第二次调用析构函数 - 局部变量总是会发生这种情况。

我认为在您的情况下,您将列表上的操作与窗口上的操作混合在一起。

构造函数应该只稳定当前对象的不变量。在正常情况下,将它放在列表中不应该是构造函数的责任。

不要在CLOSE 上调用析构函数。释放结构,并将nullptr 放入_handler。就是这样。

【讨论】:

    【解决方案3】:

    您的代码需要保持有关您的资源是否已清理的状态。通常,您将句柄设置为 -1 或将指针设置为 nullptr:

    class MyResourceContainer {
        MyResourceContainer(int someParameter) {
            open(someParameter);
        }
    
        ~MyResourceContainer() {
            close();
        }
    
        void open(int someParameter) {
            if(handle > -1) {
                // throw exception or close(), whatever you prefer
            } 
    
            // allocate resource using specified parameter
            handle = 42;
        }
    
        // can be called multiple times
        void close() {
            if(handle > -1) {
                // release resource
                handle = -1;
            }
        }
    
    private:
        int handle = -1;
    };
    

    【讨论】:

      【解决方案4】:

      你真的不应该显式调用具有自动存储的变量的析构函数,这是未定义的行为:

      标准 12.4[class.dtor]/15

      一旦为一个对象调用析构函数,该对象就不再 存在;如果为某个对象调用析构函数,则行为未定义 生命周期已结束的对象 (3.8)。 [ 例子:如果析构函数 对于自动对象被显式调用,并且该块是 随后以通常会调用隐式的方式离开 对象的破坏,行为是未定义的。 —结束示例 ]

      与其寻找禁用自动析构函数调用的“hacky”方法或其他方式,不如寻找更好的设计。

      创建一个DestroyWindow() 成员函数,负责清理并在类中设置一个标志,但如果不是手动完成,则让析构函数检查该标志并销毁窗口(+ 其他清理)修复很多。


      为了清楚起见,您可能希望它像 fstream 和标准库中的其他流一样。您可以对它们显式调用 close ,但如果您不这样做,它会在析构函数中为您完成,以免泄漏资源。但请注意,这 不应该通过显式的析构函数调用来完成。 (例如fstream 有一个close 成员函数用于显式关闭它。)

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-03-17
        • 2013-04-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多