Bootstrap

Qt 元对象系统

Qt 元对象系统

Qt元对象系统是对标准C++的扩展,为Qt提供了信号与槽机制、实时类型信息、动态属性系统等功能。

1. 元对象的概念

在计算机科学中,元对象是指能够操纵、创建、描述或执行其他对象的对象。被元对象描述的对象称为基对象。元对象可能包含的信息包括基础对象的类型、接口、类、方法、属性、变量和控制结构等。

2. 元对象系统的核心组件

2.1 QObject

QObject是Qt对象模型的基础类,具有以下关键特性:

  • 对象生命周期管理
  • 对象树和父子关系管理
  • 信号与槽通信机制
  • 属性系统支持
  • 运行时类型信息
class QObject 
{
public:
    explicit QObject(QObject *parent = nullptr);
    virtual ~QObject();
    // 对象树管理
    void setParent(QObject *parent);
    QObject* parent() const;
    // 属性系统
    bool setProperty(const char *name, const QVariant &value);
    QVariant property(const char *name) const;
};

2.2 Q_OBJECT 宏

Q_OBJECT宏必须出现在类定义的私有部分,以启用信号与槽、动态属性系统以及Qt元对象系统提供的其他服务。该宏在编译时由MOC处理,以生成相应的元对象代码。

#define Q_OBJECT \
public: \
    // 编译器警告控制:压入当前警告状态
    QT_WARNING_PUSH \
    
    // 禁止重写警告
    Q_OBJECT_NO_OVERRIDE_WARNING \
    
    // 静态元对象,存储类的元信息
    static const QMetaObject staticMetaObject; \
    
    // 虚函数:返回对象的元对象指针
    virtual const QMetaObject *metaObject() const; \
    
    // 虚函数:运行时类型转换
    virtual void *qt_metacast(const char *); \
    
    // 虚函数:处理元对象调用(信号、槽、属性)
    virtual int qt_metacall(QMetaObject::Call, int, void **); \
    
    // 国际化相关的函数宏
    QT_TR_FUNCTIONS \
    
private: \
    // 属性警告控制
    Q_OBJECT_NO_ATTRIBUTES_WARNING \
    
    // 隐藏的静态元对象调用函数
    Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
    
    // 恢复之前的警告状态
    QT_WARNING_POP \
    
    // 私有信号标记结构体
    struct QPrivateSignal {}; \
    
    // 类注解宏
    QT_ANNOTATE_CLASS(qt_qobject, "")

2.3 Meta-Object Compiler (MOC)

MOC(Meta-Object Compiler)是Qt的预处理工具,它的主要职责包括:

  • 为使用Q_OBJECT宏的类生成额外的元对象代码
  • 实现信号与槽的底层机制
  • 生成运行时类型信息
  • 支持动态属性和方法调用

3. 信号与槽

3.1 基本概念

信号与槽是Qt框架的核心通信机制,本质上是一种解耦的观察者模式(发布-订阅模式)。在Qt中,当特定事件发生时(如按钮点击),对象会发出信号,这一过程类似于广播。感兴趣的对象可以使用connect()函数将信号与特定的处理函数(槽)绑定,实现事件的自动响应。

信号与槽的本质

在Qt的元对象系统中,信号是一种特殊的函数,由框架自动生成,无需开发者手动实现。槽是普通函数,可以是成员函数、全局函数、静态函数或Lambda表达式。当信号被触发时,绑定的槽函数将自动调用,并传递相应的参数。

信号和槽的关键特征
  • 解耦性:发送者和接收者相互独立,无需直接依赖。
  • 灵活性:支持一对多、多对一的信号-槽关联。
  • 类型安全:编译期进行严格的类型检查,确保参数匹配。
  • 松散耦合:简化对象间通信。

3.2 绑定信号与槽

信号与槽绑定使用QObject::connent()函数实现,其基本格式如下:

 [static] QMetaObject::Connection connect(
     const QObject *sender, 
     const QMetaMethod &signal, 
     const QObject *receiver, 
     const QMetaMethod &method,
 	, Qt::ConnectionType type = Qt::AutoConnection)
     
 [static] QMetaObject::Connection connect(
     const QObject *sender, 
     PointerToMemberFunction signal, 
     Functor functor)
参数解析
  • sender: 信号的发出者,必须是QObject类或其子类的对象。

  • signal: 发出的具体信号,需要传递一个函数指针。

  • receiver: 信号的接收者,必须是QObject类或其子类的对象。

  • method: 信号接收者处理信号的动作,需要传递一个函数指针(槽函数)。

  • **type:**第一个connect函数独有的参数,表示信号与槽的连接类型,通常使用默认值Qt::AutoConnection

信号与槽

在调用connect函数连接信号与槽时,sender对象的信号并不会立即产生,因此receiver对象的method也不会被调用。实际的调用时机是在信号被触发之后。调用槽函数的操作由Qt框架负责,connect中的senderreceiver两个指针必须被实例化,否则连接将失败。

断开连接

信号与槽连接后,可以使用disconnect函数断开连接,断开后信号触发时,槽函数将不再被调用。注意,断开连接时的参数必须与连接时完全一致。

bool disconnect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method)

当然,也可以使用connect的返回值来断开连接:

bool disconnect(const QMetaObject::Connection &connection)

3.3 标准信号与槽

Qt提供了许多类来检测用户触发的特定事件。当这些事件被触发时,便会产生对应的信号,这些信号都是Qt类内部自带的,因此称之为标准信号。同样,Qt的许多类内部还提供了多个功能函数,这些函数可以作为信号触发后的处理动作,称为标准槽函数。

查找标准信号与槽

查找系统自带的信号和槽可以利用Qt的帮助文档。例如,对于按钮的点击信号,可以在帮助文档中输入QPushButton。首先,在Contents中寻找关键字signals,如果没有找到,应该查看该类从父类继承下来的信号。因此,可以查看其父类QAbstractButton,在该类中通常可以找到信号的相关信息。

QPushButton的信号

QPushButton类是Qt中用于创建按钮的标准控件,它提供了一些标准信号,常用的包括:

  • clicked(bool checked = false):当按钮被点击时发出此信号。如果按钮是一个切换按钮(toggle button),则checked参数指示按钮当前的状态(选中或未选中)。
  • pressed():当按钮被按下时发出此信号。
  • released():当按钮被释放时发出此信号。
  • toggled(bool checked):当按钮的状态发生变化时(例如从未选中到选中)发出此信号。checked参数指示按钮当前的状态。

