https://zhuanlan.zhihu.com/p/364014571?ivk_sa=1024320u
代码中出现一个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 a(new T()); //构造 QPointer a(b); //构造 a.isNull(); //判空 a.data(); //返回裸指针 2、QSharedPointer & QWeakPointer
QSharedPointer是引用计数(强)指针,当所有强指针销毁时,实际对象才会销毁。
QWeakPointer是弱指针,可以持有对QSharedPointer的弱引用。它作为一个观察者,不会引起实际对象销毁,当对象销毁时会自动置空。
这两种指针同时都有以下3个成员:强引用计数strongRef,弱引用计数weakRef和数据data。
Qt引用计数指针
QSharedPointer & QWeakPointer 两种指针分别对应于C++中的std::shared_ptr和std::weak_ptr。常见使用方法:
//构造 QSharedPointer a(new Plot()); QSharedPointer b = a; QWeakPointer c = a; //强指针构造弱指针 QWeakPointer d(a);
//使用 c.clear(); //清除 a.isNull(); //判空 a->func(…); //(按常规指针来使用 “->”) QSharedPointer e = d.toStrongRef(); //弱指针转为强指针。注意,弱指针无法操纵数据,必须转为强指针 QWeakPointer f = e.toWeakRef();//强指针显式转为弱指针 QSharedPointer g = e.dynamicCast(); //动态类型转换 3、QScopedPointer
QScopedPointer保证当当前范围消失时指向的对象将被删除。它拥有一个很好的名字,它向代码的阅读者传递了明确的信息:这个智能指针只能在本作用域里使用,不希望被转让,因为它的拷贝构造和赋值操作都是私有的。
相当于C++中的std::unique_ptr,实例代码:
func(){ Plot* plot = new Plot(); //QScopedPointer出作用域自动销毁内存 QScopedPointerqsp(plot); //plot没有内存泄漏 } 4、其他智能指针
QScopedArrayPointer:一个QcopedPointer,默认删除它指向Delete []运算符的对象。为方便起见,还提供了操作符[]。 QSharedDataPointer/QExplicitySharedDataPointer搭配QSharedData类一起使用,以实现自定义隐式共享或显式共享类。(参考:链接1,链接2) 三、实践记录 1、通常,要使用弱指针,必须将其转换为强指针,因为这样的操作确保了只要您使用它就会生存。这相当于“锁定”访问的对象,并且是使用弱指针指向的对象的唯一正确方法。并且转换后使用前需要判空。
QSharedPointer qsp = qwp.toStrongRef(); //qwp是QWeakPointer if(!qsp.isNull()){ qDebug() << qsp->getName(…); //使用指向的对象 //… } 2、最好在new的时候就用QSharedPointer封装,并管理起来。
QSharedPointer qsp = QSharedPointer(new Plot()); 3、使用智能指针包装后,不要直接去删除指针对象。
Plot* plot = new Plot(); QSharedPointer qsp1(plot); delete plot; //运行时会提示:“shared QObject was deleted directly. The program is malformed and may crash.” 4、不要多次使用同一裸指针构造QSharedPointer。
Plot *plot = new Plot(); QSharedPointer qsp(plot); QSharedPointer qsp_ok = qsp; QSharedPointer qsp_error(plot); //崩溃,输出: “pointer 0x1f0a8f0 already has reference counting” 5、不要使用new直接构造QWeakPointer对象,它只能通过QSharedPointer的赋值来创建
QWeakPointer a(new Plot()); //error: 引用计数:强-1 弱2 6、由于智能指针对象是值语义,参数传递时尽可能用const引用兼顾效率
7、关于动态转换。使用QSharedPointer::dynamicCast()方法。
8、关于智能指针与QVariant转换,并关联到qobject的userdata。
//注册到元对象 Q_DECLARE_METATYPE(QWeakPointer) //设置数据 item->setData(QVariant::fromValue(plot.toWeakRef()), Qt::UserRole); //取数据 QWeakPointer plot = treeModel->data(index, Qt::UserRole).value<QWeakPointer >(); 9、关于Qt元对象系统自动析构和Qt智能指针自动析构相冲突的问题,经初步实验Qt4.8中应该已经解决了?不过实际中,可以让数据用智能指针管理,不用父子层级;窗体控件用父子层级,不用智能指针。
2022/4/10更新:
搞了一个源码,写了8个case示例,如果想学习,可以粘贴到qt工程里,这个可以直接运行,在main函数中修改你想运行的案例。
//----main.cpp----- #include #include #include
/----------------定义数据类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 f1 = QSharedPointer(f); f1->setValue(7); qDebug() << f1->getValue();
//退出作用域时,智能指针对象f1被析构,
//它所“指向”的指针f已经没有任何其他智能指针“指向”它了,所以f的内存也自动析构了
//不会发生内存泄漏
}
///初始化 void case02(){ //最好在new的时候就用QSharedPointer封装,并管理起来,不要直接用裸指针 QSharedPointer f = QSharedPointer(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 s1; QSharedPointer s2 = QSharedPointer(); QSharedPointer s3 = QSharedPointer(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 s1; QSharedPointer s2 = QSharedPointer(new Plot()); s1 = s2; //让s1也指向s2所指向的内存区域 QSharedPointer s3 = QSharedPointer(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 s1; QSharedPointer s2 = QSharedPointer(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 s1 = QSharedPointer(new Plot()); QWeakPointer w1 = QWeakPointer(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 s1 = QSharedPointer(new ColorPlot()); QSharedPointer s2 = s1.dynamicCast(); // dynamicCast() if(!s2.isNull()){ s2->setValue(7); s2->print();//调用子类方法 } }
///参数传递 void case08(){ //TODO }
/------------------主程序----------------------/ int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); case08(); }
/------------------Extra----------------------/ //[tip] 1.让数据用智能指针管理;窗体控件用Qt父子层级(不用智能指针)。 //[tip] 2.不使用裸指针作为接口参数,代码里面最好没有用到QSharedPointer::data()或者QWeakPointer::data() //[tip] 3.数据管理类持有强指针成员,其他类持有弱指针成员,强指针作为参数传递。
|