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

如果是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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容

  • 1. 类的默认成员函数 包括6个:构造函数、析构函数、拷贝构造函数、赋值运算符函数、取址运算符函数、const取址...
    zillo阅读 603评论 0 0
  • 我对C++思考了很多,有一些内容和指针有关。在C++ 11中只对指针进行了小量的更新(引入了nullptr),不过...
    程序员__阅读 820评论 0 3
  • C++是一门被广泛使用的系统级编程语言,更是高性能后端标准开发语言;C++虽功能强大,灵活巧妙,但却属于易学难精的...
    某某呆阅读 219评论 0 0
  • c++名词解惑# 一。堆和栈的区别:++++++栈: FILO os自动分配释放,函数参数,局部变量等。 一级缓存...
    _Hook_阅读 277评论 0 1
  • 资源管理在软件开发中历来都是老大难问题,堆上内存的管理尤其麻烦。现代编程语言引入了垃圾回收机制,如 Java,Py...
    hanpfei阅读 1,645评论 0 0