这些信号允许开发者在用户与按钮交互时执行相应的操作。

QWidget的槽

QWidget是Qt中所有用户界面对象的基类,许多其他控件(包括QPushButton)都是从QWidget派生的。QWidget本身也提供了一些标准槽,常用的包括:

  • close():关闭窗口。当调用此槽时,窗口将关闭。
  • show():显示窗口。当调用此槽时,窗口将被显示出来。可以在调用hide()后再次使用此槽。
  • hide():隐藏窗口。当调用此槽时,窗口将被隐藏,不再可见。
  • setWindowTitle(const QString &title):设置窗口的标题。
  • resize(int width, int height):调整窗口的大小。
  • move(int x, int y):移动窗口到指定的位置。

这些槽函数可以直接用于响应信号,方便开发者实现自定义的窗口行为。

使用示例

以下是一个简单的示例,展示如何在窗口上放置一个按钮,并实现点击按钮关闭窗口的功能。

#include <QApplication>
#include <QPushButton>
#include <QMainWindow>

class MainWindow : public QMainWindow 
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr) : QMainWindow(parent)
    {
        QPushButton *btn = new QPushButton("Close Window", this);
        connect(btn, &QPushButton::clicked, this, &MainWindow::close);
        btn->setGeometry(50, 50, 150, 30);
    }
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    MainWindow window;
    window.resize(300, 200);
    window.show();
    return app.exec();
}

#include "main.moc" // 如果在单文件中编写,需要包含此行

在上述代码中,信号与槽的连接可以分析如下:

  • 信号发出者btn
  • 具体信号clicked()
  • 信号接收者this
  • 处理动作close()

连接信号与槽的代码如下:

QObject::connect(btn,&QPushButton::clicked,this,&MainWindow::close);
规则与注意事项

在关联信号与槽时,需遵循一些规则,否则无法建立关联:

  • 槽函数的参数应与信号的参数数量和类型一一对应。
  • 信号的参数个数可以大于或等于槽函数的参数个数,未被槽函数接受的参数将被忽略。例如:
    • 信号:void QPushButton::clicked(bool checked = false)
    • 槽:bool QWidget::close()
    • 在上述两个信号和槽中,槽函数没有接受信号传递的参数,因此这个bool类型的参数被忽略。

3.4 自定义槽

槽函数是信号的处理动作,自定义槽函数与普通函数的写法相同。自定义的槽函数一般放在public slots:后面。在Qt5及以后的版本中,其实可以不再强制要求写slots

定义槽函数必须遵循以下规则

  • 槽函数的返回类型必须是void,不能是其他类型。

  • 槽函数的参数数量必须小于或等于信号的参数数量。

槽函数的类型:

  • 成员函数
    • 普通成员函数
    • 静态成员函数
  • 全局函数
  • lambda表达式(匿名函数)
void global_func();
Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    QPushButton *btn = new QPushButton(this);
    //连接标准槽函数
    connect(btn,&QPushButton::clicked,self,&Widget::close);
	//连接普通成员函数
    connect(btn,&QPushButton::clicked,this,&Widget::member_func);
	//连接静态成员函数
    connect(btn,&QPushButton::clicked,this,&Widget::static_func);
    //连接全局函数
    connect(btn,&QPushButton::clicked,this,&Widget::global_func);    
	//连接lambda表达式
    connect(btn,&QPushButton::clicked,this,[=]()
            {
               qInfo()<<"lambda"; 
               this->close(); 
            });    
 
}
//普通成员函数
void Widget::member_func()
{
    this->close();
}
//静态成员函数
void Widget::static_func(bool checked)
{
    qInfo()<<"static_func"<<checked;
}
//全局函数
void global_func()
{
    qInfo()<<"global_func";
}

如果你想在槽中知道是哪个对象触发的信号,那么你可以使用 QObject *sender() const函数获取,信号的发送者。

3.5 自定义信号

Qt框架提供的信号在某些特定场景下可能无法满足项目需求,因此可以设计自定义信号。同样地,使用connect()函数可以连接自定义的信号和槽。

要使用自定义信号和槽,首先需要编写一个新的类,并让其继承Qt的某些标准类。如果您想在Qt中使用信号槽机制,必须满足以下条件:

  • 该类必须从QObject类或其子类派生。
  • 在定义类的第一行头文件中加入Q_OBJECT宏。
// 在头文件中派生类时,首先像下面这样引入Q_OBJECT宏:
class MyMainWindow : public QWidget
{
    Q_OBJECT
public:
    ......
};

如果是单文件编写的,还需要在代码的最下面加上#include "name.moc",其中name是指原文件的名称

自定义信号需要遵循以下规则:

  • 信号是类的成员函数,且返回类型必须是void

  • 信号函数仅需声明,不需要定义(即没有函数体实现)。

  • 参数可以随意指定,信号也支持重载。

  • 信号需使用signals关键字进行声明,方法类似于public等关键字。

  • 在程序中发送自定义信号的本质是调用信号函数:

    emit mysignals();	//发送信号
    

    注意:emit是一个空宏,没有特殊含义,仅用来表示这个语句是发射一个信号,不写当然可以,但是不推荐。

// 举例: 信号重载
// Qt中的类想要使用信号槽机制必须要从QObject类派生(直接或间接派生都可以)
class MyButton : public QPushButton
{
    Q_OBJECT
signals:
    void testsignal();
    void testsignal(int a);
};

信号参数的作用是数据传递,谁调用信号函数,谁就需要指定实参,实参最终会被传递给槽函数。

3.6 信号和槽重载二义性问题

在使用信号和槽时,如果信号和槽函数重载,可能会出现二义性问题。可以通过以下方法解决:

  1. 通过函数指针解决
// 定义无参信号的函数指针
void (Me::*signalWithoutArgs)() = &Me::hungury;
// 定义有参信号的函数指针
void (Me::*signalWithQString)(QString) = &Me::hungury;

// 定义无参槽的函数指针
void (Me::*slotWithoutArgs)() = &Me::eat;
// 定义有参槽的函数指针
void (Me::*slotWithQString)(QString) = &Me::eat;

// 连接有参信号和槽
connect(me, signalWithQString, me, slotWithQString);
// 连接无参信号和槽
connect(me, signalWithoutArgs, me, slotWithoutArgs);
  1. 通过Qt提供的重载类(QOverload)解决
