QT封装的指针

664 阅读8分钟

摘要

C/C++程序员来讲,内存泄露是需要特别关注的点,但是QT提供智能指针,可以避免由于使用原生指针(忘记释放堆上内存)而带来的内存泄露。

QPointer

QPointer类是一个模板类,它提供了指向QObject的保护指针。 一个受保护的指针QPointer<T>,其行为与普通c++指针T *类似,只是当被引用的对象被销毁时,它会自动设置为0(不像普通c++指针,在这种情况下会变成“悬空指针”)。T必须是QObject的子类。 当您需要存储一个指向其他人拥有的QObject的指针时,保护指针是有用的,因此当您仍然持有对它的引用时可能会被销毁。您可以安全地测试指针的有效性。

QLabel * pLabel = new QLabel();
pLabel->setText("aa");
QLabel * pLabel2 = pLabel;//其中pLabel2就是期望拥有pLabel所指代的对象的指针

此时若是delete pLabel ,pLabel2 变成悬挂指针;

如下代码可解决该问题

QLabel * pLabel = new QLabel();
pLabel->setText("aa");
QPointer<QLabel> pLabel2 = pLabel;//其中pLabel2就是期望拥有pLabel所指代的对象的指针

Notice
当使用QPointer时,Qt 5在行为上有了轻微的改变。当在QWidget(或QWidget的子类)上使用QPointer时,以前QPointer会被QWidget析构函数清除。现在,QPointer被QObject析构函数清除(因为此时QWeakPointer对象被清除)。在QWidget析构函数销毁被跟踪小部件的子部件之前,任何跟踪小部件的qpointer都不会被清除。
调用方法

  • 通过data()调用。data()是QPointer的成员变量,它返回指向的指针对象,通过指针对象对其成员变量引用,比如:pLabel.data()->setText(“aa”);
  • 直接引用。我们通常要调用指针对象的成员变量或函数时,通常的写法是:变量+“.”,然后“.”会自动变成 “->”,但是QPointer却不是,这里我们写“pLabel.” 自动联想出来的是QPointer本身的成员函数,而非QLabel的成员函数,并且“.”也不会变成"->",那么如果我们要调用QLabel的成员函数,就得手动写"->",比如:pLabel->setText(“aa”);

QScopedPointer

QScopedPointer和C++中的智能指针std::unique_ptr其概念是一样的,它包装了new操作符在堆上分配的动态对象,能够保证动态创建的对象在任何时候都可以被正确地删除。但它有更严格的所有权,并且不能转让,一旦获取了对象的管理权,你就无法再从它那里取回来。也就是说,只要出了作用域,指针就会被自动删除,因为它的拷贝构造和赋值操作都是私有的,与QObject及其派生类风格相同。

使用案例

void myFunction(bool useSubClass)
 {
     QScopedPointer<MyClass> p(useSubClass ? new MyClass() : new MySubClass);
     QScopedPointer<QIODevice> device(handsOverOwnership());

     if (m_value > 3)
         return;

     process(device);
 }

注意:因为拷贝构造和赋值操作私有的,所以不能用作容器的元素。

注意

  1. const 限制 C ++指针的const限定也可以用QScopedPointer表示:
 //指向常量的常指针
const QWidget *const p = new QWidget();
const QScopedPointer<const QWidget> p(new QWidget());

//常指针
QWidget *const p = new QWidget();
const QScopedPointer<QWidget> p(new QWidget());

 //指向常量的指针
const QWidget *p = new QWidget();
 QScopedPointer<const QWidget> p(new QWidget());
  1. QScopedPointer作为函数返回值
    上面说到,使用QScopedPointer智能指针动态创建的对象,一旦出了作用域就会 被自动释放并置空,那么如果需要函数返回值怎么办呢?

比如下面这种情况:

QLabel * createLabel()
{
    QScopedPointer<QLabel> pLabel(new QLabel());
//    return pLabel.data();  //invalid
    return  pLabel.take(); //valid
}

注意,我们在createLabel()函数中创建label对象并返回时,不能使用data(),而要使用take(); 因为 T *QScopedPointer::data() const返回指向对象的常量指针,QScopedPointer仍拥有对象所有权。 所以通过data()返回过后就被自动删除了,从而导致mian函数中的p1变成了野指针,程序崩溃。 而使用T *QScopedPointer::take()也是返回对象指针,但QScopedPointer不再拥有对象所有权,而是转移到调用这个函数的caller,同时QScopePointer对象指针置为NULL。

  1. reset函数 void QScopedPointer::reset(T *other = Q_NULLPTR):delete目前指向的对象,调用其析构函数,将指针指向另一个对象other,所有权转移到other。
  2. 处理数组

对应的还有一个指针QScopedArrayPointer,专门用于处理数组,其用法和QScopedPointer是一样的

