如果是C++程序員,應該對懸掛指針這種pain in the ass十分熟悉了。為了避免懸掛指針問題,一般有兩種解決思路:
- 在delete指針時,必須將指針置空。后續(xù)使用時,必須對指針進行判空。如果設計多線程設計,可能要配合臨界區(qū)/互斥鎖等同步原語。
- 使用
std::shared_ptr、std::unique_ptr等智能指針設施,其他類庫提供的支持RAII的指針類也可以。
嚴格遵守上述兩條實踐準則,能解決99%的懸掛指針問題。那1%是怎么回事呢?這就是本文想要討論的問題了。以我個人的理解來說,如果出現(xiàn)懸掛指針問題,首先還是要關注這兩點實踐準則,是否有嚴格遵守。但仍沒有頭緒,可能是以下兩種情況:
- 類內(nèi)部調(diào)用了
delete this,或者類似于delete this的行為。這里說到的類似于delete this的行為,在Qt開發(fā)中,一般可以表現(xiàn)為:為QWidget設置了deleteOnClose屬性(即setAttribute(Qt::WA_DeleteOnClose)),而后在類的內(nèi)部調(diào)用了close()。這會讓窗口銷毀,并調(diào)用析構函數(shù),因此這種行為本質(zhì)上也是delete this。 - 雖然使用了智能指針,但仍直接delete裸指針,或者從
std::shared_ptr、std::unique_ptr中調(diào)用get()方法獲取了裸指針,并錯誤地將其delete。
以上兩種情況,絕大多數(shù)的支持RAII的指針類都是感知不到的,包括C++11提供的std::shared_ptr、std::unique_ptr。因此,仍存在懸掛指針問題,存在造成崩潰的風險。
使用Qt中提供的QPointer類,并讓被管理的資源繼承自QObject,能夠解決此類問題,這主要是為了處理QWidget設置了deleteOnClose屬性(即setAttribute(Qt::WA_DeleteOnClose)),而后在類的內(nèi)部調(diào)用了close()的情況。QPointer類能夠感知到被管理資源的銷毀,從而自動地將自身置空,因而能夠規(guī)避懸掛指針問題。原理也很簡單,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已經(jīng)置空,所以不會導致訪問懸掛指針。
//如果使用裸指針,或者C++11的智能指針,那就GG了
if (button)
{
button->setFixedSize(100, 20);
}
那么,如果不在Qt環(huán)境下,如何能夠避免此類問題呢?Qt的信號槽機制,實際上是類似與設計模式中的觀察者模式的。所以,在必要的場合下,我們可以使用C++11中的智能指針,并配合觀察者模式,避免懸掛指針問題。示例如下:
#include <iostream>
#include <memory>
#include <set>
//發(fā)起訂閱者
class Observer {
public:
virtual void onObjectDestroyed() = 0;
};
//被訂閱的主題,特定事件下會發(fā)布消息通知到訂閱者
//這個場景下,當被訂閱的主題銷毀時,即發(fā)布消息
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對象調(diào)用release方法將自身銷毀時,父類Observed的虛析構函數(shù)會被調(diào)用,并通知訂閱主題的觀察者Observer。由于ManagingPointer繼承自Observer,因此當managedObject調(diào)用release函數(shù),ManagingPointer會感知到,即onObjectDestroyed函數(shù)會被調(diào)用(注意Observed類的notifyObservers方法,會通知被訂閱的主題Observer,而當Observed類析構時會調(diào)用notifyObservers方法,需要理清這個調(diào)用關系)。最后,ManagingPointer的成員std::shared_ptr<Observed> object會在onObjectDestroyed中被reset,從而自動將自身自動置空。
可以看到,這一個流程下來,還是有點麻煩的,如果對觀察者模式不是很熟悉,會需要一點時間理清調(diào)用關系。并且可以看到ManagingPointer類的使用并不是很方便,并不如C++11的智能指針好用,如果上述示例代碼要被使用,或許還需要重寫許多操作符函數(shù),才能投入使用。
總結
可以看到,為了規(guī)避delete this以及delete裸指針帶來的懸掛指針風險,實際上是有成本的,包括實現(xiàn)一個更復雜的且和C++智能指針一樣好用且穩(wěn)定的,支持RAII的指針類;以及更高的運行期性能消耗。這兩點都是不可忽視的。這也是C++標準庫,以及boost庫并沒有選擇去這么實現(xiàn)各自的指針類的原因。因此,除非場景特殊,比如Qt中的QWidget可能需要處理類似于delete this的場景,否則建議不要自己實現(xiàn)這么一個復雜的RAII指針類,而是在日常編程中注意自己的代碼規(guī)范,包括注意在delete后將指針置空;使用智能指針,不要操作裸指針,更不要從智能指針中獲取裸指針并將其delete;盡量不要使用delete this,或者類似的行為,除非你有特殊的機制去支持這種場合,比如使用QPointer。