// 连接有参信号和槽
connect(this, QOverload<QString>::of(&MyButton::hungury), this, QOverload<QString>::of(&MyButton::eat));
// 连接无参信号和槽
connect(this, QOverload<>::of(&MyButton::hungury), this, QOverload<>::of(&MyButton::eat));
  1. Qt4的连接方式

这种旧的信号槽连接方式在Qt6中仍支持,但不推荐使用,因为这种方式在进行信号槽连接时,信号和槽函数通过宏SIGNALSLOT转换为字符串类型。

由于信号槽函数的转换是通过宏进行的,因此传递到宏函数内部的数据不会被检查。如果使用者传错了数据,编译器不会报错,但实际上信号槽的连接已失败,只有在程序运行时才能发现问题,并且问题往往难以定位。

Me m;
// Qt4的连接方式 注意不要把信号和槽的名称写错,因为是转为字符串的,写错了不会报错,但连接会失败
connect(&m, SIGNAL(eat()), &m, SLOT(hungury()));
connect(&m, SIGNAL(eat(QString)), &m, SLOT(hungury(QString)));

// Qt5的连接方式
connect(&m, &Me::eat, &m, &Me::hungury);  // error: no matching member function for call to 'connect'
  • 总结
    • Qt4的信号槽连接方式由于使用了宏函数,宏函数对用户传递的信号槽不会做错误检测,容易产生bug。
    • Qt5的信号槽连接方式传递的是信号和槽函数的地址,编译器会进行错误检测,从而减少bug的产生。
    • 当信号槽函数被重载之后,Qt4的信号槽连接方式不受影响。
    • 当信号槽函数被重载后,在Qt6中需要为被重载的信号或槽定义函数指针。

4. 内存管理

4.1 简介

在C++中,newdelete必须配对使用,否则可能会导致内存泄漏或其他问题。在Qt中,虽然使用了new,但很少需要手动delete,这是因为Qt实现了独特的内存管理机制。
QObject以对象树的形式组织起来。当为一个对象创建子对象时,子对象会自动添加到父对象的children()列表中。父对象拥有子对象的所有权,例如,父对象可以在自己的析构函数中删除其子对象。可以使用findChild()findChildren()通过名称和类型查询子对象。

 QObject(QObject *parent = nullptr)
  1. 如果QObject及其派生类的对象的parentnullptr,那么在其父对象析构时,该对象也会被析构。
  2. 父子关系是Qt特有的,与类的继承关系无关。传递的参数与parent有关(基类、派生类或父类、子类),这是对于派生体系来说的,与parent无关。

4.2 关联图

在Qt中,最基础和核心的类是QObjectQObject内部有一个名为childrenQObjectList列表,用于保存所有子对象,还有一个指针parent,用来指向父对象。当自身析构时,会先将自己从父对象的列表中删除,并析构所有的子对象。
关联图

4.3 详解

1. 对象分配在栈上

栈对象具有自动生命周期管理,推荐使用。

int main(int argc,char*argv[])
{
    QApplication a(argc,argv);
    QObject obj;

    qInfo()<<"hello Qt!";
    return a.exec();
}
2. 对象分配在堆上

当把对象分配到堆上时,如果忘记delete,内存就不会释放,会发生内存泄漏

#include<QApplication>
#include<QDebug>
int main(int argc,char*argv[])
{
    QApplication a(argc,argv);

    QObject* obj = new QObject;

    qInfo()<<"hello Qt!";
    return a.exec();
}
释放内存
  • 使用delete或者Qt提供的成员函数deleteLater()释放内存,对象释放时会触发QObject::destroyed(QObject *obj = nullptr)信号
int main(int argc,char*argv[])
{
    QApplication a(argc,argv);

    QObject* obj = new QObject;
    
    //delete obj;				//①
    //obj->deleteLater();		//②

    qInfo()<<"hello Qt!";
    return a.exec();
}
  • 使用指定父对象的方式自动管理内存
#include<QApplication>
#include<QDebug>

class MyObject:public QObject
{
public:
    MyObject(QObject* parent = nullptr)
        :QObject(parent)
        {
            qInfo()<<"MyObject created!";
        }
    ~MyObject()
    {
        qInfo()<<"MyObject destory!";
    }
};

int main(int argc,char*argv[])
{
    QApplication a(argc,argv);
    
    {
        MyObject parent;
        {
            MyObject* obj = new MyObject(&parent);
            //obj->deleteLater();
            //MyObject obj;
        }
    }
    
    qInfo()<<"hello Qt!";
    return a.exec();
}

4.4 对象名

在Qt中,可以为对象设置对象名,从而使用findChild()通过名称(和类型)查找对象;还可以通过findChildren()找到一组对象。

  • 设置对象名

    void QObject::setObjectName(const QString &name);
    
  • 获取对象名

    QString QObject::objectName() const;
    
  • 通过对象名查找对象

    template <typename T>
    T findChild(const QString &name = QString(), Qt::FindChildOptions options = Qt::FindChildrenRecursively) const
    

    根据指定的名称name和指定的类型TT可以是父类)查找子对象。如果没有这样的子对象,则返回nullptr

    • 示例:返回名为“button1”的parentWidget的子QPushButton,即使该按钮不是父级的直接子级:

       QPushButton *button = parentWidget->findChild<QPushButton *>("button1");
      
    • 示例:返回parentWidgetQListWidget子组件:

       QListWidget *list = parentWidget->findChild<QListWidget *>();
      
    • 示例:返回parentWidget(它的直接父元素)的一个名为"button1"的子QPushButton

       QPushButton *button = parentWidget->findChild<QPushButton *>("button1", Qt::FindDirectChildrenOnly);
      
    • 示例:返回parentWidgetQListWidget子组件,它的直接父组件:

       QListWidget *list = parentWidget->findChild<QListWidget *>(QString(), Qt::FindDirectChildrenOnly);
      
  • 通过类型查找对象

     QList<T> findChildren(const QString &name = QString(), Qt::FindChildOptions options = Qt::FindChildrenRecursively) const
     QList<T> findChildren(const QRegularExpression &re, Qt::FindChildOptions options = Qt::FindChildrenRecursively) const
    

    根据指定的名称name和指定的类型TT可以是父类)查找子对象。如果没有这样的子对象,则返回nullptr

    • 示例:查找名为widgetname的指定父Widget的子Widget列表:

       QList<QWidget *> widgets = parentWidget.findChildren<QWidget *>("widgetname");
      
    • 示例:返回parentWidget的所有子QPushButton

       QList<QPushButton *> allPButtons = parentWidget.findChildren<QPushButton *>();
      
    • 示例:返回所有与parentWidget直接关联的QPushButton

       QList<QPushButton *> childButtons = parentWidget.findChildren<QPushButton *>(QString(), Qt::FindDirectChildrenOnly);
      