官方简单示例: void foo() { QScopedArrayPointer i(new int[10]); i[2] = 42; ... return; // our integer array is now deleted using delete[] }

超出作用域过后会自动调用delete[]删除指针,这里就不展开描述了。

QSharedPointer

Qt智能指针QSharedPointer 与 C++中的std::shared_ptr其作用是一样的。

QSharedPointer 是一个共享指针,它与 QScopedPointer 一样包装了new操作符在堆上分配的动态对象,但它实现的是引用计数型的智能指针 ,也就是说,与QScopedPointer不同的是,QSharedPointer可以被自由地拷贝和赋值,在任意的地方共享它,所以QSharedPointer也可以用作容器元素。

所谓的计数型指针,就是说在内部QSharedPointer对拥有的内存资源进行引用计数,比如有3个QSharedPointer同时指向一个内存资源,那么就计数3,知道引用计数下降到0,那么就自动去释放内存啦。

需要注意的是:QSharedPointer 是线程安全的,因此即使有多个线程同时修改 QSharedPointer 对象也不需要加锁。虽然 QSharedPointer 是线程安全的,但是 QSharedPointer 指向的内存区域可不一定是线程安全的。所以多个线程同时修改 QSharedPointer 指向的数据时还要应该考虑加锁。

先来看一个官方示例:

static void doDeleteLater(MyObject *obj)
{
        obj->deleteLater();
}

void otherFunction()
{
        QSharedPointer<MyObject> obj = QSharedPointer<MyObject>(new MyObject, doDeleteLater);

        // continue using obj
        obj.clear();    // calls obj->deleteLater();
}

这个示例中,传入了一个函数,用于自定义删除。也可以直接使用成员函数:

QSharedPointer<MyObject> obj = QSharedPointer<MyObject>(new MyObject, &QObject::deleteLater);

QSharedPointer使用非常方便,直接和普通指针用法一样,创建后直接用,后面就不用管了。

我们都知道Qt的QObject对象树系统(父子),当在创建对象时如果指定了父对象,那么该对象就无需手动释放,会由父对象去做资源回收。目前,对象树和共享指针结合也不会出现崩溃。 ``` QSharedPointer obj = QSharedPointer(new QWidget(this)); ```

QWeakPointer

前面我们说到的QSharedPointer是一个强引用类型的智能指针,而QWeakPointer是一个弱引用类型的智能指针,和C++中的weak_ptr功能是一样的。

QWeakPointer 是为配合 QSharedPointer 而引入的一种智能指针,它更像是 QSharedPointer 的一个助手(因为它不具有普通指针的行为,没有重载operator*和->)。它的最大作用在于协助 QSharedPointer 工作,像一个旁观者一样来观测资源的使用情况。
QWeakPointer不能用于直接取消引用指针,但它可用于验证指针是否已在另一个上下文中被删除。并且QWeakPointer对象只能通过QSharedPointer的赋值来创建。

需要注意的是,QWeakPointer不提供自动转换操作符来防止错误发生。即使QWeakPointer跟踪指针,也不应将其视为指针本身,因为它不能保证指向的对象保持有效。
QWeakPointer用于解决循环引用困境

循环引用,两个对象互相使用一个 QSharedPointer成员变量指向对方(你中有我,我中有你)。由于QSharedPointer是一个强引用的计数型指针,只有当引用数为0时,就会自动删除指针释放内存,但是如果循环引用,就会导致QSharedPointer指针的引用永远都不能为0,这时候就会导致内存无法释放。

所以QWeakPointer诞生了,它就是为了打破这种循环的。并且,在需要的时候变成QSharedPointer,在其他时候不干扰QSharedPointer的引用计数。它没有重载 * 和 -> 运算符,因此不可以直接通过 QWeakPointer 访问对象,典型的用法是通过 lock() 成员函数来获得 QSharedPointer,进而使用对象。

示例 首先,我们来看一个QSharedPointer循环使用的示例:

#include <QWidget>
#include <QDebug>
#include <QWeakPointer>
#include <QSharedPointer>

class Parent
{
public:
    ~Parent(){
        qDebug() << __FUNCTION__;
    }
    QSharedPointer<Children> m_pChildren;
};
class Children
{
public:
    ~Children(){
        qDebug() << __FUNCTION__;
    }
    QSharedPointer<Parent> m_pParent;
};

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();

    void test();
};



#include "widget.h"

Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    test();
}

Widget::~Widget()
{
    qDebug() << __FUNCTION__;
}

void Widget::test()
{
    QSharedPointer<Parent> parent(new Parent());
    QSharedPointer<Children> children(new Children());

    if(parent && children){
        parent->m_pChildren = children;
        children->m_pParent = parent;
    }
}

在构造函数中调用test()函数,执行完过后应该会自动释放parent和children对象,但是由于相互引用,在退出之前,引用计数为2,退出之后引用计数还是1,所以导致不能自动释放,并且此时这两个对象再也无法访问到。运行过后发现并没有进入到Parent和Children的析构函数中。这就导致了内存泄漏。

那么该如何解决这个问题呢?
很简单,直接将上述代码中的

QSharedPointer<Children> m_pChildren;
QSharedPointer<Parent> m_pParent;

改成
QWeakPointer<Children> m_pChildren;
QWeakPointer<Parent> m_pParent;

这是因为在test()退出之前引用计数是1,函数退出之后就自动析构,这就解除了上面的循环引用。