關于C++中懸掛指針的一些思考,以及從QPointer中能夠借鑒的經(jīng)驗

如果是C++程序員,應該對懸掛指針這種pain in the ass十分熟悉了。為了避免懸掛指針問題,一般有兩種解決思路:

  1. 在delete指針時,必須將指針置空。后續(xù)使用時,必須對指針進行判空。如果設計多線程設計,可能要配合臨界區(qū)/互斥鎖等同步原語。
  2. 使用std::shared_ptr、std::unique_ptr等智能指針設施,其他類庫提供的支持RAII的指針類也可以。

嚴格遵守上述兩條實踐準則,能解決99%的懸掛指針問題。那1%是怎么回事呢?這就是本文想要討論的問題了。以我個人的理解來說,如果出現(xiàn)懸掛指針問題,首先還是要關注這兩點實踐準則,是否有嚴格遵守。但仍沒有頭緒,可能是以下兩種情況:

  1. 類內(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
  2. 雖然使用了智能指針,但仍直接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通過連接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已經(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。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 1. 類的默認成員函數(shù) 包括6個:構造函數(shù)、析構函數(shù)、拷貝構造函數(shù)、賦值運算符函數(shù)、取址運算符函數(shù)、const取址...
    zillo閱讀 728評論 0 0
  • 我對C++思考了很多,有一些內(nèi)容和指針有關。在C++ 11中只對指針進行了小量的更新(引入了nullptr),不過...
    程序員__閱讀 908評論 0 3
  • C++是一門被廣泛使用的系統(tǒng)級編程語言,更是高性能后端標準開發(fā)語言;C++雖功能強大,靈活巧妙,但卻屬于易學難精的...
    某某呆閱讀 264評論 0 0
  • c++名詞解惑# 一。堆和棧的區(qū)別:++++++棧: FILO os自動分配釋放,函數(shù)參數(shù),局部變量等。 一級緩存...
    _Hook_閱讀 353評論 0 1
  • 資源管理在軟件開發(fā)中歷來都是老大難問題,堆上內(nèi)存的管理尤其麻煩。現(xiàn)代編程語言引入了垃圾回收機制,如 Java,Py...
    hanpfei閱讀 1,798評論 0 0

友情鏈接更多精彩內(nèi)容