4.5 智能指针

在C++中,为了有效管理内存和其他资源,程序员通常采用RAII(Resource Acquisition Is Initialization)机制:在类的构造函数中分配资源,在使用完成后通过析构函数释放资源。智能指针的引入,使得程序员不再需要手动管理new对象的delete操作,也无需编写复杂的异常捕获代码来释放资源,因为智能指针能够在作用域结束时(无论是正常退出还是异常退出)自动调用delete来销毁在堆上动态分配的对象。

在Qt框架中,提供了多种类型的智能指针,以便于资源管理:

智能指针描述
QPointerQObject 专享指针,当 QObject 或其子类对象被释放时,会自动将指针置为 nullptr。
QScopedPointer独享指针,当超出作用域时,自动释放所管理的对象。
QSharedPointer共享指针,支持多个指针共同拥有同一对象。
QWeakPointer监视指针,用于观察 QSharedPointer 所管理的对象是否仍然存在。
QScopedArrayPointer独享数组指针,当超出作用域时,自动释放所管理的对象数组。
QSharedDataPointer隐式共享指针,支持读时共享和写时拷贝。
QExplicitlySharedDataPointer显示共享指针,读时共享,写时需要手动拷贝(通过 detach() )。
QPointer

QPointer是一种受保护的指针,其行为类似于普通的C++指针T*,但当指向的对象被销毁时,QPointer会自动清空(与普通指针不同,后者会变成“悬空指针”)。T必须是QObject的子类。

QPointer在需要存储指向其他对象的QObject指针时非常有用,因为这些对象可能在你持有引用期间被销毁。使用QPointer,可以安全地检查指针的有效性。

需要注意的是,从Qt 5开始,QPointer的行为有所调整。在QWidget或其子类中,QPointer会在QObject的析构函数中清除,而非在QWidget的析构函数中。这意味着,在QWidget析构时,任何跟踪该小部件的QPointer不会被立即清除,直到QObject的析构过程进行时。

QPointer<QLabel> label = new QLabel;
label->setText("&Status:");
...
if (label)
    label->show();

在上述代码中,如果QLabel被删除,label变量将保存nullptr,而不是无效地址,最后一行将不会执行。

请注意,类T必须继承QObject,否则将导致编译或链接错误。

QScopedPointer

手动管理堆分配对象往往复杂且容易出错,常见结果是内存泄漏且难以维护。QScopedPointer是一个轻量级的工具类,它通过将基于栈的内存所有权转移到堆分配的对象,从而极大简化了资源管理(这一概念被称为RAII)。

QScopedPointer确保当当前作用域结束时,所指向的对象将被删除。编译器为QScopedPointer生成的代码与手动编写的代码等效。由于QScopedPointer没有复制构造函数或赋值操作符,它明确传达了所有权和生命周期的信息。

QSharedPointer

QSharedPointer是一种自动共享指针,其行为与普通指针相似。如果没有其他QSharedPointer对象引用它,当其超出作用域时,将自动删除所持有的指针。QSharedPointer可以从普通指针、另一个QSharedPointer对象或通过提升QWeakPointer对象创建强引用。

QWeakPointer

QWeakPointer是一种自动弱引用指针,不能直接解引用,但可用于验证指针在其他上下文中是否已被删除。QWeakPointer对象只能通过从QSharedPointer赋值创建。

需要注意的是,QWeakPointer未提供自动强制转换操作符,因此即使QWeakPointer跟踪一个指针,它本身也不能被视为一个有效指针。访问QWeakPointer所跟踪的指针时,必须首先将其提升到QSharedPointer,并验证结果是否为空。QSharedPointer保证对象不会被删除,因此如果获得一个非空对象,可以安全使用该指针。

QScopedArrayPointer

QScopedArrayPointer是QScopedPointer的变体,默认情况下使用delete[]操作符释放所指向的对象。它还提供了操作符[],例如:

 void foo()
 {
     QScopedArrayPointer<int> i(new int[10]);
     i[2] = 42;
     ...
     return; // our integer array is now deleted using delete[]
 }

QSharedDataPointer

QSharedDataPointer类表示指向隐式共享对象的指针。通过QSharedDataPointer,您可以轻松实现自己的隐式共享类。QSharedDataPointer实现了线程安全的引用计数,确保在可重入类中不会导致不可重入。

许多Qt类都使用隐式共享,以结合指针速度和内存效率以及类的易用性。有关更多信息,请参见共享类页面。

假设您想让Employee类实现隐式共享,步骤如下:

  1. 定义Employee类,包含一个类型为QSharedDataPointer的数据成员。
  2. 定义从QSharedData派生的EmployeeData类,包含通常放入Employee类中的所有数据成员。

以下是隐式共享Employee类的示例代码:

 #include <QSharedData>
 #include <QString>

 class EmployeeData : public QSharedData
 {
   public:
     EmployeeData() : id(-1) { }
     EmployeeData(const EmployeeData &other)
         : QSharedData(other), id(other.id), name(other.name) { }
     ~EmployeeData() { }

     int id;
     QString name;
 };

 class Employee
 {
   public:
     Employee() { d = new EmployeeData; }
     Employee(int id, const QString &name) {
         d = new EmployeeData;
         setId(id);
         setName(name);
     }
     Employee(const Employee &other)
           : d (other.d)
     {
     }
     void setId(int id) { d->id = id; }
     void setName(const QString &name) { d->name = name; }

     int id() const { return d->id; }
     QString name() const { return d->name; }

   private:
     QSharedDataPointer<EmployeeData> d;
 };

在上述Employee类中,注意到数据成员d为QSharedDataPointer类型。所有对员工数据的访问均应通过d指针的operator->()进行。对于写访问,操作符将自动调用detach(),如果共享数据对象的引用计数大于1,detach()将创建共享数据对象的副本。这样可以确保对一个Employee对象的写入不会影响共享相同EmployeeData对象的其他Employee对象。

在Employee类的构造函数中,创建新的EmployeeData实例并将其赋值给d指针。

Employee() { d = new EmployeeData; }

Employee(int id, const QString &name) 
{
    d = new EmployeeData;
    setId(id);
    setName(name);
}

请注意,Employee类定义了简单的复制构造函数,但在此示例中并非严格要求。

Employee(const Employee &other) : d(other.d) {}

