【QT开发手册】对象模型(对象树) 窗⼝坐标体系
文章目录
- 前言
- 一、 认识对象模型(对象树)
- 二、Qt的析构注意现象
- 三、对象树的调⽤析构函数和释放内存
- 四、 Qt窗⼝坐标体系
- 🚩总结
前言
一、 认识对象模型(对象树)
在Qt中创建很多对象的时候会提供⼀个Parent对象指针,下⾯来解释这个parent到底是⼲什么的。
- QObject是以对象树的形式组织起来的。
当创建⼀个QObject对象时,会看到QObject的构造函数接收⼀个QObject指针作为参数,这个参数就是parent,也就是⽗对象指针。
这相当于,在创建QObject对象时,可以提供⼀个其⽗对象,我们创建的这个QObject对象会⾃动添加到其⽗对象的children()列表。
当⽗对象析构的时候,这个列表中的所有对象也会被析构。(注意,这⾥的⽗对象并不是继承意义上的⽗类!)
这种机制在GUI程序设计中相当有⽤。例如,⼀个按钮有⼀个QShortcut(快捷键)对象作为其⼦对象。当删除按钮的时候,这个快捷键理应被删除。这是合理的。
Qt引⼊对象树的概念,在⼀定程度上解决了内存问题。
-
当⼀个QObject对象在堆上创建的时候,Qt会同时为其创建⼀个对象树。不过,对象树中对象的顺序是没有定义的。这意味着,销毁这些对象的顺序也是未定义的。
-
任何对象树中的QObject对象delete的时候,如果这个对象有parent,则⾃动将其从parent的children() 列表中删除;如果有孩⼦,则⾃动delete每⼀个孩⼦。Qt保证没有QObject会被delete 两次,这是由析构顺序决定的。
二、Qt的析构注意现象
如果QObject在栈上创建,Qt保持同样的⾏为。正常情况下,这也不会发⽣什么问题。来看下⾯的代码⽚段:
作为⽗组件的window和作为⼦组件的quit都是QObject的⼦类(事实上,它们都是QWidget的⼦类,⽽QWidget是QObject的⼦类)。
这段代码是正确的,quit的析构函数不会被调⽤两次,因为标准C++要求,局部对象的析构顺序应该按照其创建顺序的相反过程。因此,这段代码在超出作⽤域时,会先调⽤quit的析构函数,将其从⽗对象window的⼦对象列表中删除,然后才会再调⽤window的析构函数。
但是,如果我们使⽤下⾯的代码:
情况又有所不同,析构顺序就有了问题。我们看到,在上面的代码中,作为父对象的window会首先被析构,因为它是最后一个创建的对象。在析构过程中,它会调用子对象列表中每一个对象的析构函数,也就是说,quit此时就被析构了。
然后,代码继续执行,在window析构之后,quit也会被析构,因为quit也是一个局部变量,在超出作用域的时候当然也需要析构。但是,这时候已经是第二次调用quit的析构函数了,C++不允许调用两次析构函数,因此,程序崩溃了。
由此我们看到,Qt的对象树机制虽然在一定程度上解决了内存问题,但是也引入了一些值得注意的事情。这些细节在今后的开发过程中很可能时不时跳出来烦扰一下,所以,我们最好从开始就养成良好习惯
在Qt中,尽量在构造的时候就指定parent对象,并且⼤胆在堆上创建。
Qt对象树如图:
代码⽰例
- 创建⼀个新⼯程并编译运⾏,⽣成如下窗⼝;
- 选中⼯程名,⿏标右键------->“addnew…”(或"添加新⽂件")
-
选择"choose…",弹出如下界面;
-
点击"下一步",弹出如下对话框;
-
点击"完成"之后,⼿动创建类的头⽂件以及源⽂件会⾃动添加到⽬标⼯程中;
-
修改头⽂件;
#ifndef MYPUSHBUTTON_H
#define MYPUSHBUTTON_H// #include <QWidget>
#include <QPushButton> //修改头文件class MyPushButton : public QPushButton //修改所继承的基类
{Q_OBJECTpublic:MyPushButton(QWidget *parent = nullptr); //默认提供的构造函数~MyPushButton(); //添加手动创建类的析构函数signals:public slots:};#endif // MYPUSHBUTTON_H
7. 编写源⽂件;
#include "mypushbutton.h"
#include <QDebug>MyPushButton::MyPushButton(QWidget *parent) : QPushButton(parent) {qDebug() << "我的按钮的构造函数被调用";
}MyPushButton::~MyPushButton()
{qDebug() << "我的按钮的析构函数被调用";
}
9. 编译并运行;
MyPushButton *btn = new MyPushButton;btn->setText("我的按钮");btn->setParent(this); //设置到对象树中,当窗口关闭时就会自动调用其析构函数
-
当关闭弹出的对话框时,就会⾃动调⽤按钮的析构函数;
-
观察析构函数的执行顺序;
QPushButton quit ( "Quit" ) ;
QWidget window ;
quit.setParent ( &window) ;
-
执⾏结果:
-
执⾏结果分析:
对象树确保的是先释放⼦节点的内存,后释放⽗节点的内存.
⽽析构函数的调⽤顺序则不⼀定遵守上述要求.因此看到⼦节点的析构执⾏顺序反⽽在⽗节点析构顺序之后.
注意:调⽤析构函数和释放内存并⾮是同⼀件事情
三、对象树的调⽤析构函数和释放内存
理解:
我们需要先明确三个核心概念:对象树的内存管理逻辑、析构函数的作用、“调用析构函数”与“释放内存”的区别。下面分步骤解释:
一、对象树的核心目标:确保内存释放顺序(子先父后)
对象树(如Qt的QObject对象树)是一种“父子关系”的对象管理机制,父对象会主动管理子对象的生命周期。其核心目的是避免内存泄漏和悬空指针,因此严格规定了内存释放的顺序:
必须先释放所有子对象的内存,再释放父对象的内存。
原因很简单:如果父对象先释放内存,子对象可能还依赖父对象的资源(比如指针引用),此时子对象就会变成“悬空对象”,操作它会导致程序崩溃。
二、析构函数的作用:不是释放内存,而是“清理资源”
析构函数是对象被销毁前自动调用的成员函数,它的核心作用是清理对象自身占用的“额外资源”(而非释放对象本身的内存)。例如:
- 关闭对象打开的文件、网络连接;
- 释放对象内部动态分配的内存(如对象中用
new
创建的子数据); - 解绑与其他对象的关联等。
析构函数的调用仅仅是“执行清理逻辑”,和“释放对象占用的内存”是完全不同的操作。
三、“调用析构函数”与“释放内存”的本质区别
当我们用delete
销毁一个对象时,底层会执行两个独立的步骤:
- 先调用该对象的析构函数:完成资源清理(如上述的关闭文件、释放内部数据等);
- 再释放对象本身的内存:将对象占用的内存归还给操作系统(此时对象才真正“消失”)。
可见:
- 析构函数调用是“逻辑上的清理”,发生在内存释放之前;
- 内存释放是“物理上的回收”,是对象生命周期的最后一步。
四、为什么析构函数调用顺序可能“子后父先”?
对象树虽然严格保证“子对象内存先释放,父对象内存后释放”,但析构函数的调用顺序可能与此相反(父先调用,子后调用)。
举一个具体例子(以Qt的QObject为例):
- 当父对象被
delete
时,首先触发父对象的析构函数(第一步:调用父析构); - 父对象的析构函数内部会主动遍历子对象列表,逐个
delete
子对象:- 对每个子对象,先调用子对象的析构函数(第二步:调用子析构);
- 子析构完成后,释放子对象的内存(第三步:释放子内存);
- 所有子对象都被销毁后,父对象的析构函数执行完毕,最后释放父对象的内存(第四步:释放父内存)。
此时的顺序是:
- 析构函数调用顺序:父 → 子(父先调用,子后调用);
- 内存释放顺序:子 → 父(子先释放,父后释放)。
这就解释了“子节点的析构执行顺序反而在父节点之后”,但对象树依然保证了内存释放的正确顺序(子先父后)。
四、 Qt窗⼝坐标体系
坐标体系:以左上⻆为原点(0,0),X向右增加,Y向下增加
对于嵌套窗⼝,其坐标是相对于⽗窗⼝来说的。
⽰例:使⽤Qt中的坐标系设置控件的位置;
#include "widget.h"
#include "ui_widget.h"
// 添加“按钮”相关头文件,用于创建按钮控件
#include <QPushButton>
// 用于输出调试信息
#include <QDebug> Widget::Widget(QWidget *parent)// 调用父类构造,传递父部件指针: QWidget(parent) // 初始化 ui 成员(假设 ui 是 Ui::Widget 类型,由 Qt Designer 生成), ui(new Ui::Widget)
{// setupUi 用于初始化界面,将界面元素与当前 Widget 关联ui->setupUi(this); // 创建按钮1,父部件指定为当前 Widget,这样按钮会显示在当前界面上QPushButton *btn1 = new QPushButton("按钮1", this); // 设置按钮1在界面上的坐标(以父部件左上角为原点)btn1->move(200, 300); // 创建按钮2,父部件同样指定为当前 WidgetQPushButton *btn2 = new QPushButton("按钮2", this); // 输出按钮1的坐标信息,x() 获取横坐标,y() 获取纵坐标qDebug() << "按钮1的坐标为: [" << btn1->x() << "," << btn1->y() << "]"; // 输出按钮2的坐标信息qDebug() << "按钮2的坐标为: [" << btn2->x() << "," << btn2->y() << "]";
}Widget::~Widget()
{// 释放 ui 指针指向的内存,避免内存泄漏delete ui;
}
运⾏结果如下图⽰: