关于C++中悬挂指针的一些思考,以及从QPointer中能够借鉴
如果是C++程序员,应该对悬挂指针这种pain in the ass十分熟悉了。为了避免悬挂指针问题,一般有两种解决思路:
- 在delete指针时,必须将指针置空。后续使用时,必须对指针进行判空。如果设计多线程设计,可能要配合临界区/互斥锁等同步原语。
- 使用
std::shared_ptr
、std::unique_ptr
等智能指针设施,其他类库提供的支持RAII的指针类也可以。
严格遵守上述两条实践准则,能解决99%的悬挂指针问题。那1%是怎么回事呢?这就是本文想要讨论的问题了。以我个人的理解来说,如果出现悬挂指针问题,首先还是要关注这两点实践准则,是否有严格遵守。但仍没有头绪,可能是以下两种情况:
- 类内部调用了
delete this
,或者类似于delete this
的行为。这里说到的类似于delete this
的行为,在Qt开发中,一般可以表现为:为QWidget
设置了deleteOnClose属性(即setAttribute(Qt::WA_DeleteOnClose)
),而后在类的内部调用了close()
。这会让窗口销毁,并调用析构函数,因此这种行为本质上也是delete this
。 - 虽然使用了智能指针,但仍直接delete裸指针,或者从
std::shared_ptr
、std::unique_ptr
中调用get()
方法获取了裸指针,并错误地将其delete。
以上两种情况,绝大多数的支持RAII的指针类都是感知不到的,包括C++11提供的std::shared_ptr
、std::unique_ptr
。因此,仍存在悬挂指针问题,存在造成崩溃的风险。
使用Qt中提供的QPointer
类,并让被管理的资源继承自QObject
,能够解决此类问题,这主要是为了处理QWidget
设置了deleteOnClose属性(即setAttribute(Qt::WA_DeleteOnClose)
),而后在类的内部调用了close()
的情况。QPointer
类能够感知到被管理资源的销毁,从而自动地将自身置空,因而能够规避悬挂指针问题。原理也很简单,Qt中提供了信号槽机制,QPointer
通过连接QObject
的destroy
信号,自然就能感知到被管理资源的销毁,从而将自身置空了。这里要注意,被管理的指针类必须继承自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
。