尽管在包含QSharedDataPointer的公共类的同一文件中包含QSharedData的私有子类并不典型,通常的做法是将QSharedData的私有子类放在一个单独的文件中,以隐蔽其实现细节。如果在这里将EmployeeData类放在单独的文件中,则需在employee.h中声明该类。

class EmployeeData;

在幕后,QSharedDataPointer会自动增加引用计数,在复制、分配或作为参数传递Employee对象时进行。当Employee对象被删除或超出作用域时,引用计数会减少。当引用计数达到0时,共享的EmployeeData对象将被自动删除。

在Employee的非常量成员函数中,每当d指针被解引用时,QSharedDataPointer会自动调用detach(),以确保函数对自身数据的副本进行操作。注意,若因多次解引用而在成员函数中多次调用detach(),detach()只会在第一次调用时创建数据副本。

在Employee的const成员函数中,解引用d指针不会导致调用detach()。

int id() const { return d->id; }

QString name() const { return d->name; }

需要说明的是,不必为Employee类实现复制构造函数或赋值操作符,因为C++编译器提供的默认实现已经满足需要,逐个成员进行浅复制。唯一需要复制的是d指针,它是一个QSharedDataPointer,其operator=()仅增加共享EmployeeData对象的引用计数。

隐式与显式共享

隐式共享可能不适用于Employee类。考虑创建两个隐式共享Employee类实例的示例:

#include "employee.h"

int main()
{
    Employee e1(1001, "Albrecht Durer");
    Employee e2 = e1;
    e1.setName("Hans Holbein");
}

在第二个雇员e2被创建并被分配给e1之后,e1和e2都指向雇员1001 Albrecht Durer。两个Employee对象都指向EmployeeData的同一个实例,该实例的引用计数为2。然后e1。setName(“Hans Holbein”)被调用来更改员工名,但由于引用计数大于1,所以在更改名称之前会执行写时拷贝。现在e1和e2指向不同的EmployeeData对象。它们有不同的名称,但都有ID 1001,这可能不是您想要的。当然,如果您真的想创建第二个唯一的雇员,但如果您只想在所有地方更改雇员的名称,则可以继续使用e1.setId(1002),考虑在employee类中使用显式共享而不是隐式共享。

如果将Employee类中的d指针声明为QExplicitlySharedDataPointer<EmployeeData>,则使用显式共享,写时复制操作不会自动执行(即在非const函数中不会调用detach())。在这种情况下,e1之后。setName(“Hans Holbein”),员工的名字已经改变,但是e1和e2仍然引用EmployeeData的同一个实例,所以只有一个员工的ID是1001。

在成员函数文档中,d指针始终指向共享数据对象的内部指针。

优化Qt容器的使用性能

如果隐式共享类类似于上述Employee类,并且使用QSharedDataPointer或QExplicitlySharedDataPointer作为唯一成员,建议使用Q_DECLARE_TYPEINFO()宏将其标记为可移动类型。这可以提高在使用Qt容器类时的性能和内存效率。

QExplicitlySharedDataPointer

QExplicitlySharedDataPointer类表示指向显式共享对象的指针。QExplicitlySharedDataPointer使您可以轻松编写自己的显式共享类。它实现了线程安全的引用计数,确保将QExplicitlySharedDataPointer添加到可重入类中不会导致不可重入。

QExplicitlySharedDataPointer与QSharedDataPointer类似,但其成员函数在允许修改共享数据对象之前,不会像QSharedDataPointer的非const成员那样自动执行写时复制(detach())。可手动调用detach(),但如果频繁调用detach(),应考虑使用QSharedDataPointer替代。

5. 属性系统

The Property System

Qt提供了一个复杂而强大的属性系统,旨在简化对象属性的管理和访问。该系统与一些编译器供应商提供的属性系统相似,但作为一个独立于编译器和平台的库,Qt并不依赖于像__property[property]这样的非标准编译器特性。因此,Qt的解决方案可以在所有支持的Qt平台上使用任何标准的C++编译器,并基于元对象系统,通过信号和插槽机制实现对象之间的通信。

属性的行为类似于类的数据成员,但它具有通过元对象系统进行访问的附加特性。下面将详细介绍属性的基本用法及其附加功能。

5.1 获取/设置属性值

在Qt中,可以轻松地获取和设置对象的属性。例如,QObject类具有一个名为objectName的属性,可以通过以下方式获取其值:

qInfo() << obj->property("objectName").toString();

若要修改该属性的值,可以使用以下代码:

obj->setProperty("objectName","OBJ");

QObject::setProperty()方法不仅可以用于修改已存在的属性,还可以在运行时向类的实例添加新属性。当调用该方法时,如果QObject中已存在具有指定名称的属性,并且提供的值与该属性的类型兼容,则该值将被存储到属性中,并返回true。如果值与属性类型不兼容,则不会更改属性的值,并返回false

需要注意的是,如果QObject中不存在具有指定名称的属性(即未使用Q_PROPERTY()声明),则会自动将新属性添加到QObject中,尽管返回值仍然为false。这意味着仅凭返回值无法确定特定属性是否实际被设置,除非事先确认该属性已存在于QObject中。

动态属性是在每个实例基础上添加的,即它们是添加到QObject而不是QMetaObject中的。通过将属性名称和一个无效的QVariant值传递给QObject::setProperty(),可以从实例中删除属性。QVariant的默认构造函数构造一个无效的QVariant

5.2 声明自定义属性

除了通过setProperty动态添加属性之外,还可以在代码中显式声明属性。要声明属性,需要在继承自QObject的类中使用Q_PROPERTY()宏。

Q_PROPERTY(type name
           (READ getFunction [WRITE setFunction] |
            MEMBER memberName [(READ getFunction | WRITE setFunction)])
           [RESET resetFunction]
           [NOTIFY notifySignal]
           [REVISION int | REVISION(int[, int])]
           [DESIGNABLE bool]
           [SCRIPTABLE bool]
           [STORED bool]
           [USER bool]
           [BINDABLE bindableProperty]
           [CONSTANT]
           [FINAL]
           [REQUIRED])
