C++编程难点之一就是内存管理,尤其是对于指针的使用,管理不好很容易出现内存泄露。我们使用Qt框架开发软件时,可以用Qt封装的几种智能指针,这些指针将C++指针封装到一个对象里,使用方式与普通指针一样。这种将指针封装成对象的方式,避免了直接使用指针可能导致的内存泄漏。本文总结了QPointer、QSharedPointer、QWeakPointer、QSharedDataPointer、QScopedPointer的使用场景及示例,理解并熟练使用这些指针,能够帮助我们写出更健壮的程序。
QPointer 是一个模版类,主要用来管理QObject子类的指针,自定义类如果不是QObject的子类,则不能使用QPointer管理。当QPointer管理的指针被销毁时,它会自动设置为0。使用示例
QPointer<QLabel> label = new QLabel;
label->setText("&Status:");
...
if (label)
label->show();
编程时如果一个类A持有另一个类B的指针,而不知道B指针何时会销毁,此时可以用QPointer来保存这个指针,使用的时候用if语句判断一下,如果指针有效再执行下面的操作。
注意:QPointer管理的必须是QObject子类的指针
QSharedPointer 共享指针的强引用,通过引用计数自动地管理共享指针,它的行为与正常指针一样,即使用const修饰的QSharedPointer与普通指针行为也一样。当一个QSharedPointer对象超出作用域时,若没有其他的QSharedPointer对象引用它管理的指针,即引用计数为0时,它将自动删除持有的指针。下面举例说明如何使用:
{
QSharedPointer<QLabel> pLabel(new QLabel);
pLabel->setText(QString("test"));
//ref:1
QSharedPointer<QLabel> pLabel2 = pLabel;
//ref:2
QWeakPointer<QLabel> weakLabel = pLabel2;
//ref:2
}
//ref:0 超出作用于自动释放pLabel管理的内存
下面是错误的用法,将同一个指针给多个QSharedPointer,会导致程序崩溃。
{
QSharedPointer<QLabel> pLabel(new QLabel);
//注意不能将同一个指针给两个QSharedPointer,可以编译通过,但是会导致程序崩溃
QSharedPointer<QLabel> pLabel2(pLabel.data());
}
QWeakPointer 共享指针的弱引用,不能直接用于访问共享指针,但是可以用来验证指针是否在其他的上下文环境中被删除。当多个两个或多个类中有各自指针的QSharedPointer引用时,会形成循环引用,导致两个或多个类对象都无法释放,这种情况可以引入QWeakPointer来解除循环引用。使用示例:
QWeakPointer<QLabel> pLabel;
{
QSharedPointer<QLabel> strongLabel(new QLabel);
pLabel = strongLabel;
if(pLabel.toStrongRef())
{
pLabel.data()->setText("Hello");
qDebug() << "pWeakLabel is valid"; //打印这行
}
}
//pLabel 指向的strongLabel超过作用域已经释放了。
{
QWeakPointer<QLabel> pWeakLabel(pLabel);
if(pWeakLabel.toStrongRef())
{
pWeakLabel.data()->setText("Hello");
qDebug() << "pWeakLabel is valid";
}
else
{
qDebug() << "pWeakLabel is unvalid"; //打印这行
}
}
下面是一个循环引用的示例:
class B;
class A
{
public:
void setB(const QSharedPointer<B> &B);
private:
QSharedPointer<B> m_b; //强引用
};
class B
{
public:
void setA(const QSharedPointer<A> &A);
private:
QSharedPointer<A> m_a; //强引用
};
{
//这段代码产生了循环引用,导致a,b超过了作用域后内存不能被释放
QSharedPointer<A> a(new A);
QSharedPointer<B> b(new B);
a->setB(b);
b->setA(a);
}
上面举的例子很简单,比较容易发现问题,实际开发中可能是3个以上的类相互引用而产生循环引用,这时候问题就藏的比较深了,需要认真分析找出循环引用的环,然后用QWeakPointer打破这个环。解除循环引用示例:
class B;
class A
{
public:
void setB(const QSharedPointer<B> &B);
private:
QSharedPointer<B> m_b; //强引用
};
class B
{
public:
void setA(const QSharedPointer<A> &A);
private:
QWeakPointer<A> m_a; //弱引用
};
{
//由于一方使用了QWeakPointer,不会产生循环引用,a,b超过了作用域后内存能被释放
QSharedPointer<A> a(new A);
QSharedPointer<B> b(new B);
a->setB(b);
b->setA(a);
}
QSharedDataPointer 主要用来管理隐式共享对象,隐式共享类是指一个类里有一个QSharedDataPointer管理的对象,这个对象继承自QSharedData。举例说明
class EmployeeData : public QSharedData
{
public:
EmployeeData() : id(-1) { }
EmployeeData(const EmployeeData &other)
: QSharedData(other), id(other.id), name(other.name) { }
~EmployeeData() { }
int id;
QString name;
};
class Employee
{
public:
Employee() { d = new EmployeeData; }
Employee(int id, const QString &name) {
d = new EmployeeData;
setId(id);
setName(name);
}
Employee(const Employee &other)
: d (other.d)
{
}
void setId(int id) { d->id = id; }
void setName(const QString &name) { d->name = name; }
int id() const { return d->id; }
QString name() const { return d->name; }
private:
QSharedDataPointer<EmployeeData> d;
};
上例中Employee 即是隐式共享类。QSharedDataPointer 实现了引用计数来管理对象,Qt中有很多类具备隐式共享的性质,隐式共享可以提高内存使用效率。默认情况下一个隐式共享类的对象在程序中传递时,在没有修改数据的情况下,内存中只有一份数据拷贝。隐式共享类还有个特性是写时复制,例如
Employee a(1001, "张三");
Employee b = a;
//此时内存中只有一个 EmployeeData 对象。
b.setName("李四");
//此时a、b的数据是完全独立的两个对象了,此时内存中有量个 EmployeeData 对象
上例的写时复制在逻辑上可能是有问题的,当调用了b.setName()之后,会产生两个id为1001,但名字不同的对象出来。如果你只是想在任何地方都能修改id为1001的雇员的名字,而不是生成两个对象,那就不能用隐式共享了,需要用显示共享。显示共享就是把
QSharedDataPointer<EmployeeData> d;
//改为
QExplicitlySharedDataPointer<EmployeeData> d;
用了显示共享后下面的代码就会有新的含义了
Employee a(1001, "张三");
Employee b = a;
//此时内存中只有一个 EmployeeData 对象。
b.setName("李四");
//此时内存中仍然只有一个 EmployeeData 对象
//保证了id为1001的只有一个数据实例。
QScopedPointer 保存一个动态分配内存的对象指针,当QScopedPointer作用域失效时,自动通过析构函数来删除其管理的指针。
不用智能指针编程:
void myFunction(bool useSubClass)
{
MyClass *p = useSubClass ? new MyClass() : new MySubClass;
QIODevice *device = handsOverOwnership();
if (m_value > 3) {
delete p;
delete device;
return;
}
try {
process(device);
}
catch (...) {
delete p;
delete device;
throw;
}
delete p;
delete device;
}
使用QScopedPointer 管理指针
void myFunction(bool useSubClass)
{
QScopedPointer<MyClass> p(useSubClass ? new MyClass() : new MySubClass);
QScopedPointer<QIODevice> device(handsOverOwnership());
if (m_value > 3)
return;
process(device);
}
从上面的示例可以看出,QScopedPointer作为一个小工具类,能极大的简化编码。这里把堆内存的分配赋予了栈内存的所有权,即资源请求即初始化(RAII),超过栈的作用域就清理指针对应的内存。 QScopedPointer 没有拷贝构造函数和赋值操作符,因此指针的所有权和生命周期能清晰的表示出来,即QScopedPointer对象的生命周期。
以上就是本篇的所有内容了,有问题的朋友欢迎留言讨论!!!