源码阅读 | 您所在的位置:网站首页 › 在线源码阅读 › 源码阅读 |
我们进入 qt/src 文件夹。你可能对这里的目录名时曾相识,因为几乎这里的所有文件夹名都对应着 Qt 的模块的名字:gui,network,multimedia等等。我们从最核心的 QtCore 开始。这个模块对应的是corelib文件夹。 首先我们要去寻找 QObject 这个类。之所以选择 QObject,一是因为它是 Qt 的核心类,另外一个很重要的原因是,QObject类是一个典型的Qt类,我们可以通过这个类学习到Qt的设计思路。 回忆一下我们编写 Qt 代码的时候,使用的语句是: 因此我们回到在 corelib 里面,可以看到 kernel 文件夹。看到名字就应该知道,这就是Qt corelib 的核心。在这里面,我们可以找到有四个文件以 qobject 打头: qobject.h:QObject 的类定义,这个就是 QObject 文件引用的文件,也就是我们使用的实际头文件; qobject.cpp:QObject的实现代码; qobjectdefs.h:这个文件中定义了很多用到的宏,并且定义了QMetaObject类,而这个类是实现signal- slot的基础; qobject_p.h:对 QObject 的辅助数据类; 实际上我们还会看到另外两个文件:qobjectcleanuphandler.h 和 qobjectcleanuphandler.cpp。不过如果打开这两个文件就会发现,这里面定义的是一个QObjectCleanupHandler 类,而这个类是继承了 QObject 的,因此这只是一个普通的工具类,不在我们目前的讨论之列。因此我们可以认为,QOjbect 类是由4个文件共同实现的:qobject.h,qobject.cpp,qobjectdefs.h和qobject_p.h。 如果你阅读了 Qt 的源代码,你会看到一堆奇奇怪怪的宏,例如 Q_D,Q_Q。我们的Qt源码之旅就从理解这些宏说起。 下面先看一个C++的例子。 这就是一种信息隐藏的方法。看上去很麻烦,原本很简单的对 name 和 age 的访问都不得不通过一个指针去访问它,何必呢?其实这样做是有好处的: 1、减少头文件的依赖。像这样我们把数据成员都写在 cpp 文件中,当我们需要修改数据成员的时候就只需要修改 cpp 文件。虽然都是修改,但这和修改 .h 文件是不一样的!原因在于,如果 .h 文件发生改变,编译器会重新编译所有 include 了这个 .h 文件的文件。如果你这个类相当底层,那就会花费很长时间。 2、增加类的信息封装。这意味着你根本看不到具体数据类型,必须使用 getter 和 setter 去访问。我们知道 C++ 有一个 typedef 语句,我定义一个数据类型 ABC,如果你看不到具体文件,你会知道这个 ABC 是 string 还是 int 么? 这就是 C++ 的一种设计方法,被称为 Private Class,大约就是私有类吧!更确切地说应该是私有数据类。据说,这也是 Qt 2.x 的实现方式。但是如果你去看你的 Qt SDK 代码,你是看不到这样的语句的,取而代之的则是一些我们开头所说的 Q_D 这些宏。或许你已经隐隐约约地猜到了,这些宏就是实现这个的:Private Data。 下面在上一篇的基础上,我们进入Qt的源代码,看看Qt4.x是如何实现 Private Classes 的。 正如前面我们说的,或许你会看到很多类似 Q_D 或者 Q_Q 这类的宏。那么,我们来试着看一下这样的代码 在来看一下 Qt 的实现: 这个例子很简单,一个使用传统方法实现,另一个采用了 Qt4.x 的方法。Qt4.x 的方法被称为 D-Pointer,因为它会使用一个名为 d 的指针,正如上面写的那个 d_ptr。使用传统方法,我们需要在 private 里面写上所有的私有变量,通常这会让整个文件变得很长,更为重要的是,用户并不需要这些信息。而使用 D-Pointer 的方法,我们的接口变得很漂亮:再也没有那一串长长的私有变量了。你不再需要将你的私有变量一起发布出去,它们就在你的 d 指针里面。如果你要修改数据类型这些信息,你也不需要去修改头文件,只需改变私有数据类即可。 需要注意的一点是,与单纯的 C++ 类不同,如果你的私有类需要定义 signals 和 slots,就应该把这个定义放在头文件中,而不是像上一篇所说的放在 cpp 文件中。这是因为 qmake 只会检测 .h 文件中的 Q_OBJECT 宏(这一点大家务必注意)。当然,你不应该把这样的 private class 放在你的类的同一个头文件中,因为这样做的话就没有意义了。常见做法是,定义一个 private 的头文件,例如使用 myclass_p.h 的命名方式(这也是 Qt 的命名方式)。并且记住,不要把 private 头文件放到你发布的 include 下面!因为这不是你发布的一部分,它们是私有的。然后,在你的 myclass 头文件中,使用 在这个类的 private 部分,我们使用了一个 MyClassPrivate 的 const 指针 d_ptr。如果你需要让这个类的子类也能够使用这个指针,就应该把这个 d_ptr 放在 protected 部分,正如上面的代码那样。并且,我们还加上了 const 关键字,来确保它只能被初始化一次。 下面,我们遇到了一个神奇的宏:Q_DECLARE_PRIVATE。这是干什么用的?那么,我们先来看一下这个宏的展开: 如果你看不大懂,那么就用我们的 Q_DECLARE_PRIVATE(MyClass) 看看展开之后是什么吧: 下面还是自己展开一下这个宏,就成了 现在我们已经比较清楚的知道 Qt 是如何使用 D-Pointer 实现我们前面所说的信息隐藏的了。但是,还有一个问题:如果我们把大部分代码集中到 MyClassPrivate 里面,很可能需要让 MyClassPrivate 的实现访问到 MyClass 的一些东西。现在我们让主类通过 D-Pointer 访问 MyClassPrivate 的数据,但是怎么反过来让 MyClassPrivate 访问主类的数据呢?Qt 也提供了相应的解决方案,那就是 Q_Q 宏,例如: 现在我们已经能够使用比较 Qt 的方式来使用 Private Classes 实现信息隐藏了。这不仅仅是 Qt 的实现,当然,你也可以不用 Q_D 和 Q_Q,而是使用自己的方式,这些都无关紧要。最主要的是,我们了解了一种 C++ 类的设计思路,这是 Qt 的源代码教给我们的。 前面我们已经看到了怎样使用标准的 C++ 代码以及 Qt 提供的 API 来达到信息隐藏这一目标。下面我们来看一下 Qt 是如何实现的。 还是以 QObject 的源代码作为例子。先打开 qobject.h,找到 QObjectData 这个类的声明。具体代码如下所示: 然后在下面就可以找到 QObject 的声明: 注意,这里我们只是列出了我们所需要的代码,并且我的 Qt 版本是 2010.03。这部分代码可能会随着不同的 Qt 版本所有不同。 首先先了解一下 Qt 的设计思路。既然每个类都应该把自己的数据放在一个 private 类中,那么,为什么不把这个操作放在几乎所有类的父类 QObject 中呢?所以,Qt 实际上是用了这样一个思路,实现了我们前面介绍的数据隐藏机制。 首先回忆一下,我们前面说的 D-Pointer 需要有一个 private 或者 protected 的指向自己数据类的指针。在 QObject 中, 就扮演了这么一个角色。或许,你可以把它理解成 这不就和我们前面说的 D-Pointer 技术差不多了?QScopedPointer 是 Qt 提供的一个辅助类,这个类保存有一个指针,它的行为类似于一种智能指针:它能够保证在这个作用域结束后,里面的所有指针都能够被自动 delete 掉。也就是说,它其实就是一个比普通指针更多功能的指针。而这个指针被声明成 protected 的,也就是只有它本身以及其子类才能够访问到它。这就提供了让子类不必须声明这个 D-Pointer 的可能。 那么,前面我们说,QObjectData 这种数据类不应该放在公开的头文件中,可 Qt 怎么把它放进来了呢?这样做的用途是,QObject 的子类的数据类都可能继承自这个 QObjectData。这个类有一个纯虚的析构函数。没有实现代码,保证了这个类不能被初始化;虚的析构函数,保证了其子类都能够被正确的析构。 回到我们前面说明的 Q_DECLARE_PRIVATE 这个宏: 我们把代码中的 Q_DECLARE_PRIVATE(QObject) 展开看看是什么东西 清楚是清楚,只是这个 QObjectPrivate 是哪里来的?既然是 Private,那么它肯定不会在公开的头文件中。于是我们立刻想到到 qobject.cpp 或者是 qobject_p.h 中寻找。终于,我们在 qobject_p.h 中找到了这个类的声明: 在 qobject.cpp 中,我们看一下 QObject 的构造函数: QWidget 是 QObject 的子类,然后看它的构造函数 前面我们说过,Qt 不是使用的“标准的” C++ 语言,而是对其进行了一定程度的“扩展”。这里我们从Qt新增加的关键字就可以看出来:signals、slots 或者 emit。所以有人会觉得 Qt 的程序编译速度慢,这主要是因为在 Qt 将源代码交给标准 C++ 编译器,如 gcc 之前,需要事先将这些扩展的语法去除掉。完成这一操作的就是 moc。 moc 全称是 Meta-Object Compiler,也就是“元对象编译器”。Qt 程序在交由标准编译器编译之前,先要使用 moc 分析 C++ 源文件。如果它发现在一个头文件中包含了宏 Q_OBJECT,则会生成另外一个 C++ 源文件。这个源文件中包含了 Q_OBJECT 宏的实现代码。这个新的文件名字将会是原文件名前面加上 moc_ 构成。这个新的文件同样将进入编译系统,最终被链接到二进制代码中去。因此我们可以知道,这个新的文件不是“替换”掉旧的文件,而是与原文件一起参与编译。另外,我们还可以看出一点,moc 的执行是在预处理器之前。因为预处理器执行之后,Q_OBJECT 宏就不存在了。 既然每个源文件都需要 moc 去处理,那么我们在什么时候调用了它呢?实际上,如果你使用 qmake 的话,这一步调用会在生成的 makefile 中展现出来。从本质上来说,qmake 不过是一个 makefile 生成器,因此,最终执行还是通过 make 完成的。 为了查看 moc 生成的文件,我们使用一个很简单的 cpp 来测试: //test.cpp 1 这下了解了:正是对 Q_OBJECT 宏的展开,使我们的 Test 类拥有了这些多出来的属性和函数。注意,QT_TR_FUNCTIONS 这个宏也是在这里定义的。也就是说,如果你要使用 tr() 国际化,就必须使用 Q_OBJECT 宏,否则是没有 tr() 函数的。这期间最重要的就是 virtual const QMetaObject *metaObject() const; 函数。这个函数返回 QMetaObject 元对象类的实例,通过它,你就获得了 Qt 类的反射的能力:获取本对象的类型之类,而这一切,都不需要 C++ 编译器的 RTTI 支持。Qt 也提供了一个类似 C++ 的 dynamic_cast() 的函数 qobject_case(),而这一函数的实现也不需要 RTTI。另外,一个没有定义 Q_OBJECT 宏的类与它最接近的父类是同一类型的。也就是说,如果 A 继承了 QObject 并且定义了 Q_OBJECT,B 继承了 A 但没有定义 Q_OBJECT,C 继承了 B,则 C 的 QMetaObject::className() 函数将返回 A,而不是本身的名字。因此,为了避免这一问题,所有继承了 QObject 的类都应该定义 Q_OBJECT 宏,不管你是不是使用信号槽。 转载自:http://blog.sina.com.cn/s/blog_6e80f1390100qoc0.html |
CopyRight 2018-2019 实验室设备网 版权所有 |