属性声明参数详解
  • type:属性的数据类型,可以是QVariant支持的任何类型,也可以是用户自定义的类型。
  • name:属性的名称。
  • READ:指定用于读取属性值的访问器函数。此函数应返回属性的类型或对该类型的const引用。
  • WRITE:可选项,用于设置属性值的访问器函数。该函数必须返回void并只接受一个参数,该参数可以是属性的类型或指向该类型的指针或引用。
  • MEMBER:可选项,指定直接关联的成员变量,使其可读可写,而无需显式定义READWRITE函数。
  • RESET:可选项,指定将属性重置为其特定于上下文的默认值的函数。
  • NOTIFY:可选项,指定信号,该信号在属性值更改时发出。该信号必须与属性类型匹配。
  • REVISION:可选项,用于定义API中特定修订版中使用的属性。
  • DESIGNABLE:指示属性是否应在GUI设计工具(如Qt Designer)的属性编辑器中可见。默认为true
  • SCRIPTABLE:指示脚本引擎是否可以访问该属性。默认为true
  • STORED:指示该属性是否应被视为独立属性,默认为true
  • USER:指示该属性是否为面向用户的属性,默认值为false
  • BINDABLE:指示该属性支持绑定。
  • CONSTANT:指示该属性的值是常量。
  • FINAL:指示该属性不会被派生类覆盖。
  • REQUIRED:指示该属性应由类的用户设置。
自定义属性示例
class MyObject : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name READ getName WRITE setName RESET unsetName NOTIFY nameChanged)

public:
    MyObject(QObject *parent = nullptr) : QObject(parent) {}

    QString getName() const {
        qInfo() << __FUNCTION__;
        return m_name;
    }

    void setName(const QString &name) {
        if (m_name != name) {
            m_name = name;
            emit nameChanged(m_name); // 发射信号以通知属性变化
        }
    }

    void unsetName() {
        m_name = "unknown";
    }

signals:
    void nameChanged(const QString &);

private:
    QString m_name;
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MyObject obj;

    QObject::connect(&obj, &MyObject::nameChanged, [](const QString &name) {
        qInfo() << "slot:" << name;
    });

    obj.setName("maye");
    qInfo() << obj.getName(); // 输出 "maye"
    
    obj.unsetName(); // 将name重置为"unknown"

    obj.setProperty("name", "顽石");
    qInfo() << obj.property("name").toString(); // 输出 "顽石"
    
    obj.setProperty("name", QVariant()); // 将name重置为"unknown"

    return a.exec();
}

在此示例中,通过属性name访问和设置值,相当于直接操作成员变量m_name。当使用setName方法更改名称时,程序手动发射信号以通知属性变化。

属性关联成员变量示例
class MyObject : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name MEMBER m_name NOTIFY nameChanged)

public:
    MyObject(QObject *parent = nullptr) : QObject(parent) {}

signals:
    void nameChanged(const QString &);

private:
    QString m_name;
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MyObject obj;

    QObject::connect(&obj, &MyObject::nameChanged, [](const QString &name) {
        qInfo() << "slot:" << name;
    });

    obj.setProperty("name", "顽石"); // 修改属性,会自动触发nameChanged信号
    qInfo() << obj.property("name").toString(); // 输出 "顽石"

    return a.exec();
}

在上述代码中,使用属性name直接修改了成员变量m_name,通过属性接口访问或设置属性值时,不需要显式的读写函数。如果指定了NOTIFY信号,则通过属性接口改变name的值时,信号会自动触发。

5.3 绑定属性

Qt Bindable Properties

Qt引入了可绑定属性,允许开发者创建依赖于其他属性值的属性。这些属性可以具有静态值或通过C++函数(通常是lambda表达式)动态计算的值。当依赖的属性发生变化时,可绑定属性会自动更新其值。

可绑定属性的实现主要依赖于QProperty类,该类包含数据对象以及指向管理数据结构的指针。QObjectBindableProperty类用于封装这些功能,适用于QObject的子类。

  • QPropertyQObjectBindableProperty 是在 Qt 6 中引入的特性,因此需要 Qt 6 或更高版本才能使用这些功能。
  • Q_PROPERTY 宏及其相关功能在 Qt 4 及更高版本中均可用。
  • QProperty是可绑定属性的通用类,而QObjectBindableProperty只能在QObject的子类中使用。
QProperty示例

定义一个Rectangle矩形类,通过构造函数传入宽度和高度,自动计算矩形面积:

struct Rectangle
{
    int w;
    int h;
    int area;
    Rectangle(int width,int height)
        :w(width),h(height)
    {
        area = w * h;
    }
};

void test()
{
    Rectangle rect(2,5);
    qInfo()<<rect.w<<rect.h<<rect.area; //2 5 10

    rect.w = 3;     //area:15
    qInfo()<<rect.w<<rect.h<<rect.area; //3 5 10
}

从上面代码可以看出,只有在构造对象时,才能正确计算面积;如果在对象定义之后,修改宽度或者高度,面积都将变得不正确,因为它没有及时更新。

矩形的面积是依赖于矩形的宽度和高度的,那么当高度或者高度变化之后,应该需要自动更新面积,这个应该如何做到呢?

绑定表达式通过读取其他QProperty值来计算该值。在幕后跟踪这种依赖关系。每当检测到任何属性的依赖关系发生更改时,都会重新计算绑定表达式,并将新的结果应用于该属性。例如:

#include <QCoreApplication>
#include <QProperty>  // Qt6引入
#include <QDebug>

struct Rectangle
{
    QProperty<int> w{0}; // 宽度
    QProperty<int> h{0}; // 高度
    QProperty<int> area{0}; // 面积

    // 构造函数,初始化宽度和高度
    Rectangle(int width, int height) : w(width), h(height)
    {
        // 设置绑定属性
        area.setBinding([this]() -> int { return w * h; });
    }
};

void test()
{
    Rectangle rect(2, 5);
    qInfo() << rect.w << rect.h << rect.area; // 输出: 2 5 10

    rect.w = 3; // 触发area计算
    qInfo() << rect.w << rect.h << rect.area; // 输出: 3 5 15
    
    rect.h = 4; // 触发area计算
    qInfo() << rect.w << rect.h << rect.area; // 输出: 3 4 12
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    test(); // 调用测试函数

    return a.exec();
}

在这个例子中,当矩形的宽度或高度发生变化时,面积area会自动更新。这是因为area属性通过setBinding方法与wh属性建立了依赖关系。

QObjectBindableProperty 示例

QObjectBindableProperty是一个通用容器,它保存T的一个实例,其行为主要类似于QProperty。它是实现Qt绑定属性的类之一。与QProperty不同,它将其管理数据结构存储在QObject中。额外的模板参数用于标识周围的类和作为更改处理程序的类的成员函数。

QObjectBindableProperty允许在使用Q_PROPERTY的代码中添加绑定支持。以下是一个使用QObjectBindableProperty的示例:

