侧边栏壁纸
博主头像
lmg博主等级

  • 累计撰写 55 篇文章
  • 累计创建 6 个标签
  • 累计收到 2 条评论
标签搜索

Qt

lmg
lmg
2022-07-29 / 0 评论 / 0 点赞 / 582 阅读 / 4,741 字
温馨提示:
本文最后更新于 2023-01-03,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

特性:一次编写,随处编译,可移植性高

元对象系统实现功能:

  1. 信号和槽机制
  2. 属性系统

每个带有Q_OBJECT宏的类,里面都有:

static const QMetaObject staticMetaObject; // 类的静态元对象,保存该类的元对象系统信息。
//使用"静态"元对象,说明该类的所有实例都会共享这个静态元对象,而不需要重复占用内存。

virtual const QMetaObject *metaObject() const; // 获取元对象

virtual void *qt_metacast(const char *); //是非常重要的虚函数
// 在信号到槽的执行过程中,qt_metacall 就是负责槽函数的调用,
// 属性系统的读写等也是靠 qt_metacall 实现
// SIGNAL 0 moc实现的signal函数
void Widget::nickNameChanged(const QString & _t1)
{
    void *_a[] = { Q_NULLPTR, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
// 首先是指针数组 _a 的定义,_a 里面第一个指针是 Q_NULLPTR(就是 NULL),
// 这个指针是预留给 元对系统内部注册元方法参数类型时使用;
// 第二个指针是参数_t1 的指针;如果有更多的参数,那么会继续将指针填充到 _a 里面。
// _a 是用于传递从信号到槽函数的参数的。
// 为什么信号的参数可以比槽函数的参数多?因为槽函数接收到 _a 指针数组时,只需要取出自己需要的前面几个参数就够了,槽函数不管多余的参数。
//信号里的参数不能比槽函数里的少,那样槽函数访问指针数组时会越界,造成内存访问错误。
//以 valueChanged 信号实体代码为例,QMetaObject::activate 函数是负责联络接收方槽函数的,
//它根据源头对象指针 this、源头的元对象指针 &staticMetaObject、信号序号 2、信号参数数组  _a 去找寻需要激活的槽函数,最终会调用每个关联到该信号的槽函数。


QMetaObject::activate

当我们写下一下emit signal代码的时候,与这个signal相连接的slot就会被调用,那么这个调用是如何发生的呢?让我们来逐一解开其中的谜团。

让我们来看一段例子代码:

class ZMytestObj : public QObject
{
Q_OBJECT
signals:
void sigMenuClicked();
void sigBtnClicked();
};

MOC编译器在做完预处理之后的代码如下:

// SIGNAL 0
void ZMytestObj::sigMenuClicked()
{
QMetaObject::activate(this, &amp;staticMetaObject, 0, 0);
}
 
// SIGNAL 1
void ZMytestObj::sigBtnClicked()
{
QMetaObject::activate(this, &amp;staticMetaObject, 1, 0);

哈哈,看到了把,每一个signal都会被转换为一个与之相对应的成员函数。也就是说,当我们写下这样一行代码:
emit sigBtnClicked();
当程序运行到这里的时候,实际上就是调用了void ZMytestObj::sigBtnClicked() 这个函数。

大家注意比较这两个函数的函数体,
void ZMytestObj::sigMenuClicked() void ZMytestObj::sigBtnClicked(),
它们唯一的区别就是调用 QMetaObject::activate 函数时给出的参数不同,一个是0,一个是1,它们的含义是什么呢?它们表示是这个类中的第几个signal被发送出来了,回头再去看头文件就会发现它们就 是在这个类定义中,signal定义出现的顺序,这个参数可是非常重要的,它直接决定了进入这个函数体之后所发生的事情。

当执行流程进入到QMetaObject::activate函数中后,会先从connectionLists这个变量中取出与这个signal相对应的 connection list,它根据的就是刚才所传入进来的signal index。这个connection list中保存了所有和这个signal相链接的slot的信息,每一对connection(即:signal 和 slot 的连接)是这个list中的一项。

在每个一具体的链接记录中,还保存了这个链接的类型,是自动链接类型,还是队列链接类型,或者是阻塞链接类型,不同的类型处理方法还不一样的。这里,我们就只说一下直接调用的类型。

对于直接链接的类型,先找到接收这个signal的对象的指针,然后是处理这个signal的slot的index,已经是否有需要处理的参数,然后就使用这些信息去调用receiver的qt_metcall 方法。

在qt_metcall方法中就简单了,根据slot的index,一个大switch语句,调用相应的slot函数就OK了

qt_meta_stringdata_Widget字符串
qt_meta_data_Widget 数组
存className, 信号函数名,信号参数名,槽函数名,属性名等信息。
元对象系统里面凡是字符串类型的都保存在之前的 qt_meta_stringdata_Widget 结构体实例里面,凡是索引数值之类的保存在 qt_meta_data_Widget 数组里。这两个数据块都是全局类型,而类通常要通过自己的元对象来使用这些数据,就是 moc_widget.cpp 里面定义的第三个静态数据:类的静态元对象。

void *Widget::qt_metacast(const char *_clname)
{
    if (!_clname) return Q_NULLPTR;
    if (!strcmp(_clname, qt_meta_stringdata_Widget.stringdata))
        return static_cast<void*>(const_cast< Widget*>(this));
    return QWidget::qt_metacast(_clname);
}
eg: 把某个派生类对象指针转成合法的基类对象指针 obj->qt_metacast("基类名Widget");
一直在继承树往上找到那个基类,找到的话就让他转为相应的基类对象指针
// 为什么要再转成void* ?

int Widget::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
_c是元调用方式,如信号,槽函数,其他invokable方法等
函数头有三个参数,_c 是元调用方式,而 _id 和 _a 的意义根据用途有区别:
对于元方法调用,_id 是元方法的绝对序号, _a 与信号函数里封装的参数指针数组 _a 是对应的;
对于属性操作,_id 是属性的绝对序号,_a 是属性操作需要的参数指针数组。

enum Call {
        InvokeMetaMethod,
        ReadProperty,
        WriteProperty,
        ResetProperty,
        QueryPropertyDesignable,
        QueryPropertyScriptable,
        QueryPropertyStored,
        QueryPropertyEditable,
        QueryPropertyUser,
        CreateInstance,
        IndexOfMethod,
        RegisterPropertyMetaType,
        RegisterMethodArgumentMetaType
    };
int Widget::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QWidget::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id < 5)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 5;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
        if (_id < 5)
            *reinterpret_cast<int*>(_a[0]) = -1;
        _id -= 5;
    }
#ifndef QT_NO_PROPERTIES
      else if (_c == QMetaObject::ReadProperty) {
        void *_v = _a[0];
        switch (_id) {
        case 0: *reinterpret_cast< QString*>(_v) = nickName(); break;
        case 1: *reinterpret_cast< int*>(_v) = count(); break;
        case 2: *reinterpret_cast< double*>(_v) = m_value; break;
        default: break;
        }
        _id -= 3;

这一句直接调用了基类 QWidget::qt_metacall 函数,参数里的 _id 都是绝对序号,但是经过层层基类函数迭代处理之后,每个基类都会把自己的元方法计数或属性计数减掉,然后返回新的 _id ,当把所有基类的计数都减掉之后,我们得到新的 _id 就是我们当前类 Widget 里面元方法或属性的相对计数了。
问题: 为什么要基类函数处理,比如调用的是当前classs的方法,基类处理什么?

QPointer

QPointer类是一个模板类,它提供了指向QObject的guarded pointer。

guarded pointer是什么指针?

当其指向的对象(必须是QObject及其派生类)被销毁时,它会被自动置NULL
和C++普通指针T *相比:普通指针被删除之后会变成悬空指针(野指针)

原理:

  • QPointer对QMetaObject的相关操作做了简单的封装,这里的基本思想是在QPointer构造的时候调用QMetaObject::addGuard(&o),把T的指针加入QMetaObject内的一个哈希表中,
  • 在QPointer析构的时候调用QMetaObject::removeGuard(&o),把T的指针从哈希表中删除。
  • 对象析构时会执行QObject的析构函数,进而执行QObjectPrivate::clearGuards(this),这也是基于其指向对象都继承自QObject的原因
        QWidget* w = new QWidget;
        QLable* label = new QLabel("hello");
        label->setParent(w);

        QPointer<QLabel> qp = label;
        delete w;
        printf("is null:%d\n",qp.isNull()); // qp is NULL
        printf("is null:%d\n",label==NULL); // label不为空,野指针
        label->setText("helloword"); // Segmentation fault
0

评论区