4、智能指针入门:QSharedPointer、QWeakPointer、QScopedPointer的使用场景

说到内存泄漏,我估计不少朋友都吃过这个亏。我自己刚入行那会儿,写Qt程序最头疼的就是new出来的对象到底该谁删。后来项目越做越大,手动管理内存简直成了噩梦。直到我彻底搞懂了智能指针,才算是真正解脱了。

说白了,智能指针就是让对象自己管理自己的生命周期。你只管用,不用管什么时候释放。Qt给我们提供了三种主力智能指针:QSharedPointer、QWeakPointer和QScopedPointer。它们各有各的脾气,用错了地方反而会出问题。

核心观点:智能指针不是银弹,但用对了地方,能帮你干掉80%的内存泄漏问题。

4.1 QScopedPointer:最轻量的“保镖”

QScopedPointer是我个人最喜欢用的一个。它简单、高效,几乎没有额外开销。它的设计哲学就是:对象离开作用域,自动销毁。

你想想看,很多时候我们new一个对象,就是为了在当前函数或者当前类里用一下。比如一个对话框,弹出来让用户填信息,填完就关掉。这种场景用QScopedPointer再合适不过了。

void MainWindow::showSettingsDialog()
{
    // 这个对话框只在函数内部使用
    QScopedPointer<SettingsDialog> dlg(new SettingsDialog(this));
    dlg->exec();
    // 函数结束,dlg自动delete,不用你操心
}

嗯,这里要注意:QScopedPointer不允许拷贝和赋值。它就是个独占的保镖,只能有一个主人。如果你试图把它传给别的函数,编译器会直接报错。这其实是好事,能帮你避免很多隐晦的bug。

我的经验:在类的成员变量中,如果某个成员对象只属于这个类,不会被共享,我习惯用QScopedPointer。比如一个QTimer、一个QNetworkAccessManager,用这个最省心。

4.2 QSharedPointer:共享的“管家”

那如果多个地方都要用同一个对象呢?比如一个数据模型,好几个视图都要引用它。这时候QSharedPointer就派上用场了。

它的原理很简单:内部维护一个引用计数。每多一个人引用它,计数加1;每少一个人,计数减1。当计数降到0,对象自动销毁。说白了就是“最后一个走的人关门”。

void setupData()
{
    QSharedPointer<UserModel> model(new UserModel());
    
    // 多个视图共享同一个model
    view1->setModel(model);
    view2->setModel(model);
    
    // 函数结束,model的引用计数变成2(被两个视图持有)
    // 只有当两个视图都释放时,model才会被销毁
}

我在项目中遇到过一个问题:两个QSharedPointer互相引用,结果谁都不释放,形成了循环引用。那内存泄漏就妥妥的了。怎么破?这时候就需要QWeakPointer出场了。

避坑指南:我曾经在一个树形结构里用了QSharedPointer,父节点持有子节点,子节点又持有父节点。结果整个树销毁时,引用计数永远不为0。后来改成父节点用QSharedPointer,子节点用QWeakPointer,问题才解决。

4.3 QWeakPointer:不增加计数的“观察者”

QWeakPointer是个很有意思的存在。它指向一个对象,但不增加引用计数。它就像个“观察者”,只负责看,不负责管。

你可能会问:那它有什么用?用处大了。最常见的场景就是打破循环引用。另外,它还能帮你检查对象是否还活着。

class TreeNode {
public:
    QSharedPointer<TreeNode> parent;
    QList<QWeakPointer<TreeNode>> children;
    
    void addChild(QSharedPointer<TreeNode> child) {
        child->parent = QSharedPointer<TreeNode>(this); // 父节点强引用
        children.append(child.toWeakRef()); // 子节点弱引用
    }
};

使用QWeakPointer时,有个关键步骤:必须先提升(lock)成QSharedPointer才能访问对象。为什么要这样?因为对象可能已经被销毁了,直接访问就是野指针。

void processNode(QWeakPointer<TreeNode> weakNode)
{
    // 提升为强引用,如果对象已销毁,返回空
    QSharedPointer<TreeNode> node = weakNode.toStrongRef();
    if (node) {
        // 安全使用
        node->doSomething();
    } else {
        // 对象已经没了,优雅处理
        qDebug() << "节点已被销毁";
    }
}

小技巧:在Qt的信号槽中,如果你用QSharedPointer作为参数,记得用QWeakPointer来捕获。否则lambda表达式会延长对象的生命周期,可能导致意想不到的行为。

4.4 三者的对比与选择

说了这么多,到底什么时候用哪个?我整理了一个表格,你一看就明白了。

指针类型 所有权 引用计数 拷贝支持 最佳场景
QScopedPointer 独占 无 不支持 局部对象、类内独占成员
QSharedPointer 共享 有 支持 多处引用的对象、数据共享
QWeakPointer 无 不增加 支持 打破循环引用、缓存、观察者模式

我个人习惯是:能用QScopedPointer就不用QSharedPointer。为什么?因为QSharedPointer有引用计数的开销,多线程环境下还要加锁。而QScopedPointer几乎零开销。只有确定需要共享时,才升级到QSharedPointer。

4.5 知识体系总览

下面这张图,是我自己总结的智能指针选择逻辑。你照着这个思路走,基本不会出错。

智能指针选择流程图 需要管理对象生命周期? 是否独占所有权? 是 QScopedPointer 否 存在循环 引用风险? 是 QWeakPointer 否 QSharedPointer 选择建议 优先使用 QScopedPointer(零开销) 需要共享时用 QSharedPointer,注意循环引用

你看,选择其实不复杂。先问自己:这个对象需要别人来管理生命周期吗?如果不需要,QScopedPointer走起。如果需要共享,再问有没有循环引用的风险。有风险就用QWeakPointer配合,没有就直接上QSharedPointer。

最后说一句:智能指针不是万能的。它解决的是所有权和生命周期的问题,不是线程安全的问题。多线程环境下,即使用了智能指针,该加锁的地方还是得加锁。这个坑我踩过,希望你别再踩了。

可可精品资料库