首页 Qt Creator 源码学习 Qt Creator 源码学习 07:D 指针

Qt Creator 源码学习 07:D 指针

9 2K

按照我们之前的习惯,我们会按照 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

这里有四个带有图形界面的类,分别是PluginViewPluginErrorViewPluginDetailsViewPluginErrorOverview。顾名思义,这些是有关插件的信息、错误信息、详细列表以及错误概览。目前,这些图形界面类不在我们的探讨范围之内。

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 下运行崩溃了。

为什么?

原因在于,我们添加了一个新的数据成员,从而改变了WidgetLabel对象的大小。当 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 中,Labeltext属性位于偏移量为 1 的逻辑位置。在编译器生成的这个 CuteApp 应用程序代码中,Label::text()函数会返回偏移量为 1 的那个数据成员。在 WidgetLib 1.1 中,由于我们添加了一个成员,Labeltext属性的偏移量变成了 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 评论

Zt 2016年12月7日 - 09:36

豆子,请问Qt的帮助文档有pdf版本的吗?我想下载下来没事在公交车地铁上看看

回复
豆子 2016年12月7日 - 16:16

好像没有 pdf 版本的

回复
blueshake 2017年1月23日 - 16:39

请问有看到qt creator代码自动完成那一块的代码了,现有的代码自动完成要输入三个字母才能自动提示,我一直找不到这个代码的地方。知道相关代码在哪里吗?我想改成只要输入一个字母就能自动提示。

回复
姜敏 2017年3月16日 - 10:42

请问这个版块没有更新了吗?

回复
豆子 2017年3月16日 - 22:02

没有,还在继续,只是最近有点忙,没有及时更新

回复
clown 2017年3月21日 - 16:36

能不能讲讲Qt源码里面的类隐藏 ,实现隐藏 ,我看源码感觉看不懂,到处都是 d->ptr 到处都是 Qxxx_p.h ,根本找不到想要看的代码

回复
clown 2017年3月21日 - 16:53

我的提问有点傻逼,请博主略过

回复
clown 2017年3月21日 - 16:38

源码学习系列 标题上 ,附上本章核心内容的标题比较好

回复
qtnewbie 2021年7月23日 - 23:10

博主,我不太理解,有了D指针后,成员变量是隐藏在D指针背后了,但是成员函数呢?如果新版本的库添加、删除了新、旧的成员函数,是不是依然会不兼容旧版库?

回复

回复 blueshake 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2