在这个示例中,定义了一个Rectangle类,其中的宽度和高度属性被声明为可绑定属性。当宽度或高度改变时,面积属性会自动重新计算并发射信号。

#include <QObject>
#include <QDebug>
#include <QCoreApplication>
#include <QObjectBindableProperty>

struct Rectangle : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int w MEMBER w)
    Q_PROPERTY(int h MEMBER h)
    Q_PROPERTY(int area MEMBER area)

public:
    Rectangle(int width, int height) : w(width), h(height)
    {
        // 设置绑定属性
        area.setBinding([&]() { return w * h; });
    }

signals:
    void areaChanged(int area);

public:
    Q_OBJECT_BINDABLE_PROPERTY(Rectangle, int, w);                              // 定义可绑定属性
    Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(Rectangle, int, h, 0);                 // 可以给默认值
    Q_OBJECT_BINDABLE_PROPERTY(Rectangle, int, area, &Rectangle::areaChanged);   // 属性改变时,会触发areaChanged信号
};  

void test()
{
    Rectangle rect(2, 5);
    QObject::connect(&rect, &Rectangle::areaChanged, [&](int area) { 
        qInfo() << "Area changed to:" << area; 
    });

    qInfo() << rect.w << rect.h << rect.area; // 输出: 2 5 10

    rect.w = 3; // area: 15
    qInfo() << rect.w << rect.h << rect.area; // 输出: 3 5 15
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    test(); // 调用测试函数

    return a.exec();
}

通常不会直接使用QObjectBindableProperty,而是通过使用Q_OBJECT_BINDABLE_PROPERTY宏创建它的实例。

在类声明中使用Q_OBJECT_BINDABLE_PROPERTY宏将属性声明为可绑定的。

如果需要使用一些非默认值直接初始化属性,可以使用Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS宏。它接受一个初始化值作为它的参数之一。

6. 实时类型信息

何为内省

内省(Introspection)是面向对象编程语言的一种特性,它允许在运行时查询对象的信息。这种能力使得能够检查对象的类型,从而实现多态性。若一种语言具备在运行期间检查对象类型的能力,则称之为类型内省(Type Introspection)。

  1. Qt是通过QObjectQMetaObject类实现其内省机制。
  2. QObject暴露给用户的共有自省方法有objectName(), inherits()isWidgetType()等。
  3. 大多数自省方法是QObject派发给QMetaObject实现 (QMetaObject::className),元对象模型编译器moc负责自省方法的实现。
  4. 更多自省方法定义在QMetaObject,是为了信号槽通讯、事件派发等机制。

C++的内省功能相对有限,主要支持通过运行时类型识别(RTTI,Run-Time Type Information)实现的类型内省。C++的RTTI通过typeiddynamic_cast关键字来实现,以下是一个简要示例:

// Dog类派生自Animal类,jump为虚函数  
if (Dog *pdog = dynamic_cast<Dog*>(obj)) {
   pdog->cry();     
}     
// 还可以使用typeid获取对象的类型信息,如对象的名称   
std::cout << typeid(obj).name() << std::endl;   

在Qt中,内省机制得到了扩展。实际上,Qt并未采用C++的RTTI,而是提供了更为强大的元对象(Meta Object)机制来实现内省。理解Qt的内省机制需要首先理解QObject类,因为QObject是整个Qt对象模型的核心。Qt对象模型的主要功能是提供无缝的对象通信机制,即信号和槽。QObject承担着三大主要职责:内存管理、内省和事件处理。接下来将重点讨论内省。

QObject类提供了多种内省方法,以下是一个示例代码,展示了如何判断一个类是否继承自指定的类:

// 判断该类是否继承自指定的类
bool inherits(const char *className) const;

QWidget* w = new QWidget;
bool isQObject = w->inherits("QObject");  // true
bool isQWidget = w->inherits("QWidget");  // false

深入了解QObject::inherits方法的底层实现,可以发现其实际实现如下:

inline bool inherits(const char *classname) const { 
    return const_cast<QObject *>(this)->qt_metacast(classname) != nullptr; 
}

可见,QObject::inherits方法是通过虚函数qt_metacast()实现的。每个QObject的派生类必须实现metaObject()以及其他qt_metacall()方法,从而支持内省方法如classNameinherits等的调用。

用户在派生自QObject的类中只需声明宏Q_OBJECT,Qt的元对象编译器(MOC)便会负责实现这些内省方法。

#define Q_OBJECT \
public: \
    QT_WARNING_PUSH \
    Q_OBJECT_NO_OVERRIDE_WARNING \
    static const QMetaObject staticMetaObject; \
    virtual const QMetaObject *metaObject() const; \
    virtual void *qt_metacast(const char *); \
    virtual int qt_metacall(QMetaObject::Call, int, void **); \
    QT_TR_FUNCTIONS \
private: \
    Q_OBJECT_NO_ATTRIBUTES_WARNING \
    Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
    QT_WARNING_POP \
    struct QPrivateSignal {}; \
    QT_ANNOTATE_CLASS(qt_qobject, "")

此外,所有的Qt widgets类均继承自QObject, QObject所提供的isWidgetType自省方法可以很方便让QObject子对象查询自己是否是Wideget, 而且它会比 qobject_cast<QWidget *>(obj) 或者obj->inherits快很多。原因qobject_cast()inherits()都是借助元对象系统来实现其功能的,isWidgetType()QObject本身的标志位得以实现。

更多自省方法定义在QMetaObject

枚举

使用枚举可以便利地表示某些状态标志。然而,在查看枚举值时,通常只能看到数值,无法直接查看枚举的名称。Qt提供了方法,使得在输出时能够显示定义的枚举名称。

命名空间中的枚举
namespace Maye {
    Q_NAMESPACE
    enum  Type
    {
        Player,
        Enemy,
        Bullet
    };
    Q_ENUM_NS(Type) // 将枚举注册到元对象系统
}

首先定义命名空间,并在命名空间的第一行添加Q_NAMESPACE宏,以将整个命名空间注册到元对象系统中。接着定义枚举类型,最后使用**Q_ENUM_NS(enum type)**将枚举类型注册到元对象系统中。

