03-1_Qt 5.9 C++开发指南_番外1_QWidget项目默认代码解析(Qt对象模型:对象树与元对象系统;初始化列表用于调用父类有参构造和初始化)

我们在创建Qt Widgets Application的时候,会默认生成一些代码,本篇主要是对其涉及到的内容进行解析。

文章目录

  • [1. 创建Qt Widgets Application程序-WidgetTest](#1. 创建Qt Widgets Application程序-WidgetTest)
  • [2. 首先从main.cpp开始分析:](#2. 首先从main.cpp开始分析:)
  • [3. Qt对象模型:对象树与元对象系统](#3. Qt对象模型:对象树与元对象系统)
    • [3.1 对象树的概念](#3.1 对象树的概念)
    • [3.2 对象树的示例程序](#3.2 对象树的示例程序)
    • [3.3 元对象系统的概念](#3.3 元对象系统的概念)
  • [4. WidgetTest.h](#4. WidgetTest.h)
  • [5. WidgetTest.cpp](#5. WidgetTest.cpp)

1. 创建Qt Widgets Application程序-WidgetTest

会创建WidgetTest.pro(配置文件,此处不做解析)、WidgetTest.h、main.cpp、WidgetTest.cpp(我这里习惯使用代码编写界面,没有自动生成界面文件,代码会与使用UI设计器进行界面存在差异,需要的话后期再做解析)

WidgetTest.h

cpp 复制代码
#ifndef WIDGETTEST_H
#define WIDGETTEST_H

#include <QWidget>

class WidgetTest : public QWidget
{
    Q_OBJECT

public:
    WidgetTest(QWidget *parent = 0);
    ~WidgetTest();
};

#endif // WIDGETTEST_H

main.cpp

cpp 复制代码
#include "WidgetTest.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    WidgetTest w;
    w.show();

    return a.exec();
}

WidgetTest.cpp

cpp 复制代码
#include "WidgetTest.h"

WidgetTest::WidgetTest(QWidget *parent)
    : QWidget(parent)
{
}

WidgetTest::~WidgetTest()
{

}

2. 首先从main.cpp开始分析:

cpp 复制代码
#include "WidgetTest.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    WidgetTest w;
    w.show();

    return a.exec();
}

main函数是程序的入口,这个就不做详细介绍,下面对QApplication a(argc,argv);'和return a.exec();进行解析

(1)QApplication a(argc,argv);

定义一个 Qt 应用程序对象,它的构造函数接收和 main 函数一样的参数,是 Qt 图形界面程序的入口,就像 main 函数是普通 C++ 程序的入口一样。

(2)return a.exec();

会进入 Qt 应用程序的事件循环函数等待用户操作和系统的消息然后进行处理

常见的c/c++语言main函数中都是直接return 0 的,程序直接退出。但图形程序通常需要与用户交互,不会自动关闭,而是一直等待用户操作。如果用户点击窗口的关闭按钮, 程序才会结束并返回一个值,默认是 0 。

qt中main函数开始就只是进行一个初始化工作,然后将控制权交给qt,接下来所有事件的处理就只剩下qt的事件循环处理了。

(3)WidgetTest w;

不管这个w实例是dialog或者Widgrt,都会在栈上创建,而不是new出来,这是为啥?

原因:main的窗口是顶层窗口,应用程序会形成以此为根基的对象树,顶层窗口关闭后,如果没有销毁,而是隐藏了,那么在退出的时候没有手动销毁的话,其延伸出来的任何部件都不会释放。所以,在顶层窗口上创建的部件,只 需要制定其parent的参数为this,也就是widget为父窗口就不需要进行delete操作,Qt的对象树使用析构函数会替我们自己完成。

w.show();是显示方法,用来显示窗口

WidgetTest w;的详细解释涉及到了对象树与元对象系统,下面对其进行详细介绍

3. Qt对象模型:对象树与元对象系统

3.1 对象树的概念

Qt中使用对象树(object tree)来组织和管理所有的QObject类及其子类的对象。当创建一个QObject时,如果使用了其他的对象作为其父对象(parent),那么这个 QObject就会被添加到父对象的children()列表中,这样当父对象被销毁时,这个QObject也会被销毁。实践表明,这个机制非常适合于管理GUI对象。例如,一个 QShortcut(键盘快捷键)对象是相应窗口的一个子对象,所以当用户关闭了这个窗口 时,这个快捷键也可以被销毁。

QWidget作为能够在屏幕上显示的所有部件的基类,扩展了对象间的父子关系。 一个子对象一般也就是一个子部件,因为它们要显示在父部件的区域之中。例如,当关闭一个消息对话框(message box)后要销毁它时,消息对话框中的按钮和标签也会被销毁,这也正是我们所希望的,因为按钮和标签是消息对话框的子部件。当然,也可以自己来销毁一个子对象。关于这一部分内容,可以在帮助索引中査看Object Trees & Ownership关键字。

3.2 对象树的示例程序

新建Qt Gui应用,项目名称为myOwnership,基类选择QWidget,然后类名保持Widget不变。完成后向项目中添加新文件,模板选择C+ +类,类名为MyButton,基类为QPushButton,类型信息选择"继 承自QWidget"。添加完文件后将mybuuon. h文件修改如下:

cpp 复制代码
#ifndef MYBUTTON_H
#define MYBUTTON_H
 
#include <QPushButton>
#include <QDebug>
 
class MyButton : public QPushButton
{
    Q_OBJECT
public:
    explicit MyButton(QWidget *parent = nullptr);
    ~MyButton();
};
 
#endif // MYBUTTON_H

这里主要是添加了析构函数的声明。然后到mybutton. cpp文件中,修改如下:

cpp 复制代码
#include "mybutton.h"
 
MyButton::MyButton(QWidget *parent) :
    QPushButton(parent)
{
}
 
MyButton::~MyButton()
{
    qDebug() << "delete button";
}

这里添加了析构函数的定义,这样当 MyButton 的对象被销毁时,就会输出相应的信息。

下面到widget.cpp文件中,修改如下:

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include "mybutton.h"
#include <QDebug>
#include <QHBoxLayout>
 
Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
 
    //创建按钮部件,指定widget为父部件
    MyButton *button = new MyButton(this);
    button->setText(tr("button"));
}
 
Widget::~Widget()
{
    delete ui;
    qDebug() << "delete widget";
}

当Widget窗口被销毁时,将输出信息。下面运行程序,然后关闭窗口,在Qt Creator的应用程序输出栏中的输出信息为:

cpp 复制代码
delete widget
delete button

可以看到,当关闭窗口后,因为该窗口是顶层窗口,所以应用程序要销毁该窗口部件(如果不是顶层窗口,那么关闭时只是隐藏,不会被销毁),而当窗口部件销毁时会自动销毁其子部件。这也就是为什么在Qt中经常只看到new操作而看不到delete操作 的原因。

再来看一下main.cpp文件,其中Widget对象是建立在栈上的:

cpp 复制代码
Widget w;
w.show();

这样对于对象w,在关闭程序时会自动销毁。而对于Widget中的部件,如果是在堆上创建(使用new操作符),那么只要指定Widget为其父窗口就可以了,也不需要进行delete操作。当对象w销毁时会自动销毁它的所有子部件,这些都是Qt的对象树所完成的。

所以,对于规范的Qt程序,我们要在main()函数中将主窗口部件创建在栈上,例如"Widget w;",而不要在堆上进行创建(使用new操作符)。对于其他窗口部件,可以使用new操作符在堆上进行创建,不过一定要指定其父部件,这样就不用使用de­lete操作符来销毁该对象了。

3.3 元对象系统的概念

Qt中的元对象系统(Meta-Object System)提供了对象间通信的信号和槽机制、运行时类型信息和动态属性系统。元对象系统是基于以下3个条件的:

  • 该类必须继承自QObject类;
  • 必须在类的私有声明区声明Q_OBJECT宏(在类定义时,如果没有指定public或者private,则默认为private);
  • 元对象编译器Meta-Object Compiler(moc),为QObject的子类实现元对象特性提供必要的代码。

其中,moc工具读取一个C+ +源文件,如果它发现一个或者多个类的声明中包含有Q_OBJECT宏,便会另外创建一个C+ +源文件(就是在项目目录中的debug目录 下看到的以moc开头的C+ +源文件),其中包含了为每一个类生成的元对象代码。 这些产生的源文件或者被包含进类的源文件中,或者和类的实现同时进行编译和链接。

元对象系统主要是为了实现信号和槽机制才被引入的,不过除了信号和槽机制以外,元对象系统还提供了其他一些特性:

  • QObjeCt::metaObject()函数可以返回一个类的元对象,它是QMetaObject类的对象;
  • QMetaObject::className()可以在运行时以字符串形式返回类名,而不需要C+ +编辑器原生的运行时类型信息(RTTI)的支持
  • QObject:: "inherits()函数返回一个对象是否是QObject继承树上一个类的实例的信息;
  • QObject: :tr()和QObject: :trUtf8()迸行字符串翻译来实现国际化;
  • QObject::setProperty()和QObject::property()通过名字来动态设置或者获取对象属性;
  • QMetaObject: :newlnstance()构造该类的一个新实例。

除了这些特性,还可以使用qobject_cast()函数来对QObject类进行动态类型转换,这个函数的功能类似于标准C+ +中的dynamic_cast()函数,但它不再需要RTTI的支持。这个函数尝试将它的参数转换为尖括号中的类型的指针,如果是正确的类型,则返回一个非零的指针,如果类型不兼容则返回0。例如:

cpp 复制代码
QObject *obj = new MyWidget;
QWidget *widget = qobject_cast<QWidget *>(obj);

信号和槽机制是Qt的核心内容,而信号和槽机制必须依赖于元对象系统,所以它是Qt中很关键的内容。这里只是说明了它的一些应用,关于它的具体实现机制,这里不再讲述。关于元对象系统的具体描述,可以在Qt中查看The Meta Object System关键字。

此部分链接地址:https://www.cnblogs.com/linuxAndMcu/p/11026850.html和[Qt对象模型:对象树与元对象系统_对象树的概念-CSDN博客](https://blog.csdn.net/QtCompany/article/details/131794829?ops_request_misc={"request_id"%3A"171281599416800182110192"%2C"scm"%3A"20140713.130102334.pc_blog."}&request_id=171281599416800182110192&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-131794829-null-null.nonecase&utm_term=对象树&spm=1018.2226.3001.4450)

4. WidgetTest.h

cpp 复制代码
#ifndef WIDGETTEST_H
#define WIDGETTEST_H

#include <QWidget>

class WidgetTest : public QWidget
{
    Q_OBJECT

public:
    WidgetTest(QWidget *parent = 0);
    ~WidgetTest();
};

#endif // WIDGETTEST_H

以下代码用于避免重复定义,不细讲

cpp 复制代码
#ifndef WIDGETTEST_H
#define WIDGETTEST_H
......
#endif // WIDGETTEST_H

以下代码定义了公共一个继承自QWidget的WidgetTest类

cpp 复制代码
class WidgetTest : public QWidget
{
    Q_OBJECT

public:
    WidgetTest(QWidget *parent = 0);
    ~WidgetTest();
};
  • Q_OBJECT宏定义是为了实现元对象系统,具体见3.3;

  • WidgetTest(QWidget *parent = 0);是一个有参构造,其形参是其基类指针,基类指针初始定义为0,相当于一个默认参数。

  • ~WidgetTest();析构函数就不细讲,

5. WidgetTest.cpp

cpp 复制代码
#include "WidgetTest.h"

WidgetTest::WidgetTest(QWidget *parent)
    : QWidget(parent)
{
}

WidgetTest::~WidgetTest()
{

}

对于WidgetTest::WidgetTest(QWidget *parent) : QWidget(parent) {}的含义进行解释:

在讲解原因之前,先请大家看下面的一个例子:

cpp 复制代码
#include <iostream>
using namespace std;
class Base
{
public:
    Base() :m_num(0){
        cout << "this is Base()" << endl;
    }
    Base(int val):m_num(val){
        cout << "this is Base(int val)" << endl;
    }
private:
    int m_num;
};

上方代码定义了一个基类Base,并且有两个构造函数,一个是默认构造函数,一个是有一个整型参数的构造函数。

cpp 复制代码
class BaseChild: public Base
{
public:
    BaseChild(){
        cout << "this is BaseChild()" << endl;
    }
    BaseChild(int val): Base(val){
        cout << "this is BaseChild(val)" << endl;
    }
private:
    int m_num;
};

上方代码定义了一个BaseChild类,并继承Base类,同样的,它也定义了两个构造函数,一个默认,一个有整型参数。

cpp 复制代码
int main(int argc, char *argv[])
{
    BaseChild child1;
    BaseChild child2(5);

    return 0;
}

main函数实例化了两个子类实例,child1,child2。child1调用默认构造函数。child2调用有整型参数的构造函数。

现在,我们运行程序,会有如下打印:

cpp 复制代码
this is Base()
this is Basechild()
this is Base(int val)
this is Basechild(val)

看到了吗,我们发现:

  • 创建child1时,是先调用了Base的默认构造函数,再调用自己的默认构造函数
  • 创建child2时,是先调用了Base(int)这个构造函数,再调用自己的整型参数构造函数。

所以我们回头看BaseChild的构造函数

cpp 复制代码
BaseChild(int val): Base(val){
        cout << "this is BaseChild(val)" << endl;
    }

细心的同学,可能早就发现了,初始化列表中的Base(val)正是调用了我们Base基类的有参构造函数,而这样的写法就刚好是我们开头代码中的那段Widget::Widget(QWidget *parent) :QWidget(parent)

所以Widget是调用了QWidget下面的构造函数

cpp 复制代码
QWidget(QWidget* parent = Q_NULLPTR, Qt::WindowFlags f = Qt::WindowFlags());

所以得出如下总结:

总结: 如果不指定构造函数,则派生类会调用基类的默认构造函数 · 派生类构造函数的初始化列表只能初始化派生类成员,不能直接初始化继承成员,如果想 要调用基类的有参构造函数,则可以在派生类的初始化列表中显示指定

关于初始化列表部分,可以参考:C++57个入门知识点_31 初始化列表及派生类中的函数隐藏(初始化列表:用于调用父类有参构造并初始化、用于自身成员初始化、对于常量成员初始化;函数隐藏:不同作用域里两个数据名称相同,由内向外隐藏)_初始化列表调用父类构造-CSDN博客

好的,那么我们又提出一个问题,"调用QWidget(parent)这个构造函数,QWidget父类都做了哪些动作呢?"

下面是QWidget源码中的一部分节选:

cpp 复制代码
QWidget::QWidget( QWidget *parent, const char *name, WFlags f )
    : QObject( parent, name ), QPaintDevice( PDT_WIDGET ),
      pal( parent ? parent->palette()		// use parent's palette
           : *qApp->palette() )			// use application palette
{
    if ( parent ) {
	QChildEvent *e = new QChildEvent( Event_ChildInserted, this );
	QApplication::postEvent( parent, e );
    }
}

大家从上面可以看出,如果parent参数非空的话,那么该构造函数使用了其父窗口的调色板,并且发送了QChildEvent事件,这会让新的窗口成为parent所指窗口的子窗口,那么当父窗口被删除时,子窗口也会自动的被删除。

这其实是用到了Qt对象树的概念。

本部分转自:正确理解Widget::Widget(QWidget *parent) :QWidget(parent)这句话,也可参考简单总结的:正确理解Widget::Widget(QWidget *parent) :QWidget(parent)这句话-CSDN博客

相关推荐
shinelord明4 分钟前
【再谈设计模式】建造者模式~对象构建的指挥家
开发语言·数据结构·设计模式
黑不拉几的小白兔19 分钟前
PTA部分题目C++重练
开发语言·c++·算法
写bug的小屁孩21 分钟前
websocket身份验证
开发语言·网络·c++·qt·websocket·网络协议·qt6.3
chordful42 分钟前
Leetcode热题100-32 最长有效括号
c++·算法·leetcode·动态规划
材料苦逼不会梦到计算机白富美1 小时前
线性DP 区间DP C++
开发语言·c++·动态规划
java小吕布1 小时前
Java Lambda表达式详解:函数式编程的简洁之道
java·开发语言
sukalot1 小时前
windows C#-查询表达式基础(一)
开发语言·c#
ahadee1 小时前
蓝桥杯每日真题 - 第12天
c++·vscode·算法·蓝桥杯
一二小选手1 小时前
【Java Web】分页查询
java·开发语言
大G哥1 小时前
python 数据类型----可变数据类型
linux·服务器·开发语言·前端·python