标准 C++ 对象模型在运行时效率方面卓有成效,但是在某些特定问题域下的静态特性就显得捉襟见肘。GUI 界面需要同时具有运行时的效率以及更高级别的灵活性。为了解决这一问题,Qt “扩展”了标准 C++。所谓“扩展”,实际是在使用标准 C++ 编译器编译 Qt 源程序之前,Qt 先使用一个叫做 moc(Meta Object Compiler,元对象编译器)的工具,先对 Qt 源代码进行一次预处理(注意,这个预处理与标准 C++ 的预处理有所不同。Qt 的 moc 预处理发生在标准 C++ 预处理器工作之前,并且 Qt 的 moc 预处理不是递归的。),生成标准 C++ 源代码,然后再使用标准 C++ 编译器进行编译。如果你曾经为信号函数这样的语法感到奇怪(现在我们已经编译过一些 Qt 程序,你应当注意到了,信号函数是不需要编写实现代码的,那怎么可以通过标准 C++ 的编译呢?),这其实就是 moc 进行了处理之后的效果。
Qt 使用 moc,为标准 C++ 增加了一些特性:
- 信号槽机制,用于解决对象之间的通讯,这个我们已经了解过了,可以认为是 Qt 最明显的特性之一;
- 可查询,并且可设计的对象属性;
- 强大的事件机制以及事件过滤器;
- 基于上下文的字符串翻译机制(国际化),也就是 tr() 函数,我们简单地介绍过;
- 复杂的定时器实现,用于在事件驱动的 GUI 中嵌入能够精确控制的任务集成;
- 层次化的可查询的对象树,提供一种自然的方式管理对象关系。
- 智能指针(QPointer),在对象析构之后自动设为 0,防止野指针;
- 能够跨越库边界的动态转换机制。
通过继承QObject
类,我们可以很方便地获得这些特性。当然,这些特性都是由 moc 帮助我们实现的。moc 其实实现的是一个叫做元对象系统(meta-object system)的机制。正如上面所说,这是一个标准 C++ 的扩展,使得标准 C++ 更适合于进行 GUI 编程。虽然利用模板可以达到类似的效果,但是 Qt 没有选择使用模板。按照 Qt 官方的说法,模板虽然是内置语言特性,但是其语法实在是复杂,并且由于 GUI 是动态的,利用静态的模板机制有时候很难处理。而自己使用 moc 生成代码更为灵活,虽然效率有些降低(一个信号槽的调用大约相当于四个模板函数调用),不过在现代计算机上,这点性能损耗实在是可以忽略。
在本节中,我们将主要介绍 Qt 的对象树。还记得我们前面在MainWindow
的例子中看到了 parent 指针吗?现在我们就来解释这个 parent 到底是干什么的。
QObject
是以对象树的形式组织起来的。当你创建一个QObject
对象时,会看到QObject
的构造函数接收一个QObject
指针作为参数,这个参数就是 parent,也就是父对象指针。这相当于,在创建QObject
对象时,可以提供一个其父对象,我们创建的这个QObject
对象会自动添加到其父对象的children()
列表。当父对象析构的时候,这个列表中的所有对象也会被析构。(注意,这里的父对象并不是继承意义上的父类!)这种机制在 GUI 程序设计中相当有用。例如,一个按钮有一个QShortcut
(快捷键)对象作为其子对象。当我们删除按钮的时候,这个快捷键理应被删除。这是合理的。
QWidget
是能够在屏幕上显示的一切组件的父类。QWidget
继承自QObject
,因此也继承了这种对象树关系。一个孩子自动地成为父组件的一个子组件。因此,它会显示在父组件的坐标系统中,被父组件的边界剪裁。例如,当用户关闭一个对话框的时候,应用程序将其删除,那么,我们希望属于这个对话框的按钮、图标等应该一起被删除。事实就是如此,因为这些都是对话框的子组件。
当然,我们也可以自己删除子对象,它们会自动从其父对象列表中删除。比如,当我们删除了一个工具栏时,其所在的主窗口会自动将该工具栏从其子对象列表中删除,并且自动调整屏幕显示。
我们可以使用QObject::dumpObjectTree()
和QObject::dumpObjectInfo()
这两个函数进行这方面的调试。
Qt 引入对象树的概念,在一定程度上解决了内存问题。
当一个QObject
对象在堆上创建的时候,Qt 会同时为其创建一个对象树。不过,对象树中对象的顺序是没有定义的。这意味着,销毁这些对象的顺序也是未定义的。Qt 保证的是,任何对象树中的 QObject
对象 delete 的时候,如果这个对象有 parent,则自动将其从 parent 的children()
列表中删除;如果有孩子,则自动 delete 每一个孩子。Qt 保证没有QObject
会被 delete 两次,这是由析构顺序决定的。
如果QObject
在栈上创建,Qt 保持同样的行为。正常情况下,这也不会发生什么问题。来看下下面的代码片段:
{ QWidget window; QPushButton quit("Quit", &window); }
作为父组件的 window 和作为子组件的 quit 都是QObject
的子类(事实上,它们都是QWidget
的子类,而QWidget
是QObject
的子类)。这段代码是正确的,quit 的析构函数不会被调用两次,因为标准 C++ (ISO/IEC 14882:2003)要求,局部对象的析构顺序应该按照其创建顺序的相反过程。因此,这段代码在超出作用域时,会先调用 quit 的析构函数,将其从父对象 window 的子对象列表中删除,然后才会再调用 window 的析构函数。
但是,如果我们使用下面的代码:
{ QPushButton quit("Quit"); QWidget window; quit.setParent(&window); }
情况又有所不同,析构顺序就有了问题。我们看到,在上面的代码中,作为父对象的 window 会首先被析构,因为它是最后一个创建的对象。在析构过程中,它会调用子对象列表中每一个对象的析构函数,也就是说, quit 此时就被析构了。然后,代码继续执行,在 window 析构之后,quit 也会被析构,因为 quit 也是一个局部变量,在超出作用域的时候当然也需要析构。但是,这时候已经是第二次调用 quit 的析构函数了,C++ 不允许调用两次析构函数,因此,程序崩溃了。
由此我们看到,Qt 的对象树机制虽然帮助我们在一定程度上解决了内存问题,但是也引入了一些值得注意的事情。这些细节在今后的开发过程中很可能时不时跳出来烦扰一下,所以,我们最好从开始就养成良好习惯,在 Qt 中,尽量在构造的时候就指定 parent 对象,并且大胆在堆上创建。
50 评论
只能指针(QPointer),在对象析构之后自动设为 0,放置野指针;
应该是智能指针。
多谢指出,已经修改!
原来qt是这样析构的,这真不智能呀,受教了...
博主你说“在 Qt 中,尽量在构造的时候就指定 parent 对象,并且大胆在堆上创建。”,可是您又在《hello world》中拒了一个在堆上创建QLabel的反例。求解惑。
在 main() 函数中,不应该在堆上面创建对象。这是由于如果在 main() 中在堆上面创建对象,app.exec() 函数是一个死循环,创建出的这个对象没有办法被 delete(不开启事件循环,组件就不能显示,不显示组件就不能 delete,否则你还创建它干什么呢?)。另外的原因是,由于我们的 QApplication 是在栈上面创建的,在堆上面创建的 QLabel 对象生命周期要长于 QApplication,这在 Qt 中是应该避免的。而对于我们自己定义的组件就没有这个问题,因为不在 main() 函数中,我们始终可以保证最晚在关闭时销毁(当然是不发生内存泄露的情况下),也就没有这个问题。
豆哥,你好;
你说的这句话我不是很理解“另外的原因是,由于我们的 QApplication 是在栈上面创建的,在堆上面创建的 QLabel 对象生命周期要长于 QApplication,这在 Qt 中是应该避免的。而对于我们自己定义的组件就没有这个问题,因为不在 main() 函数中,我们始终可以保证最晚在关闭时销毁(当然是不发生内存泄露的情况下),也就没有这个问题。”
为什么我们自己定义的组件不在 main() 函数中,可以解释一下吗?
谢谢!
我们自己定义的组件不在 main() 函数直接被调用,而是通过 MainWindow 之类的顶层窗口调用,而 MainWindow 通常直接被 main() 实例化。因此,只要保证了最顶层的 MainWindow 能够正确释放,并且 parent 能够连接成链,就可以保证每一个组件被正确释放。
有个问题需要请教豆子,假如一个继承自QObject的堆对象添加一个继承自QObject的栈对象,会有问题吗?如下代码:
QStatusBar* status=window.statusBar();
QLabel lab("status");
status->addWidget(&lab);
QLabel先于QStatusBar释放,会不会出现2次delete?还是您在此文中说的QLabel释放的时候会将自己从QStatusBar的子对象集合中删除,从而避免2次delete?
不会有问题的,子组件在释放的时候会自动从父组件的子对象集合中删除。否则的话,应用程序中所有弹出的对话框岂不是都不能释放空间了吗?
我问个比较白的问题,你说在堆、栈上面创建对象,我怎么控制?我怎么知道它是创建到哪里的?
使用 new 创建的对象全部是在堆上面创建的,不使用 new 的都是在栈上创建的。这是标准 C++ 规定的。
o.o,我看了C++的语法都没看见这个。
一般应该有的,不过的确就是这么回事,嘿嘿
不使用new的对象的private数据成员一定是栈上创建的,但不能保证这个对象的构造函数里没有new操作。所以不使用new的不是说一定在栈上创建,应该说编译器会帮我们管理内存。
最后那个例子的避免方法就是在堆上创建。
很可能是不是跳出来烦扰一下
应该是时不时吧,虽然不是程序方面的问题,但如果以后做pdf文档的话修正一下比较好
什么跳出来了啊?
抱歉,没注意到我的邮箱写错了
这些细节在今后的开发过程中很可能 是不是 跳出来烦扰一下===修改成:这些细节在今后的开发过程中很可能 时不时 跳出来烦扰一下
感谢指出,已经修改过了
第一段“现在智能指针我们已经编译过一些 Qt 程序”一句,语义无法理解……“智能指针”是否为笔误?//顺带对辛苦奉献表达衷心的感谢! 🙂
多谢指出!的确应该是笔误
“放置野指针” 应该是 “防止野指针” 吧
已经修改过了,感谢指出!
请问豆子,文章最后那个例子,如果都在堆上创建,是否也会delete两次?
啊。。我傻了。new 出来的不会自己释放
但是如果new的时候指定父对象,就不需要自己手动释放了
谁分配谁释放,Qt这种释放方式太暴力了。
这里你理解有错误,本来只有动态创建的对象才应该设置父对象,栈上创建的对象不应该设置父对象
我觉得你对Qt对象模型理解有点偏差,父子对象是为了简化动态分配的对象的管理,本来就应该是只有动态创建的对象才可以设置父对象,栈上创建的对象本来就不应该设置父对象
这个不知道算不算偏差。毕竟这样写还是正确的,既然提供了 setParent() 函数,也就可以设置栈上对象的父对象。这一点无论从语法还是语义来说都是正常的。这段代码也并不是鼓励这样使用,只是提醒一下可能出现的问题。毕竟文档中也有这样的例子。
首先非常感谢博主的辛勤劳动,为我等qt初学者提供了非常棒的学习教程~
关于父对象我有个问题请问豆子:
如果有一个子对象在堆上创建,并在创建的时候设置父对象为A,然后这个子对象又手动调用setParent()设置父对象为B
1. 这样合不合法,子对象在A对象中会被取消吗?
2. 如果A,B两个对象能共用一个子对象,那么在A对象销毁的时候,是否也把这个子对象销毁了呢? 这样B再访问这个子对象的时候,程序不就崩溃了嘛。
setParent() 函数会按照预想的方式进行:将对象从原来的父对象中移除。所以并不是发生你所担心的第二个问题。
看上去原则就是按对象树的顺序从上层到下层依次创建,先容器后内容
比如说我在头文件里定义了一个类
class PaintWidget:public QWidget
{
public:
PaintWidget(QWidget*parent=0)
}
然后在cpp中写
PaintWidget::PaintWidget(QWidget*parent):QWidget(parent)
PaintWidget这个派生类把parent指针给了基类QWidget了 他是怎么通过基类来指定父对象的? 这样的话不就成了指定了基类的父对象了吗?
这里是调用了父类的构造函数,parent 属性是由父类定义的,可以认为是通过继承作为自己的属性。
其实可不可以理解成:
每次构建时,先定义父级,再定义子级,这样即使在构建子级的时候不传递parent,再后面使用setParent也没有问题。
比如
QWidget window;
QPushButton quit("Quit");
quit.setParent(&window);
这种情况就可以了
这样写也是可以的,只不过有点麻烦,而且很容易忘记
特意跑了下博主的代码,果然会崩。后来想下这种情况基本不会出现在代码中,也难怪之前自己都没有发现这个可以崩溃。作为UI控件在程序段中用栈分配,我想了半天还真没有想到实际的应用。不过博主的例子还是发人深省的,赞一个
如果是函数内的局部对象就在栈上创建,没必要添加一个父类来依靠Qt的内存管理机制。
如果是在堆上创建的对象,就依靠Qt的管理机制,给其添加一个父类对象。这样是不是比较OK?
一般是这样子的。
对象的析构讲的很到位,感谢豆子的分享
博主 给窗口添加部件是不是就代表隐藏制定部件的父类是窗口 比如:
mainwindow->addDockWidget(Qt::LeftDockWidgetArea,dock);//主窗口添加浮动窗口
是不是就代表dock部件父类是主窗口 还是说我必须在new时候制定:
QDockWidget *dock=new QDockWidget(QObject::tr("File Browser"),mainwindow);
还有一个问题就是在布局套布局情况下 子布局的父类可以是布局吗 如果两个布局的父类都指向同一个部件时候 系统就会提示部件已经存在布局:
QLayout: Attempting to add QLayout "" to QDockWidget "", which already has a layout
博主好人 求解答!!
对于第一个问题,有些函数是会自动设置 parent 指针,有些则不会,这需要仔细阅读文档。例如,addDockWidget() 就不会,而 addLayout() 函数说明中有这么一句:layout becomes a child of the grid layout,因此就会自动设置 parent 指针。为避免这一情况,我们还是在创建时显式指定,这样无论 Qt 会不会修改 parent 指针,都能保证自动销毁。
对于第二个问题,目前只有 QBoxLayout 和 QGridLayout 有 addLayout() 函数。一个组件只能设置一个布局,如果需要类似的嵌套,可以尝试使用组件的嵌套替代。
博主 你好:感谢你的教程,下面这个例子我有一点不同的看法:
QPushButton quit("Quit");
QWidget window;
quit.setParent(&window);
你说会导致quit两次引起崩溃? 我觉得应该是:window在析构的时候delete子对象 quit 引起的,而不因为quit析构两次,因为quit是在栈上分配的。
for (int i = 0; i < children.count(); ++i) {
currentChildBeingDeleted = children.at(i);
children[i] = 0;
delete currentChildBeingDeleted;
}
delete currentChildBeingDeleted; //这里导致崩溃!
个人觉得这里的 parent 翻译成"容器对象"更易于初学者理解一点.
豆哥,另外的原因是,由于我们的 QApplication 是在栈上面创建的,在堆上面创建的 QLabel 对象生命周期要长于 QApplication,这在 Qt 中是应该避免的
我找的资料说先析构堆上的在析构栈上的
这个我也不大清楚,因为栈上的是超出作用域就会被析构,而堆上的则是需要手动调用 delete 才会被析构。所以这个到底谁先析构,就得看是怎么编写的了吧
“{
QPushButton quit("Quit");
QWidget window;
quit.setParent(&window);
}
情况又有所不同,析构顺序就有了问题。我们看到,在上面的代码中,作为父对象的 window 会首先被析构,因为它是最后一个创建的对象。在析构过程中,它会调用子对象列表中每一个对象的析构函数,也就是说, quit 此时就被析构了。然后,代码继续执行,在 window 析构之后,quit 也会被析构,因为 quit 也是一个局部变量,在超出作用域的时候当然也需要析构。但是,这时候已经是第二次调用 quit 的析构函数了,C++ 不允许调用两次析构函数,因此,程序崩溃了。”
这个例子没有理解,当我点击quit的时候,他肯定关联了window的close,所以window在调用close的时候,自然会将其子对象都delet掉,怎么会出现quit delet 两次的情况呢?
因为 quit 按钮是定义在 {} 中的局部变量。window 析构的同时销毁其所有子对象 quit 是 Qt 定义的操作,超出作用域析构作用域中所有局部变量是 C++ 定义的操作。所以在超出 {} 时,window 的析构会销毁 quit,而 quit 自己也有销毁自己的操作,所以 quit 析构了两次。