按照我们之前的习惯,我们会按照 libs.pro 的SUBDIRS
部分的顺序进行阅读。前面一章我们读过 aggregation 子项目,下面则要开始重中之重,extensionsystem。我们曾经介绍过,Qt Creator 的核心是一个插件系统。Qt Creator 的所有功能都是以插件的形式提供的。这个插件系统的实现,就是 extensionsystem。在这一阶段,我们将了解到 Qt Creator 的整个插件系统的基础,以便在我们自己的项目中使用类似的实现。
在 extensionsystem.pro 中,我们可以看到下面的代码:
HEADERS += pluginerrorview.h \ plugindetailsview.h \ invoker.h \ iplugin.h \ iplugin_p.h \ extensionsystem_global.h \ pluginmanager.h \ pluginmanager_p.h \ pluginspec.h \ pluginspec_p.h \ pluginview.h \ optionsparser.h \ plugincollection.h \ pluginerroroverview.h SOURCES += pluginerrorview.cpp \ plugindetailsview.cpp \ invoker.cpp \ iplugin.cpp \ pluginmanager.cpp \ pluginspec.cpp \ pluginview.cpp \ optionsparser.cpp \ plugincollection.cpp \ pluginerroroverview.cpp FORMS += \ pluginerrorview.ui \ plugindetailsview.ui \ pluginerroroverview.ui
这里有四个带有图形界面的类,分别是PluginView
、PluginErrorView
、PluginDetailsView
和PluginErrorOverview
。顾名思义,这些是有关插件的信息、错误信息、详细列表以及错误概览。目前,这些图形界面类不在我们的探讨范围之内。
Qt Creator 在源代码中使用了大量 D-Pointer 设计,被称为 D 指针,这里有必要先进行一些补充说明,以便更好理解。
D 指针是 C++ 特有的一种“设计模式”,并不适用于 Java 等其它语言(这是与另外一些设计模式区别的地方,因而在之前加上了引号)。D 指针主要为了解决二进制兼容的问题。二进制兼容与源代码兼容,在 KDE Wiki 上面是这样写的:
A library is binary compatible, if a program linked dynamically to a former version of the library continues running with newer versions of the library without the need to recompile.
If a program needs to be recompiled to run with a new version of library but doesn't require any further modifications, the library is source compatible.
一个动态链接到较早版本的库的程序,在不经过重新编译的情况下能够继续运行在新版本的库,这样的库即“二进制兼容”。
一个程序不需要经过额外的修改,只需要重新编译就能够在新版本的库下继续运行,这样的库被称为“源代码兼容”。
二进制兼容对于库尤其重要,因为你不能指望每次升级了库之后,要求所有使用了这个库的应用程序都要重新编译。就 Qt 而言,假如一个程序使用 Qt 5.5 编译,当 Qt 5.6 发布之后,如果 Qt 的升级能够保持二进制兼容,那么这个程序应该可以直接能够使用 Qt 5.6 正常运行;否则,就需要这个程序重新编译,才能够运行在 Qt 5.6 平台。
要使一个库能够实现二进制兼容,就要求每一个结构以及每一个对象的数据模型保持不变。所谓“数据模型保持不变”,就是不能在类中增加、删除数据成员。这是 C++ 编译器要求的,其根本是保持数据成员在数据模型中的位移保持不变。
例如,我们有这样的类:
class Widget { // ... private: Rect m_geometry; }; class Label : public Widget { public: // ... String text() const { return m_text; } private: String m_text; };
Widget
包含了一个Rect
成员变量,用于保存组件的大小和位置。我们将其编译为 WidgetLib 1.0。在 WidgetLib 1.1 中,有人灵机一动,说“让我们添加一个样式吧”,于是就有了下面的升级:
class Widget { // ... private: Rect m_geometry; String m_stylesheet; // NEW in WidgetLib 1.1 }; class Label : public Widget { public: // ... String text() const { return m_text; } private: String m_text; };
添加一行很简单,但是,这让我们原本使用 WidgetLib 1.0 能够正常运行的程序 CuteApp,未经编译时,在 WidgetLib 1.1 下运行崩溃了。
为什么?
原因在于,我们添加了一个新的数据成员,从而改变了Widget
和Label
对象的大小。当 C++ 编译器生成目标代码时,它使用“偏移量”来访问对象中的数据。我们使用一个简化了的图示来表现这一点:
WidgetLib 1.0 中的 Label 对象 | WidgetLib 1.1 中的 Label 对象 |
m_geometry <offset 0> | m_geometry <offset 0> |
--- | m_stylesheet <offset 1> |
m_text <offset 1> | --- |
--- | m_text <offset 2> |
在 WidgetLib 1.0 中,Label
的text
属性位于偏移量为 1 的逻辑位置。在编译器生成的这个 CuteApp 应用程序代码中,Label::text()
函数会返回偏移量为 1 的那个数据成员。在 WidgetLib 1.1 中,由于我们添加了一个成员,Label
的text
属性的偏移量变成了 2!但是,我们并没有重新编译 CuteApp,程序依然会返回偏移量为 1 的那个成员,现在却返回了stylesheet
。
有人可能会问,为什么Label::text()
函数计算的偏移量会固定在 CuteApp,而不是在 WidgetLib 中?如果在 WidgetLib 就能确定这个偏移量,岂不是没有这个问题了?因为如果这样,只需要重新编译 WidgetLib 库就可以了。答案是,Label::text()
函数是在头文件中定义的,而头文件是要内联到最终的源代码的。
所以,是不是把Label::text()
函数的内联属性去除就可以了?也就是把Label::text()
函数的实现放到源代码文件中?答案还是不行。C++ 编译器依赖于在编译期和运行期,相同对象的大小是一致的。例如,如果我们在栈上创建一个Label
对象,编译器需要在编译期依照Label
的大小分配内存空间。但是应用程序在运行期使用的是 WidgetLib 1.1,Label
的大小已经不一样了;Label
的构造函数会覆盖已有的栈,于是就把栈给破坏了。
现在我们已经知道导致二进制不兼容的原因了,因而也就知道如何保持二进制兼容:那就是,不要改变成员变量。但是,对于一个库来说,这怎么可能呢?
好在我们能想到一个技巧,来规避这一问题。我们让所有的公共类都只持有一个指针,这个指针指向一个包含了所有数据的私有数据结构。这个私有的数据结构可以在未来的版本中任意修改,而且不会影响到使用了这个库的程序,因为这个数据结构是私有的,外部根本不能使用。外部只能看到一个不变的公共类,因为它只持有一个指针。这个指针就是 D 指针。
使用这种思路,可以把上面的代码修改为如下:
// widget.h /* 由于 d_ptr 是一个指针,并且没有在头文件中被引用, WidgetPrivate 就不需要被 include,只需要前置声明即可。 这个类的定义可以在 widget.cpp,也可以在一个单独的文件 widget_p.h */ class WidgetPrivate; class Widget { // ... Rect geometry() const; // ... private: WidgetPrivate *d_ptr; };
那么,我们就来一个 widget_p.h 吧:
/* widget_p.h (_p 意思是私有) */ struct WidgetPrivate { Rect geometry; String stylesheet; };
于是,源代码文件可以是这样的:
// 通过这个 include,我们就可以访问 WidgetPrivate #include "widget_p.h" Widget::Widget() : d_ptr(new WidgetPrivate) { // Creation of private data } Rect Widget::geometry() const { // The d-ptr is only accessed in the library code return d_ptr->geometry; }
那么,Label
就可以是这样的:
class Label : public Widget { // ... String text(); private: // 每个类都维护着自己的 D 指针 LabelPrivate *d_ptr; };
Label
的实现则是:
// 不同于 WidgetPrivate,我们把 LabelPrivate 定义在源代码文件中 struct LabelPrivate { String text; }; Label::Label() : d_ptr(new LabelPrivate) { } String Label::text() { return d_ptr->text; }
观察以上结构,CuteApp 不能直接访问 D 指针。因为 D 指针仅在 WidgetLib 内部使用,并且 WidgetLib 是在每次发布预先编译好的,私有的数据结构就可以任意修改,无需担心影响到 CuteApp。
使用 D 指针,除了二进制兼容,还有很多其它好处:
- 隐藏实现细节:我们只需要发布 WidgetLib 的头文件和二进制文件,.cpp 文件可以是闭源的
- 头文件很干净,没有实现细节的干扰,可以作为 API 实现
- 由于供实现的头文件都被移动到了实现的源代码文件中,编译会更快
现在,我们有一个 D 指针,指向一个 C 风格的数据结构。在实际应用中,这个数据结构会包含私有函数,用于帮助它所在的类。例如,LabelPrivate
可能会有getLinkTargetFromPoint()
函数,用于获取鼠标点击的链接地址。在很多情况下,这种私有函数都要求访问它所在的公共类。例如,setTextAndUpdateWidget()
就需要调用Widget::update()
,后者是Widget
类的公共函数。为了能够在私有的实现类中访问其所在公共类,我们引入一个 Q 指针:
struct WidgetPrivate { // 在构造函数直接初始化 Q 指针 WidgetPrivate(Widget *q) : q_ptr(q) { } Widget *q_ptr; // q-ptr points to the API class Rect geometry; String stylesheet; };
通过新增加的 Q 指针,我们就可以访问到外部的公共类。
D 指针和 Q 指针的结合使用,才能够发挥其最大的威力。
如果你好奇地读过 Qt 的源代码,可以看到 Qt 的类中有很多Q_D
以及Q_Q
,这正是 Qt 实现的 D 指针和 Q 指针。Qt 实现的方式比我们上面所说的复杂,但是思路是一致的。感兴趣的话,可以在 Qt 的 Wiki 页面阅读相关内容。
9 评论
豆子,请问Qt的帮助文档有pdf版本的吗?我想下载下来没事在公交车地铁上看看
好像没有 pdf 版本的
请问有看到qt creator代码自动完成那一块的代码了,现有的代码自动完成要输入三个字母才能自动提示,我一直找不到这个代码的地方。知道相关代码在哪里吗?我想改成只要输入一个字母就能自动提示。
请问这个版块没有更新了吗?
没有,还在继续,只是最近有点忙,没有及时更新
能不能讲讲Qt源码里面的类隐藏 ,实现隐藏 ,我看源码感觉看不懂,到处都是 d->ptr 到处都是 Qxxx_p.h ,根本找不到想要看的代码
我的提问有点傻逼,请博主略过
源码学习系列 标题上 ,附上本章核心内容的标题比较好
博主,我不太理解,有了D指针后,成员变量是隐藏在D指针背后了,但是成员函数呢?如果新版本的库添加、删除了新、旧的成员函数,是不是依然会不兼容旧版库?