Qt中信号与槽的实现
今天和朋友聊到关于Qt中类间通信是如何实现的,所以打算对源对象编译器处理过的代码做个简单的分析,了解下信号和槽到底是什么样子。
为了了解元对象编译器都做了些什么,先创建一个简单最简单的GUI程序。打开程序的debug目录,看到了moc_mainwindow.cpp文件,这就是Qt为我们做的事情了。
/********************************************************************
** Meta object code from reading C++ file 'mainwindow.h'
**
** Created: Sat Oct 17 23:38:12 2009
** by: The Qt Meta Object Compiler version 62 (Qt 4.6.0)
**
** WARNING! All changes made in this file will be lost!
********************************************************************/
可以看到关于该文件的一些介绍,接下来看下面的定义:
#include "../mainwindow.h"
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'mainwindow.h' doesn't include <QObject>."
#elif Q_MOC_OUTPUT_REVISION != 62
#error "This file was generated using the moc from 4.6.0. It"
#error "cannot be used with the include files from this version of Qt."
#error "(The moc has changed too much.)"
#endif
首先看到的是,判断Q_MOC_OUTPUT_REVISION是否被定义,从而确定,该类是否继承自QObject。那么这个宏是在什么地方定义的呢?看下QObject的代码,就能找到了:
#ifndef Q_MOC_OUTPUT_REVISION
#define Q_MOC_OUTPUT_REVISION 62
#endif
在Qt4.6源代码的56行位置。
接下来是对宏编译器版本的判定,接着往下看:
static const uint qt_meta_data_MainWindow[] = {
// content:
4, // revision
0, // classname
0, 0, // classinfo
0, 0, // methods
0, 0, // properties
0, 0, // enums/sets
0, 0, // constructors
0, // flags
0, // signalCount
0 // eod
};
该结构体主要存储了类的一些解析的信息,我们可以通过加个信号,看看其改变。
加了一个信号之后,该结构体变成了:
static const uint qt_meta_data_MainWindow[] = {
// content:
4, // revision
0, // classname
0, 0, // classinfo
1, 14, // methods
0, 0, // properties
0, 0, // enums/sets
0, 0, // constructors
0, // flags
1, // signalCount
// signals: signature, parameters, type, tag, flags
12, 11, 11, 11, 0x05,
0 // eod
};
变化的几个地方是:1, 14, // methods,这里意思应该是有1个方法,但是14的含义就不清楚了。之后是增加了信号的计数,再往后是对信号的记录,具体的含义,有待研究。
static const char qt_meta_stringdata_MainWindow[] = {
"MainWindow/0"
};
这个数组存放的应该是需要的字符串信息,当我们增加信号以后,会是:
static const char qt_meta_stringdata_MainWindow[] = {
"MainWindow/0/0hello()/0"
};
可以看出来,主要是看存放信号和槽名称的,另外就是类名了。
const QMetaObject MainWindow::staticMetaObject = {
{ &QMainWindow::staticMetaObject, qt_meta_stringdata_MainWindow,
qt_meta_data_MainWindow, 0 }
};
我们看到这是一个静态变量对象的复制操作,不仔细看还以为是一个方法的实现呢,之前在MainWindow里我们没有定义啊,这个只有函数实现怎么可以呢?还是让我们看看Q_OBJECT的宏吧。
#define Q_OBJECT /
public: /
Q_OBJECT_CHECK /
static const QMetaObject staticMetaObject; /
Q_OBJECT_GETSTATICMETAOBJECT /
virtual const QMetaObject *metaObject() const; /
virtual void *qt_metacast(const char *); /
QT_TR_FUNCTIONS /
virtual int qt_metacall(QMetaObject::Call, int, void **); /
private:
/* tmake ignore Q_OBJECT */
原来,Q_OBJECT定义了这样一些东西,这样的话,我们的类实际上是这样的:
Class MainWindow: public QMainWindow
{
Public:
Q_OBJECT_CHECK
Static const QMetaObject staticMetaObject;
Q_OBJECT_GETSTATICMETAOBJECT
virtual const QMetaObject *metaObject() const;
virtual void *qt_metacast(const char *);
QT_TR_FUNCTIONS
virtual int qt_metacall(QMetaObject::Call, int, void **);
}
这样,我们就看到了之前的那个静态变量:
const QMetaObject MainWindow::staticMetaObject = {
{ &QMainWindow::staticMetaObject, qt_meta_stringdata_MainWindow,
qt_meta_data_MainWindow, 0 }
};
这里将之前看到的数据信息传递给了QMetaObject类。
接下来继续看:
const QMetaObject *MainWindow::metaObject() const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->metaObject : &staticMetaObject;
}
这里返回了元对象的指针。
void *MainWindow::qt_metacast(const char *_clname)
{
if (!_clname) return 0;
if (!strcmp(_clname, qt_meta_stringdata_MainWindow))
return static_cast<void*>(const_cast< MainWindow*>(this));
return QMainWindow::qt_metacast(_clname);
}
这里提供了元对象中的类型转换方式,联想到了C++中的dynamic_cast方法,不过只是猜测。
int MainWindow::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = QMainWindow::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
switch (_id) {
case 0: hello(); break;
default: ;
}
_id -= 1;
}
return _id;
}
这里就是信号和槽被调用的地方了,但是采用switch case的方法,显然速度不是很快,所以尽量在一个文件中写少一点的信号和槽,否则,时间复杂度将是o(n),如果这里采用map或者hash的方式,应该会好很多。但是Qt没有这么做,原因不得而知。
void MainWindow::hello()
{
QMetaObject::activate(this, &staticMetaObject, 0, 0);
}
实际上,信号也是一个函数。由元对象系统负责对用与之关联的槽。
现在,我们再增加一个槽,看一看。
在数据结构体中,增加了新的关于槽的描述:
// slots: signature, parameters, type, tag, flags
20, 11, 11, 11, 0x09,
if (_c == QMetaObject::InvokeMetaMethod) {
switch (_id) {
case 0: hello(); break;
case 1: helloworld(); break;
default: ;
}
_id -= 2;
}
原来信号和槽是放在一起进行判定的。下面将信号和槽连在一起,会发生什么呢?结果是没有变化,那么,可以看出来,元对象编译器,主要产生了信号和槽的调用函数。所有的信息会提交给一个上层类,QMetaObject进行管理,负责调度,信号函数是直接被调用的,而槽的函数则是在信号函数中调用active之后,由元对象系统负责调用。同样的,我们还可以将一个信号和另外一个信号连接在一起,这样的过程,后面的信号也会被以同样的方式触发。
今天就到这里了,明天继续了解信号和槽在QMetaObject中的实现。
2009年10月18日星期日 00:31