Qt对象树与生命期
本文为原创文章,转载请注明出处,或注明转载自“黄邦勇帅(原名:黄勇)
本文出自本人原创著作《Qt5.10 GUI完全参考手册》网盘地址:
https://pan.baidu.com/s/1iqagt4SEC8PUYx6t3ku39Q
《C++语法详解》网盘地址:https://pan.baidu.com/s/1dIxLMN5b91zpJN2sZv1MNg
若对C++语法不熟悉,建议参阅本人所著《C++语法详解》一书,电子工业出版社出版,该书语法示例短小精悍,对查阅C++知识点相当方便,并对语法原理进行了透彻、深入详细的讲解,可确保读者彻底弄懂C++的原理,彻底解惑C++,使其知其然更知其所以然。此书是一本全面了解C++不可多得的案头必备图书。
为什么要使用对象树:GUI程序通常是存在父子关系的,比如一个对话框之中含有按钮、列表等部件,按钮、列表、对话框等部件其实就是一个类的对象(注意是类的对象,而非类),很明显这些对象之间是存在父子关系的,因此一个GUI程序通常会由一个父对象维护着一系列的子对象列表,这样更方便对部件的管理,比如当按下tab键时,父对象会依据子对象列表令各子对象依次获得焦点。当关闭对话框时,父对象依据子对象列表,找到每个子对象,然后删除它们。在Qt中,对对象的管理,使用的是树形结构,也就是对象树。
子对象和父对象:本小节的父/子对象是相对于由对象组成的树形结构而言了,父节点对象被称为父对象,子节点对象被称为子对象。注意:子对象并不是指类中的对象成员。
2.5.1 组合模式与对象树
组合模式指的是把类的对象组织成树形结构,这种树形结构也称为对象树,Qt使用对象树来管理QObject及其子类的对象。注意:这里是指的类的对象而不是类。把类组织成树形结构只需使用简单的继承机制便可实现。
使用组合模式的主要作用是可以通过根节点对象间接调用子节点中的虚函数,从而可以间接的对子节点对象进行操作。
组合模式的基本思想是使用父类类型的指针指向子类对象,并把这个指针存储在一个数组中(使用容器更方便),然后每创建一个类对象就向这个容器中添加一个指向该对象的指针。下面的示例为核心代码
示例2.12:组合模式核心代码(C++代码)
#include <iostream>
#include<vector> //使用容器。
using namespace std;
class A {public: //顶级父类。
vector<A*> v; /*创建一个存储父类类型指针的容器,此步也可使用形如A* m[11]的数组代替,但数组不能自动扩容。*/
void add(A* ma) { v.push_back(ma);} }; //add函数的主要作用是将ma添加到容器v中。
class B :public A {public: //需要继承自类A
void add(A* ma) { v.push_back(ma); } };
class C :public A {public: };//需要继承自类A,该类无add函数,也就是说该类的对象不能有子对象。
int main(){ A ma, ma1, ma2, ma3; B mb, mb1, mb2, mb3; C mc, mc1, mc2, mc3;
//创建对象树。
ma.add(&mb); ma.add(&mb1); mb.add(&mb3); mb.add(&mc); mb.add(&mc1);
mb1.add(&mc2); mb1.add(&ma1); mb1.add(&ma2); }
程序的结构如图2-2
示例2.13:打印出组合模式中的各对象的名称(C++代码)
#include <iostream>
#include <string>
#include<vector>
using namespace std;
class A {public:
string name;//用于存储创建的对象的名称
vector<A*> v; //创建一个存储父类类型指针的容器。
A(){}
A(string n){name=n;}
void add(A* ma) { v.push_back(ma);} //将ma添加到容器v中。
virtual string g(){return name;} //虚函数,用于获取对象名。
void f(){ //用于显示当前对象的容器v中存储的内容。
if(!v.empty()) //若v不为空,则执行以下语句。
{cout<<name<<"="; //输出当前对象的名称。
for (vector<int>::size_type i = 0; i!=v.size(); i++) //遍历容器v。
{cout<<v[i]->g()<<",";} //输出容器v中存储的对象的名称,注意g是虚函数。
cout<<endl;} } //f结束
virtual void pt(){ //该函数会被递归调用,用以输出整个对象树中对象的名称。
f();
for (vector<int>::size_type i = 0; i!=v.size(); i++)
v[i]->pt(); } //注意pt是虚函数,假如v[i]类型为其子类型B时,则会调用B::pt()
}; //类A结束
class B :public A {public: //需要继承自类A,代码与类A类似
string name;
B(string n){name=n;}
void add(A* ma) { v.push_back(ma); }
string g(){return name;}
void f(){
if(!v.empty()){cout<<name<<"=";
for (vector<int>::size_type i = 0; i!=v.size(); i++) {cout<<v[i]->g()<<",";}
cout<<endl;} } //f结束
void pt(){ f();
for (vector<int>::size_type i = 0; i!=v.size(); i++) v[i]->pt(); } }; //类B结束
class C :public A {public: //需要继承自类A,该类无add函数,也就是说该类的对象不能有子对象。
string name;
C(string n){name=n;}
void f(){
if(!v.empty()){cout<<name<<"=";
for (vector<int>::size_type i = 0; i!=v.size(); i++) {cout<<v[i]->g()<<",";}
cout<<endl; } } //f结束
string g(){return name;}
void pt(){ f();
for (vector<int>::size_type i = 0; i!=v.size(); i++) v[i]->pt();} }; //类C结束
int main()
{ //创建对象时传递该对象的名称以便存储。
A ma("ma"),ma1("ma1"),ma2("ma2"),ma3("ma3"),ma4("ma4");
B mb("mb"),mb1("mb1"),mb2("mb2"),mb3("mb3"),mb4("mb4");
C mc("mc"),mc1("mc1"),mc2("mc2"),mc3("mc3"),mc4("mc4");
ma.add(&mb); //ma.v[0]=&mb;
mb.add(&mb1); //mb.v[0]=&mb1;
mb.add(&mb2); //mb.v[1]=&mb2;
ma.add(&mb1); //ma.v[1]=&mb1;
mb1.add(&mb3); //mb1.v[0]=&mb3;
mb1.add(&mb4); //mb1.v[1]=&mb4;
mb2.add(&mc1); //mb2.v[0]=&mc1;
mb2.add(&mc2); //mb2.v[1]=&mc2;
mb2.add(&ma1); //mb2.v[2]=&ma1;
cout<<"各对象拥有的子对象"<<endl;
ma.f(); mb.f(); mb1.f(); mb2.f();
cout<<endl<<"整个对象中结构"<<endl;
ma.pt(); }
运行结果及对象的组织结构如图2-3
2.5.2 QObject类、对象树、生命期
为方便讲解,本文把由QObject及其子类创建的对象称为QObject、QObject对象或Qt对象。在Qt中,QObject对象通常就是指的Qt部件。
QObject类是所有Qt对象的基类,是Qt对象模型的核心,所有Qt部件都继承自QObject。
QObject类既没有复制构造函数也没有赋值操作符函数(实际上它们被声明为私有的),因此无法通过值传递的方式向函数传递一个QObject对象。QObject及其派生类的单形参构造函数应声明为explicit,以避免发生隐式类型转换。
Qt库中的QObject对象是以树状结构组织自已的,当创建一个QObject对象时,可以为其设置父对象,新创建的对象会被加入到父对象的子对象列表中(可通过QObject::children()函数查询),因为Qt的部件类,都是以QObject为基类,因此,Qt的所有部件类都具有对象树的特性。
1、对象树的组织规则如下
每一个QObject对象只能有一个父QObject对象,但可以有任意数量的子QObject对象。比如
A ma; B mb; C mc;
ma.setParent(&mb); //将对象ma添加到mb的子对象列表中,
ma.setParent(&mc); //该语句会把ma从mb的子对象列表中移出,并将其添加到mc的子对象列表中。setParent函数见后文。
QObject对象会把指向各个子对象地址的指针放在QObjectList之中。QObjectList是QList<QObject*>的别名,QList是Qt的一个列表容器。
2、对象删除规则如下(注意:以下规则并非C++语法规则,而是Qt的对象树规则):
基本规则:父对象会自动删除子对象。父对象拥有对象的所有权,在父对象被删除时会在析构函数中自动删除其子对象。
手动删除子对象:当手动删除子对象时,会把该子对象从父对象的列表中移除,以避免父对象被删除时该子对象被再次删除。总之QObject对象不会被删除两次。
当一个对象被删除时,会发送一个destroyed()信号,可以捕捉该信号以避免对QObject对象的悬垂引用。
3、对象创建规则如下
子对象通常应创建在堆中(使用new创建),当父对象被删除时,会自动删除该子对象,此时就不再需要使用delete将其删除了。
对于Qt程序,父对象通常创建在栈上,不应创建在堆上(使用new创建)
子对象不应创建在栈中,因为若父对象比子对象更早的结束生命期(即父对象创建于子对象之后),则子对象会被删除两次,第一次发生在父对象生命期结束时,由Qt对象树的规则,使用父对象删除子对象,第二次发生在子对象生命期结束时,由C++规则删除子对象。这种错误可使用先创建父对象后创建子对象的方法解决,依据C++规则,子对象会先被删除,由Qt对象树规则知,此时子对象会从父对象的列表中移除,当父对象结束生命期时,就不会再次删除子对象了。
4、设置父对象的方法如下
创建对象时,在构造函数中指定父对象。QObject类及其子类都有一个形如QObject *parent=0形参的构造函数,因此可以在创建对象时在构造函数中直接指定父对象。
使用void QObject::setParent(QObject *parent)函数为该对象设置父对象。
5、其他规则
应确保每一个QObject对象在QApplication之后创建,在QApplication销毁之前销毁,因此QObject对象不能是static存储类型的,因为static对象将在main()返回之后才被销毁,其销毁时间太迟了。
6、对象名称
可以为每个对象设置一个对象名称,其主要作用是方便对对象树进行查询和管理。对象名称和对象是不同的,比如A ma; 其中ma是对象,若为ma设置一个名称为“SSS”,则对象ma的对象名称为“SSS”。
对象名称由QObject的objectName属性指定(默认值为空字符串),该属性的读取和设置函数分别如下所示(注:对象的类名可通过QMetaObject::className()查询。)
Qstring objectName() const //读取该对象名称
void setObjectName(const QString &name); //设置该对象的名称为name
示例2.15:Qt的对象树与对象的删除
#include<QObject>
#include <iostream>
using namespace std;
class A:public QObject{public: //子类化QObject
A(){} A(QObject *p):QObject(p){}
~A(){cout<<objectName().toStdString()<<"=~A"<<endl;} };
class B:public QObject{public: //子类化QObject
B(){} B(QObject *p):QObject(p){}
~B(){cout<<objectName().toStdString()<<"=~B"<<endl;} };
int main(int argc, char *argv[]){
A ma; //父对象通常创建在栈上
A *pa1=new A(&ma); //在堆上指定父对象
A *pa2=new A(&ma); B *pb1=new B(pa1); B *pb2=new B(pa1);
ma.setObjectName("ma"); //为各对象设置对象名
pa1->setObjectName("pa1"); pa2->setObjectName("pa2");
pb1->setObjectName("pb1"); pb2->setObjectName("pb2");
A ma1; B mb1;
mb1.setParent(&ma1); //在栈上把ma1指定为mb1的父对象,此处父对象创建于子对象之前。
ma1.setObjectName("ma1"); mb1.setObjectName("mb1");
B mb2; A ma2;
//mb2.setParent(&ma2); /*错误,在栈上指定父对象时,父对象应创建于子对象之前,此处会导致子对象mb2被删除两次。*/
return 0; }
运行结果如图2-5所示
7、查询对象树的信息,可使用以下QObject类中的成员函数
示例2.16:查询对象树
#include<QDebug> //需使用qDebug()函数,该函数的用法类似于C++的cout
#include<QObject>
class A:public QObject{ public: A(){} A(QObject *p):QObject(p){} void f(){qDebug()<<"AF"; }};
class B:public QObject{ public:B(){} B(QObject *p):QObject(p){} void f(){qDebug()<<"BF";}};
class C:public QObject{public: int c;
C(){} C(QObject *p):QObject(p){} void f(){qDebug()<<"CF";}};
int main(int argc, char *argv[]){
A ma; A *pa1=new A(&ma); A *pa2=new A(&ma);
B *pb1=new B(pa1); B *pb2=new B(pa1); C *pc1=new C(pb1); C *pc2=new C(pb1);
ma.setObjectName("ma"); pa1->setObjectName("pa1"); pa2->setObjectName("pa2");
pb1->setObjectName("pb1"); pb2->setObjectName("pb2");
pc1->setObjectName("pc1"); pc2->setObjectName("pc2");
pc2->c=2; pc1->c=1;
QObjectList st= ma.children();
qDebug()<<"ma="<<ma.children();
//以上输出:ma= (QObject(0x82d638, name = "pa1"), QObject(0x82d478, name = "pa2"))
qDebug()<<"pb1="<<pb1->children();
//以上输出:pb1= (QObject(0x840f38, name = "pc1"), QObject(0x840bf0, name = "pc2"))
qDebug()<<"\n##### dumpObjectTree #####";
ma.dumpObjectTree(); //输出见图2-6
qDebug()<<"############";
pb1->dumpObjectTree(); //输出见图2-6
qDebug()<<"\n###### findChild #####";
C* p1=ma.findChild<C*>("pc1"); //通过父对象ma获取指向子对象pc1的指针。
p1->f(); //输出CF
qDebug()<<p1->c; //输出1
C* p2=ma.findChild<C*>("pc2",Qt::FindDirectChildrenOnly); /*获取ma的直接子对象中名称为pc2的对象的指针,因为pc2不是ma的直接子类,所以该处返回NULL。*/
//qDebug()<<p2->c; //错误,此时p2指向的是NULL
qDebug()<<"\n###### findChildren #####";
QList<C*> s=ma.findChildren<C*>("pc1"); //Qt::FindDirectChildrenOnly
qDebug()<<s; //输出:(QObject(0x840f38, name = "pc1"))
qDebug()<<s[0]->c; //输出1
QList<C*> s1=ma.findChildren<C*>("pc2",Qt::FindDirectChildrenOnly);
qDebug()<<s1; //输出一个空圆括号,因为pc2不是ma的直接子对象。
//查找ma的直接子对象
QList<C*> s2=ma.findChildren<C*>(QString(),Qt::FindDirectChildrenOnly);
qDebug()<<s2; //输出:(QObject(0x82d638, name = "pa1"), QObject(0x82d478, name = "pa2"))
QList<C*> s3=ma.findChildren<C*>(); //获取ma的整个对象树结构的列表
qDebug()<<s3;
//以上输出: (QObject(0x82d638, name = "pa1"), QObject(0x840f00, name = "pb1"),
//QObject(0x840f38, name = "pc1"), QObject(0x840bf0, name = "pc2"),
//QObject(0x840b48, name = "pb2"), QObject(0x82d478, name = "pa2"))
return 0; }
运行结果及说明见图2-6
本文作者:黄邦勇帅(原名:黄勇)