以下代码能够成功输出枚举名,而非仅仅是数值:

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    using namespace Maye;
    Type type = Type::Player;
    qDebug() << type; // 输出: Maye::Player
    
    // 获取枚举的元信息
    const QMetaObject &metaObject = Maye::staticMetaObject; // 获取命名空间的静态元对象
    QMetaEnum metaEnum = metaObject.enumerator(metaObject.indexOfEnumerator("Type")); // 获取 Type 枚举的元信息

    // 将枚举值转换为字符串
    QString typeStr = metaEnum.valueToKey(static_cast<int>(type));
    qInfo() << typeStr; // 输出: "Player"

    return a.exec();
}
类中的枚举
class Test : public QObject
{
    Q_OBJECT
public:
    enum Type
    {
        Player,
        Enemy,
        Bullet
    };
    Q_ENUM(Type)
};

在自定义类中,首先需直接继承自QObject或其子类;然后,在public权限下定义枚举;最后,使用Q_ENUM(enum type)将枚举类型注册到元对象系统中。

Test::Type type = Test::Type::Player;
qDebug() << type; // 输出: Test::Player

// 转为字符串
// 获取枚举元信息
const QMetaObject &metaObject = Test::staticMetaObject;
QMetaEnum metaEnum = metaObject.enumerator(metaObject.indexOfEnumerator("Type"));

// 将枚举值转换为字符串
QString typeStr = metaEnum.valueToKey(static_cast<int>(type));
qDebug() << typeStr; // 输出: "Player"
QMetaEnum

QMetaEnum类提供了多种功能以处理枚举。其主要功能包括:

  • name():返回枚举项的名称。
  • key():返回每个枚举项的键(名称)。
  • keyCount():查找键的数量。
  • isFlag():返回该枚举是否被设计为标志,意味着它的值可以使用OR操作符组合。
  • keyToValue()valueToKey()keysToValue()**和**valueToKeys():这些转换函数允许在枚举或集合值的整数表示与其文字表示之间进行转换。
  • scope():返回声明此枚举的类的范围。

下面是一个使用QMetaEnum的示例,展示如何定义枚举、注册枚举到元对象系统,并利用QMetaEnum来获取枚举值和名称。

#include <QCoreApplication>
#include <QMetaEnum>
#include <QObject>
#include <QDebug>

class TrafficLight : public QObject
{
    Q_OBJECT
public:
    enum State 
    {
        Red,
        Yellow,
        Green
    };
    Q_ENUM(State) // 注册枚举到元对象系统

    TrafficLight(QObject *parent = nullptr) : QObject(parent) {}

    void printCurrentState(State state) 
    {
        // 使用QMetaEnum获取状态的名称
        const QMetaObject *metaObject = this->metaObject();
        int index = metaObject->indexOfEnumerator("State");
        QMetaEnum metaEnum = metaObject->enumerator(index);
        qDebug() << "Current state:" << metaEnum.valueToKey(state);
    }
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    TrafficLight light;
    light.printCurrentState(TrafficLight::Red);    // 输出: Current state: "Red"
    light.printCurrentState(TrafficLight::Yellow); // 输出: Current state: "Yellow"
    light.printCurrentState(TrafficLight::Green);  // 输出: Current state: "Green"

    return a.exec();
}

#include "main.moc"

QMetaObject

QMetaObject类包含有关Qt对象的元信息。它提供了一种机制,通过该机制可以在运行时获取有关对象的结构和属性信息。以下是一些重要的成员函数:

  • className():返回类的名称。
  • classInfoCount():返回类信息的数量。
  • classInfo(int index):获取指定索引的类信息。
  • classInfoOffset():返回类信息的偏移量。

附加信息

Q_CLASSINFO()宏可用于将附加的<名称-值>对附加到类的元对象,例如:

Q_CLASSINFO("Version", "3.0.0")

可以通过QMetaObject的几个函数访问类信息:

QMetaClassInfo classInfo(int index) const
int classInfoCount() const
int classInfoOffset() const
const char *className() const

示例:

下面示例展示了如何使用QMetaObject类和Q_CLASSINFO()宏来获取类的元信息。

class MyObject : public QObject
{
    Q_OBJECT
    Q_CLASSINFO("version", "1.0")
    Q_CLASSINFO("author", "顽石")
public:
};

class MyObject1 : public MyObject
{
    Q_OBJECT
    Q_CLASSINFO("version", "2.0")
    Q_CLASSINFO("name", "maye")
public:
};

int main(int argc, char* argv[])
{
    QApplication a(argc, argv);

    MyObject1 obj;
    const QMetaObject* metaObj = obj.metaObject();
    int cnt = metaObj->classInfoCount();
    for (int i = 0; i < cnt; i++)
    {
        qInfo() << metaObj->classInfo(i).name() << metaObj->classInfo(i).value();
    }
    qInfo() << metaObj->classInfoOffset();
    qInfo() << metaObj->className();

    return a.exec();
}

输出:

version 1.0
author 顽石
version 2.0
name maye
2
MyObject1

oc"




## QMetaObject

`QMetaObject`类包含有关Qt对象的元信息。它提供了一种机制,通过该机制可以在运行时获取有关对象的结构和属性信息。以下是一些重要的成员函数:

- **className()**:返回类的名称。
- **classInfoCount()**:返回类信息的数量。
- **classInfo(int index)**:获取指定索引的类信息。
- **classInfoOffset()**:返回类信息的偏移量。

### 附加信息

`Q_CLASSINFO()`宏可用于将附加的<名称-值>对附加到类的元对象,例如:

```cpp
Q_CLASSINFO("Version", "3.0.0")

可以通过QMetaObject的几个函数访问类信息:

QMetaClassInfo classInfo(int index) const
int classInfoCount() const
int classInfoOffset() const
const char *className() const

示例:

下面示例展示了如何使用QMetaObject类和Q_CLASSINFO()宏来获取类的元信息。

class MyObject : public QObject
{
    Q_OBJECT
    Q_CLASSINFO("version", "1.0")
    Q_CLASSINFO("author", "顽石")
public:
};

class MyObject1 : public MyObject
{
    Q_OBJECT
    Q_CLASSINFO("version", "2.0")
    Q_CLASSINFO("name", "maye")
public:
};

int main(int argc, char* argv[])
{
    QApplication a(argc, argv);

    MyObject1 obj;
    const QMetaObject* metaObj = obj.metaObject();
    int cnt = metaObj->classInfoCount();
    for (int i = 0; i < cnt; i++)
    {
        qInfo() << metaObj->classInfo(i).name() << metaObj->classInfo(i).value();
    }
    qInfo() << metaObj->classInfoOffset();
    qInfo() << metaObj->className();

    return a.exec();
}

输出:

version 1.0
author 顽石
version 2.0
name maye
2
MyObject1
;