Qt 核心之对象模型、对象树以及对象所有权

 

为什么需要对象模型?

GUI 编程中通常需要管理相当多的对象,Qt 提供的对象模型结合了标准 C++ 的运行效率及它所没有的高灵活性。说白了,是因为标准 C++ 没有提供 Qt GUI 编程所需的对象模型,Qt 才设计出自己的对象模型(Qt Object Model)的。

Qt 向 C++ 扩展出以下功能:

  • 一个强大地无缝对象通信机制:信号与槽
  • 可查询和可设计的对象属性
  • 强大的事件和事件过滤器
  • 用于国际化的多语言翻译机制
  • 一个内部计时器
  • 一个受保护的指针(QPoint),当它指向的对象被析构后自动置为 0

以上这些功能是基于 QObject 的,也就是说基于 Qt 基类的。这样设计出的 Qt 对象模型才能实现上面的这些功能。

把 Qt 对象当作标识( Identity,ID)而非值 (Value)

Qt 扩展出 C++ 的一些特性,要求 Qt 对象应该被看作是标识而非值。标识和值有着很大的不一样:值是用来被复制或者赋值的;而标识是被克隆(cloned)的,这意味着创建 Qt 对象不需要精确地复制旧对象。打个比方,有一对双保胎,她们长得一模一样,但是她们有不同的名字、此刻处在不同的的位置、甚至完全不同的社交圈。

标识和值的关系是否可以类似于函数传参中的“传引用”与“传值”?

一个 Qt 对象有以下需求:

  • 可能唯一的 QObject::objectName()。复制它时如何怎样给新对象取名?
  • 在对象树中有特定的位置。如果要复制它,新对象应该放在对象树的哪里?
  • 一个对象可能通过信号/槽连接到另一个对象。复制的时候怎么把这种连接复制过去?
  • 可以在运行时动态添加属性。复制的时候,怎样把旧对象的动态属性包括进去?

以上种种要求 Qt 对象必须是标识而非值。因为是值的话,无法处理上述几点需求。因此,QObject 的派生类都要求禁用拷贝构造函数和赋值操作符。

class MyClass : public QObject
{
private:
    Q_DISABLE_COPY(MyClass)  // 禁用拷贝构造函数和赋值操作符
};

Qt 中的对象树是怎样的一棵树?

  • Qt 对象存在于一颗对象树中。当创建一个对象并指定了它的父对象时,就把它加入了父对象所在的对象树中。这个对象就是它父对象的儿子,当父对象析构时它也被析构。这种构造对象树的方法在 GUI 对象中被证明是非常好用的。例如 QWindow(窗口) 对象有一个子对象 QShortcut(快捷键),当窗口关闭时,快捷键这个子对象也会随着删除。
  • QQuickItem 分虚拟父对象和数据父对象,前者用于布局,后者用于内存管理。
  • QWidget 是所有 Qt Widgets 的基类,它扩展出它们的 父-子 关系。一个子对象通常也会成为子部件,也就是说它显示在父对象的坐标系统中,并且受限于父对象的坐标边界(将坐标移到父对象以外的部分无法显示)。例如,对话框关掉时,它上面的按钮也被析构。
  • 可以用 QObject::dumpObjectTree() QObject::dumpObjectInfo() 来调试对象树。

对象的构造和析构顺序

分两种情况:在堆中创建和在栈中创建。

  • 在堆中创建(用 new 创建)的对象,假如它指定了父对象,那么对象树会自动管理它的内存,Qt 保证不会析构两次。
  • 在栈中创建的对象,由于标准 C++ 的规定局部对象的析构顺序与构造顺序是相反的,所以要求父子对象有正确的创建顺序。
int main()
{
    QWidget window;
    QPushButton quit("Quit", &window);
    ...
}
// 析构时先析构 quit,quit 从父对象 window 上移除,再析造 window,不会有任何问题。
int main()
{
    QPushButton quit("Quit");
    QWidget window;

    quit.setParent(&window);
    ...
}
// 析构时先析构 window,它的子对象 quit 也随之被析构;按照 C++ 的析造顺序,quit 将再次析构一次,造成程序崩溃!