关于C++中悬挂指针的一些思考,以及从QPointer中能够借鉴

2023-04-24  本文已影响0人  饮茶先啦靓仔

如果是C++程序员,应该对悬挂指针这种pain in the ass十分熟悉了。为了避免悬挂指针问题,一般有两种解决思路:

  1. 在delete指针时,必须将指针置空。后续使用时,必须对指针进行判空。如果设计多线程设计,可能要配合临界区/互斥锁等同步原语。
  2. 使用std::shared_ptrstd::unique_ptr等智能指针设施,其他类库提供的支持RAII的指针类也可以。

严格遵守上述两条实践准则,能解决99%的悬挂指针问题。那1%是怎么回事呢?这就是本文想要讨论的问题了。以我个人的理解来说,如果出现悬挂指针问题,首先还是要关注这两点实践准则,是否有严格遵守。但仍没有头绪,可能是以下两种情况:

  1. 类内部调用了delete this,或者类似于delete this的行为。这里说到的类似于delete this的行为,在Qt开发中,一般可以表现为:为QWidget设置了deleteOnClose属性(即setAttribute(Qt::WA_DeleteOnClose)),而后在类的内部调用了close()。这会让窗口销毁,并调用析构函数,因此这种行为本质上也是delete this
  2. 虽然使用了智能指针,但仍直接delete裸指针,或者从std::shared_ptrstd::unique_ptr中调用get()方法获取了裸指针,并错误地将其delete。

以上两种情况,绝大多数的支持RAII的指针类都是感知不到的,包括C++11提供的std::shared_ptrstd::unique_ptr。因此,仍存在悬挂指针问题,存在造成崩溃的风险。

使用Qt中提供的QPointer类,并让被管理的资源继承自QObject,能够解决此类问题,这主要是为了处理QWidget设置了deleteOnClose属性(即setAttribute(Qt::WA_DeleteOnClose)),而后在类的内部调用了close()的情况。QPointer类能够感知到被管理资源的销毁,从而自动地将自身置空,因而能够规避悬挂指针问题。原理也很简单,Qt中提供了信号槽机制,QPointer通过连接QObjectdestroy信号,自然就能感知到被管理资源的销毁,从而将自身置空了。这里要注意,被管理的指针类必须继承自QObject,并使用Q_OBJECT宏,否则是不生效的。简单的示例如下:

QPointer<QPushButton> button(new QPushButton("Close"));
button->setAttribute(Qt::WA_DeleteOnClose);
button->show();

QObject::connect(button, &QPushButton::clicked, [&button]() {
    if (button) {
        button->close();
        qDebug() << "Button closed";
    }
});

//处理其他的逻辑....
//这期间,用户按下了按钮,这会导致按钮销毁,QPointer自动置空

//由于QPointer已经置空,所以不会导致访问悬挂指针。
//如果使用裸指针,或者C++11的智能指针,那就GG了
if (button)
{
    button->setFixedSize(100, 20);
}

那么,如果不在Qt环境下,如何能够避免此类问题呢?Qt的信号槽机制,实际上是类似与设计模式中的观察者模式的。所以,在必要的场合下,我们可以使用C++11中的智能指针,并配合观察者模式,避免悬挂指针问题。示例如下:


#include <iostream>
#include <memory>
#include <set>

//发起订阅者
class Observer {
public:
    virtual void onObjectDestroyed() = 0;
};

//被订阅的主题,特定事件下会发布消息通知到订阅者
//这个场景下,当被订阅的主题销毁时,即发布消息
class Observed {
public:
    void addObserver(std::weak_ptr<Observer> observer) {
        observers.insert(observer);
    }

    void removeObserver(std::weak_ptr<Observer> observer) {
        observers.erase(observer);
    }

protected:
    virtual ~Observed() {
        notifyObservers();
    }

private:
    void notifyObservers() {
        for (auto& observer : observers) {
            if (auto lockedObserver = observer.lock()) {
                lockedObserver->onObjectDestroyed();
            }
        }
    }

    std::set<std::weak_ptr<Observer>, std::owner_less<std::weak_ptr<Observer>>> observers;
};

class ManagingPointer : public Observer, public std::enable_shared_from_this<ManagingPointer> {
public:
    ManagingPointer(std::shared_ptr<Observed> object)
        : object(object) {
        object->addObserver(shared_from_this());
    }

    ~ManagingPointer() {
        if (object) {
            object->removeObserver(shared_from_this());
        }
    }

    void onObjectDestroyed() override {
        object.reset();
    }

private:
    std::shared_ptr<Observed> object;
};

class ManagedObject : public Observed {
public:
    void release() {
        delete this;
    }
};

int main() {
    auto managedObject = std::make_shared<ManagedObject>();
    auto managingPointer = std::make_shared<ManagingPointer>(managedObject);

    managedObject->release();

    return 0;
}

在这里,被订阅的主题类是Observed,订阅主题的观察者是Observer。这里需要被管理的资源ManagedObject继承自Observed;管理资源的指针类是ManagingPointer,继承自Observer
由于使用了观察者模式,当managedObject对象调用release方法将自身销毁时,父类Observed的虚析构函数会被调用,并通知订阅主题的观察者Observer。由于ManagingPointer继承自Observer,因此当managedObject调用release函数,ManagingPointer会感知到,即onObjectDestroyed函数会被调用(注意Observed类的notifyObservers方法,会通知被订阅的主题Observer,而当Observed类析构时会调用notifyObservers方法,需要理清这个调用关系)。最后,ManagingPointer的成员std::shared_ptr<Observed> object会在onObjectDestroyed中被reset,从而自动将自身自动置空。

可以看到,这一个流程下来,还是有点麻烦的,如果对观察者模式不是很熟悉,会需要一点时间理清调用关系。并且可以看到ManagingPointer类的使用并不是很方便,并不如C++11的智能指针好用,如果上述示例代码要被使用,或许还需要重写许多操作符函数,才能投入使用。

总结

可以看到,为了规避delete this以及delete裸指针带来的悬挂指针风险,实际上是有成本的,包括实现一个更复杂的且和C++智能指针一样好用且稳定的,支持RAII的指针类;以及更高的运行期性能消耗。这两点都是不可忽视的。这也是C++标准库,以及boost库并没有选择去这么实现各自的指针类的原因。因此,除非场景特殊,比如Qt中的QWidget可能需要处理类似于delete this的场景,否则建议不要自己实现这么一个复杂的RAII指针类,而是在日常编程中注意自己的代码规范,包括注意在delete后将指针置空;使用智能指针,不要操作裸指针,更不要从智能指针中获取裸指针并将其delete;尽量不要使用delete this,或者类似的行为,除非你有特殊的机制去支持这种场合,比如使用QPointer

上一篇 下一篇

猜你喜欢

热点阅读