添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
[图解]Qt的智能指针

[图解]Qt的智能指针

1 年前

代码中出现一个bug,最终发现是由于在某个特殊情况下出现了使用垂悬指针,造成了程序崩溃,进而学习了解了Qt的智能指针机制。

一、悬垂指针的问题

如图,有两个指针a和b指向同一片内存,如果删除其中一个指针a,再去使用指针b的话,程序会崩溃。因为指针b此时已经是一个垂悬指针(Dangling pointer)了,它指向的内存已经被释放不再有效。

垂悬指针

使用指针b之前先判断b是否为空,这个做法在这里是不起作用的。问题的本质是通过指针a去释放内存时,指针b没有同步地置为空。

假如指针b能随内存的释放而自动置为空就好了,这正是智能指针所要解决的问题。

二、Qt中的智能指针

Qt提供了若干种智能指针:QPointer、QSharedPointer、QWeakPointer、QScopedPointer、QScopedArrayPointer、QSharedDataPointer、QExplicitlySharedDataPointer。

注:1、笔者Qt版本为4.8; 2、下述示例代码中"Plot"为"QObject"类的子类。

1、QPointer
QPointer只用于QObject的实例。如果它指向的对象被销毁,它将自动置空。如图:

QPointer

这是Qt体系下的专门用于QObject的智能指针。常见使用方法:

QPointer<Plot> a(new T());   	//构造
QPointer<Plot> a(b);		//构造
a.isNull();			//判空
a.data();			//返回裸指针

2、QSharedPointer & QWeakPointer

QSharedPointer是引用计数(强)指针,当所有强指针销毁时,实际对象才会销毁。

QWeakPointer是弱指针,可以持有对QSharedPointer的弱引用。它作为一个观察者,不会引起实际对象销毁,当对象销毁时会自动置空。

这两种指针同时都有以下3个成员:强引用计数strongRef,弱引用计数weakRef和数据data。

Qt引用计数指针
QSharedPointer &amp;amp;amp;amp;amp;amp; QWeakPointer

两种指针分别对应于C++中的std::shared_ptr和std::weak_ptr。常见使用方法:

//构造
QSharedPointer<Plot> a(new Plot()); 
QSharedPointer<Plot> b = a;
QWeakPointer<Plot> c = a;  //强指针构造弱指针
QWeakPointer<Plot> d(a);     
c.clear();	//清除
a.isNull();	//判空
a->func(...); 	//(按常规指针来使用 "->")
QSharedPointer<Plot> e = d.toStrongRef();   //弱指针转为强指针。注意,弱指针无法操纵数据,必须转为强指针
QWeakPointer<Plot> f = e.toWeakRef();//强指针显式转为弱指针
QSharedPointer<Plot> g = e.dynamicCast<T>();  //动态类型转换

3、QScopedPointer

QScopedPointer保证当当前范围消失时指向的对象将被删除。它拥有一个很好的名字,它向代码的阅读者传递了明确的信息:这个智能指针只能在本作用域里使用,不希望被转让,因为它的拷贝构造和赋值操作都是私有的。

相当于C++中的std::unique_ptr,实例代码:

func




    
(){
    Plot* plot = new Plot();
    //QScopedPointer出作用域自动销毁内存
    QScopedPointer<Plot>qsp(plot);
    //plot没有内存泄漏

4、其他智能指针

  • QScopedArrayPointer:一个QcopedPointer,默认删除它指向Delete []运算符的对象。为方便起见,还提供了操作符[]。
  • QSharedDataPointer/QExplicitySharedDataPointer搭配QSharedData类一起使用,以实现自定义隐式共享或显式共享类。(参考: 链接1 链接2

三、实践记录

1、通常,要使用弱指针,必须将其转换为强指针,因为这样的操作确保了只要您使用它就会生存。这相当于“锁定”访问的对象,并且是使用弱指针指向的对象的唯一正确方法。并且转换后使用前需要判空。

QSharedPointer<Plot> qsp = qwp.toStrongRef();     //qwp是QWeakPointer
if(!qsp.isNull()){ 
qDebug() << qsp->getName(...);  //使用指向的对象
        //...

2、最好在new的时候就用QSharedPointer封装,并管理起来。

QSharedPointer<Plot> qsp = QSharedPointer(new Plot());

3、使用智能指针包装后,不要直接去删除指针对象。

Plot* plot = new Plot();
QSharedPointer<Plot> qsp1(plot);
delete plot;  //运行时会提示:"shared QObject was deleted directly. The program is malformed and may crash."

4、不要多次使用同一裸指针构造QSharedPointer。

Plot *plot = new Plot();    
QSharedPointer<Plot> qsp(plot);
QSharedPointer<Plot> qsp_ok = qsp;
QSharedPointer<Plot> qsp_error(plot);   //崩溃,输出: “pointer 0x1f0a8f0 already has reference counting”

5、不要使用new直接构造QWeakPointer对象,它只能通过QSharedPointer的赋值来创建

QWeakPointer<Plot> a(new Plot());      //error: 引用计数:强-1 弱2

6、由于智能指针对象是值语义,参数传递时尽可能用const引用兼顾效率

7、关于动态转换。使用QSharedPointer::dynamicCast<T>()方法。

8、关于智能指针与QVariant转换,并关联到qobject的userdata。

//注册到元对象
Q_DECLARE_METATYPE(QWeakPointer<Plot>) 				
//设置数据
item->setData(QVariant::fromValue(plot.toWeakRef()), Qt::UserRole);	
//取数据
QWeakPointer<Plot> plot = treeModel->data(index, Qt::UserRole).value<QWeakPointer<BasePlot> >();

9、关于Qt元对象系统自动析构和Qt智能指针自动析构相冲突的问题,经初步实验Qt4.8中应该已经解决了?不过实际中,可以让数据用智能指针管理,不用父子层级;窗体控件用父子层级,不用智能指针。


2022/4/10更新:

搞了一个源码,写了8个case示例,如果想学习,可以粘贴到qt工程里,这个可以直接运行,在main函数中修改你想运行的案例。

//----main.cpp-----
#include <QCoreApplication>
#include <QDebug>
#include <QSharedPointer>
/*----------------定义数据类Plot----------------------*/
class Plot {
public:
    Plot() {qDebug()<<"Plot()";m_value=0;}
    virtual ~Plot(){qDebug()<<"~Plot()";}
    int getValue() const;
    void setValue(int value);
protected:
    int m_value;
inline int Plot::getValue() const
    return m_value;
inline void Plot::setValue(int value)
    m_value = value;
/*------------------定义子类ColorPlot----------------------*/
class ColorPlot : public Plot {
public:
    void print(){qDebug() << "The Value is " << m_value;}
/*------------------测试例子----------------------*/
///裸指针
void case00(){
    Plot* f = new Plot;
    f->setValue(7);
    qDebug() << f->getValue();
    //对象f没有被析构,内存泄漏
    //所以需要调用一次:
    delete f;
///概述
void case01(){
    Plot* f = new Plot;
    QSharedPointer<Plot> f1 = QSharedPointer<Plot>(f);
    f1->setValue(7);
    qDebug() << f1->getValue();
    //退出作用域时,智能指针对象f1被析构,
    //它所“指向”的指针f已经没有任何其他智能指针“指向”它了,所以f的内存也自动析构了
    //不会发生内存泄漏
///初始化
void case02(){
    //最好在new的时候就用QSharedPointer封装,并管理起来,不要直接用裸指针
    QSharedPointer<Plot> f = QSharedPointer<Plot>(new Plot());
    f->setValue(7);
    qDebug() << f->getValue();
    //[tip] 1.使用智能指针包装后,不要直接去删除指针对象。
    //Qt会提示:"shared QObject was deleted directly. The program is malformed and may crash."
    //[tip] 2.也不要多次使用同一裸指针构造QSharedPointer。
    //crashed: “pointer 0x1f0a8f0 already has reference counting”
///判空与删除
void case03(){
    QSharedPointer<Plot> s1;
    QSharedPointer<Plot> s2 = QSharedPointer<Plot>();
    QSharedPointer<Plot> s3 = QSharedPointer<Plot>(new Plot());
    qDebug() << s1.isNull();//true
    qDebug() << s2.isNull();//true
    qDebug() << s3.isNull();//false
    //用'.'来使用智能指针自身的方法;用'->'使用指向的对象的方法,调用前需要先判空。
    if(!s3.isNull()){
        s3->setValue(7);
        qDebug() << s3->getValue();
    //用clear()清空s3的指向
    s3.clear();
    qDebug() << s3.isNull();//true
///引用计数(强)
void case04(){
    QSharedPointer<Plot> s1;
    QSharedPointer<Plot> s2 = QSharedPointer<Plot>(new Plot());
    s1 = s2; //让s1也指向s2所指向的内存区域
    QSharedPointer<Plot> s3 = QSharedPointer<Plot>(s2);//让s3也指向s2所指向的内存区域
    //通过调试能看到,一个QSharedPointer的内部保存有一个强引用计数和一个弱引用计数,即对指向的那一片内存的引用计数。
    //此时,s1 s2 s3的引用计数都一样:强引用为3,弱引用为3
    s1.clear();//s2 s3的引用计数都一样:强引用为2,弱引用为2
    s2.clear();//s3的引用计数:强引用为1,弱引用为1
    s3.clear();//内存释放
    //只有s1,s2,s3的指向都被清空,实际对象才会被析构,内存才会被释放
///引用计数(强&弱)
void case05(){
    QSharedPointer<Plot> s1;
    QSharedPointer<Plot> s2 = QSharedPointer<Plot>(new Plot());
    s1 = s2; //让s1也指向s2所指向的内存区域
    QWeakPointer<Plot> w1 = QWeakPointer<Plot>(s1);//初始化QWeakPointer
    QWeakPointer<Plot> w2;
    w1 = s1; //初始化QWeakPointer
    QWeakPointer<Plot> w3(s2);
    //s1 s2 w1 w2 w3都指向同一片内存,这片内存的引用计数:强引用为2,弱引用为4
    s1.clear();//引用计数:强引用为2,弱引用为4
    w1.clear();//引用计数:强引用为1,弱引用为3
    s2.clear();//引用计数:强引用为0,弱引用为0
    //无论w1 w2 w3怎么样,只有s1,s2的指向都被清空,
    //Foo的这个对象才会被析构,w1 w2 w3会被自动clear,内存才会被释放
    qDebug() << w3.isNull();//true
///弱指针
void case06(){
    QSharedPointer<Plot> s1 = QSharedPointer<Plot>(new Plot());
    QWeakPointer<Plot> w1 = QWeakPointer<Plot>(s1);
    s1->setValue(7);
    if(!s1.isNull()){
        qDebug() << s1->getValue();//7
    if(!w1.toStrongRef().isNull()){
        //弱指针无法操纵数据,必须转为强指针
        //用toStrongRef()将弱引用转为强引用,来调用成员方法
        qDebug() << w1.toStrongRef()->getValue();//7
        w1.toStrongRef()->setValue(66);
        qDebug() << w1.toStrongRef()->getValue();//66
    //[tip] 通常,要使用弱指针,必须将其转换为共享指针,因为这样的操作确保了只要您使用它就会生存。
    //这相当于“锁定”访问的对象,并且是使用弱指针指向的对象的唯一正确方法。并且转换后使用前需要判空。
///类型转换
void case07(){
    QSharedPointer<Plot> s1 = QSharedPointer<ColorPlot>(new ColorPlot());
    QSharedPointer<ColorPlot> s2 = s1.dynamicCast<ColorPlot>(); // dynamicCast<T>()
    if(!s2.isNull()){
        s2->setValue(7);
        s2->print();//调用子类方法
///参数传递
void case08(){
    //TODO
/*------------------主程序----------------------*/
int main(int argc, char *argv[])
    QCoreApplication a(argc, argv);
    case08();