内容目录
什么是d-指针
如果 妳曾经阅读过Qt 的源代码,比如说 这个 , 妳会发现, 在其中大规模地使用了 Q_D 和 Q_Q 这两个宏。 本文章,即是为了说明这两个宏的作用。
Q_D 和 Q_Q ,这两个宏,与其它组件配合起来,组成了 一个名为 d-指针 ( 也称作 暗影指针 )的设计模式 。 在这种模式下,某个 库的实现细节 ,对于用户来说是隐藏的 。这样,当对这个库的实现细节进行变更时, 不会打破二进制兼容性。
二进制兼容性 — 是什么?
在设计像 Qt 这样的开发库的时候,有一个人们狠在意的点,就是,对于那些以动态链接方式与Qt 链接的应用程序,在Qt 库被升级/替换为另一个版本的情况下,仍然能够无需重新编译而继续正常运行。举个例子,妳的应用程序,名为 CuteApp ,是基于Qt 4.5 编译链接的。那么,妳应当能够将Qt 库从Qt 4.5 升级到Qt 4.6(通常由软件包管理器自动进行!),在升级之后,妳自己之前基于Qt 4.5 编译的CuteApp 应当仍然能够正常运行。
什么因素会打破二进制兼容性?
回过头来看,对于开发库所做的哪种改变,会使得应用程序必须重新编译才能继续工作呢?下面举个例子:
class Widget
{
// ...
private :
Rect m_geometry ;
} ;
class Label : public Widget
{
public :
// ...
String text () const
{
return m_text ;
}
private :
String m_text ;
} ;
此处,我们有一个类Widget,其中有一个成员变量geometry。我们编译Widget,并且以版本 WidgetLib 1.0 的形式发布。
对于后续的 WidgetLib 1.1 版本呢,某人想到了一个好点子,让它支持样式单功能。好嘛,我们毫不费力地加上了对应的新的成员方法,以及一个新的 数据成员 。
class Widget
{
// ...
private :
Rect m_geometry ;
String m_stylesheet ; // WidgetLib 1.1 中新加入的
} ;
class Label : public Widget
{
public :
// ...
String text () const
{
return m_text ;
}
private :
String m_text ;
} ;
我们在做了以上变更之后,发布WidgetLib 1.1。然后,会发现,之前针对WidgetLib 1.0 编译并且正常运行的CuteApp,如今竟然崩溃了!
为什么它会崩溃?
因为,加入一个新的成员变量之后,我们实际上改变了Widget 和Label 对象的尺寸。那么,这个东西狠重要吗? 是的,当妳的C++编译器生成代码时,它使用 偏移 值 (offsets)来访问对象内部的数据。
以下图表,简单地表现出了上面那些 简单数据结构 对象在内存中的布局。
Label对象在WidgetLib 1.0中的布局 | Label对象在WidgetLib 1.1中的布局 |
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 成员,其位置在 (逻辑 上的 ) offset 1 。编译 器为该应用程序生成的代码中,对于 Label::text() 方法 ,具体上是翻译成访问应用程序内存 中label 对象的offset 1 位置。 而在WidgetLib 1.1 中,Label 的 text 成员,其位置已经移动到(逻辑 上的 )offset 2!由于应用程序本身 并未被重新编译,因此, 它仍然认为 text 是位于 offset 1 , 结果 呢,实际访问的是 stylesheet 变量!
到这个时候,一定会有一些人在想, 为什么 Label::text() 的偏移计算代码是位于CuteApp 程序的二进制文件中,而不是位于WidgetLib 的二进制文件中。原因是, Label::text() 的代码是定义于头文件中,而编译器将它 内联 了。
那么 ,如果不将 Label::text() 内联的话,情况会发生变化吗?比如 说,将 Label::text() 的定义移动到源文件中?答案 是,结果不会变化。 C++编译 器要求各个对象的尺寸在编译时和运行时保持不变。 举个例子,考虑运行时栈的压栈(winding)和弹栈(unwinding)操作,如果 妳在栈上创建一个Label 对象,那么 ,编译器会根据在编译时计算出来的Label 的尺寸来生成代码,以在栈上分配空间。由于 使用了WidgetLib 1.1,这样,在运行时,Label 的尺寸已经与编译时不同了,于是 , Label 的构造函数就会覆盖掉已有的栈数据,因而对栈造成破坏。
永远不要改变一个已经导出的C++类的尺寸
总的来说,一旦妳的库已经被发布了,那么,永远不要改变任何一个 已导出的 (也就是说,对于用户是可见的)C++类的尺寸及布局(不要改变成员变量的顺序)。C++编译器,在生成代码时,会遵循一个假设,即,在应用程序被编译完毕 之后 ,每个类的尺寸以及其中数据的顺序将不再改变。
那么,如何在不改变对象的尺寸的前提下,加入新的特性呢?
d-指针
此处使用的技巧就是,保持库中所有公有类的尺寸不变,具体手段就是,仅在公有类中储存单个指针。这个指针,指向一个私有/内部的数据结构,那个数据结构中包含着所有的数据。这个内部结构的尺寸,可以随意地减小或变大,而不会对应用程序产生任何副作用。这是因为,它的指针只会被库中的代码访问到,而从应用程序的角度来看,该对象的尺寸永远不变 - 它永远保持着一个指针的尺寸。这个指针, 就被称为 d- 指针 。
以下代码展示了此种模式的关键思想(此文章中的所有代码都未提供析构函数,但是在实际代码中,妳应当加上它们)。
widget.h
/* 由于d_ptr只是 一个指针,并且永远不会在头文件中被引用
(不然会引起编译错误),所以 WidgetPrivate不需要被包含,
只需要被前向声明。
这个类的定义可写在 widget.cpp 中,
或者 写在一个单独的文件中,例如 widget_p.h 。 */
class WidgetPrivate;
class Widget
{
// ...
Rect geometry () const ;
// ...
private :
WidgetPrivate * d_ptr ;
} ;
widget_p.h , 即是 widget类的私有头文件。
/* widget_p.h (_p表示私有) */
struct WidgetPrivate
{
Rect geometry ;
String stylesheet ;
} ;
widget.cpp
// 通过这句#include代码,我们就可以访问到WidgetPrivate。
#include "widget_p.h"
Widget:: Widget () : d_ptr( new WidgetPrivate)
{
// 创建私有数据
}
Rect Widget:: geometry () const
{
// d-ptr只会 被库代码访问
return d_ptr - > geometry ;
}
接下来,是以Widget 为基类的子类示例。
label.h
class Label : public Widget
{
// ...
String text () ;
private :
// 每个 类都维护自己独有的一个 d-指针
LabelPrivate * d_ptr ;
} ;
label.cpp
// 与WidgetPrivate 不同的是,作者决定直接在源文件中定义LabelPrivate
struct LabelPrivate
{
String text ;
} ;
Label:: Label () : d_ptr( new LabelPrivate)
{
}
String Label:: text ()
{
return d_ptr - > text ;
}
在以上的代码结构中,CuteApp永远不会直接访问到d-指针。并且,由于 d- 指针 只会被WidgetLib 访问,而 WidgetLib 又会在每次发布时被重新编译,因此,那两个Private 类就可以自由地改变其实现方式,而不会影响到CuteApp。
d-指针所带来的其它好处
好处不仅仅局限于二进制兼容性。d-指针还能带来其它好处:
-
•.隐藏实现细节 - 我们在发布WidgetLib 时,只需要发布头文件和二进制文件。那些.cpp文件可以保持闭源状态。
-
•.头文件中不会暴露出实现细节,因而可以作为应用编程接口参考文档来使用。
-
•.由于与实现细节相关的头文件被从头文件中移动到了实现(源代码)文件中,所以,编译起来会更快。
当然,相比之下,以上列出的好处确实只是轻微的好处。在Qt 中使用 d-指针的真正原因就是,保持二进制兼容性,并且,Qt 在一开始是闭源软件。
q-指针
到这个阶段,我们仅仅 是将d-指针作为一个C-风格的数据结构来看待。 而在实际项目中, 它会包含有私有方法 (辅助函数) 。例如, 在 LabelPrivate 中,可能会需要一个辅助函数 getLinkTargetFromPoint() , 它的作用是,在发生鼠标点击时,用来找到链接 的目标地址。 在狠多情况下,这些辅助方法都需要访问到公有类,也就是,访问到Label 或其基类Widget 中的函数。例如 ,某个辅助函数, setTextAndUpdateWidget() ,可能需要调用 Widget::update() ,后者 是一个公有方法,用来计划下次 重绘该Widget。因此 , WidgetPrivate 也会存储一个指向其公有类的指针,称为q-指针。 对上面的代码进行修改,以实现q-指针模式,结果如下:
widget.h
class WidgetPrivate;
class Widget
{
// ...
Rect geometry () const ;
// ...
private :
WidgetPrivate * d_ptr ;
} ;
widget_p.h
struct WidgetPrivate
{
// 构造函数 ,初始化 q-ptr
WidgetPrivate ( Widget * q ) : q_ptr ( q ) { }
Widget * q_ptr ; // q-ptr指向应用编程接口 类
Rect geometry ;
String stylesheet ;
} ;
widget.cpp
#include "widget_p.h"
// 创建私有数据。
// 传入'this'指针,以初始化q-ptr
Widget:: Widget () : d_ptr( new WidgetPrivate( this ))
{
}
Rect Widget:: geometry () const
{
// d-ptr只会 被库代码访问
return d_ptr - > geometry ;
}
接下来,是另一个基于Widget 的类。
label.h
class Label : public Widget
{
// ...
String text () const ;
private :
LabelPrivate * d_ptr ;
} ;
label.cpp
// 与WidgetPrivate 不同的是,作者决定直接在源文件中定义LabelPrivate
struct LabelPrivate
{
LabelPrivate ( Label * q ) : q_ptr ( q ) { }
Label * q_ptr ;
String text ;
} ;
Label:: Label () : d_ptr( new LabelPrivate( this ))
{
}
String Label:: text ()
{
return d_ptr - > text ;
}
继承d-指针,以达到优化目的
在以上代码中,每当创建单个Label 对象 ,就会导致在内存中为 LabelPrivate 和 WidgetPrivate 分配空间。如果 我们在Qt 中采用这种策略,那么,对于像 QListWidget 这样的类,情况会更糟 - 它在类继承层级关系中处于6 层的深度, 这将导致 6 次内存分配!
对于这种问题,解决办法是,让我们的 私有 类组成继承层级关系,并让实际被实例化的类向上逐层传递单个d-指针。
注意,在进行 d-指针的继承时,私有类的声明就必须放置在单独的文件中,例如widget_p.h。再也不能将它声明到widget.cpp 文件中了。
widget.h
class Widget
{
public :
Widget () ;
// ...
protected :
// 只有子类能够访问 以下部分
// 允许子类 以自己独有的实体Private类来进行初始化
Widget ( WidgetPrivate & d ) ;
WidgetPrivate * d_ptr ;
} ;
widget_p.h
struct WidgetPrivate
{
WidgetPrivate ( Widget * q ) : q_ptr ( q ) { } // 构造函数 ,初始化 q-ptr
Widget * q_ptr ; // q-ptr ,指向应用编程接口类
Rect geometry ;
String stylesheet ;
} ;
widget.cpp
Widget:: Widget () : d_ptr( new WidgetPrivate( this ))
{
}
Widget:: Widget (WidgetPrivate &d) : d_ptr( &d)
{
}
label.h
class Label : public Widget
{
public :
Label () ;
// ...
protected :
Label ( LabelPrivate & d ) ; // 允许Label 的子类来传入它们自己独有的 Private 类
// 注意 , Label 并不具有 d_ptr !它直接使用Widget的d_ptr。
} ;
label.cpp
#include "widget_p.h"
class LabelPrivate : public WidgetPrivate
{
public :
String text ;
} ;
Label:: Label ()
: Widget ( * new LabelPrivate ) // 使用 我们自己独有的Private类来初始化 d-指针
{
}
Label:: Label (LabelPrivate &d) : Widget(d)
{
}
有没有感受到其中的美妙之处?如今, 当我们创建一个 Label 对象时,它会创建一个 LabelPrivate ( 它是的 WidgetPrivate 子类 ) 。 它向Widget 的受保护构造函数中传入了实体 d- 指针 ! 这样,当某个 Label 对象被创建时,只会发生一次内存分配。 Label 同样有一个 受 保护的构造函数, 可 由其子类传入独有的私有类 。
Qt中的d-指针
在Qt 中,实际上 , 公有类都使用了d-指针技术。只有少数情况 下没有使用这种技术,对于那些特例 ,都是事先明确知道 了,日后 再也不会向那个 类中加入 新的成员变量了。例如 ,对于像 QPoint 和 QRect 这样的类来说, 可以预期,不会再向它们添加任何新的成员了 。因此,数据成员 都是直接储存 在那些类本身, 而不是使用d-指针。
注意 ,在Qt 中,所有Private 对象 的基类都是 QObjectPrivate 。
Q_D和Q_Q
我们在之前的步骤中进行的优化,导致了一个副作用,那就是, q-ptr 和 d-ptr 的类型分别是 Widget 和 WidgetPrivate 。 这就意味着,以下代码无法正常工作。
void Label:: setText ( const String &text)
{
// 无法正常工作 !因为,尽管 d_ptr指向 一个LabelPrivate 对象,但它 的类型是 WidgetPrivate
d_ptr - > text = text ;
}
因此,在从子类中访问时d-指针,我们需要将它静态转换(static_cast)成适当的类型。
void Label:: setText ( const String &text)
{
LabelPrivate * d = static_cast < LabelPrivate * > ( d_ptr ) ; // 转换 成我们自己的私有类型
d - > text = text ;
}
如妳所见,如果代码中到处都是static_cast 的话,狠丑。为了解决这个问题,在src/corelib/global/qglobal.h 中定义了两个宏,使得我们可以写出直观的代码:
global.h
#define Q_D(Class) Class##Private * const d = d_func()
#define Q_Q(Class) Class * const q = q_func()
label.cpp
// 利用Q_D,妳就可以从Label中使用 LabelPrivate 的成员了
void Label:: setText ( const String &text)
{
Q_D ( Label ) ;
d - > text = text ;
}
// 利用Q_Q,妳就可以从LabelPrivate中使用 Label的成员了
void LabelPrivate:: someHelperFunction ()
{
Q_Q ( Label ) ;
q - > selectAll () ;
}
Q_DECLARE_PRIVATE和Q_DECLARE_PUBLIC
对于Qt 类,还 可以 在公有类中使用 Q_DECLARE_PRIVATE 宏。 这个宏的定义如下:
qglobal.h
#define Q_DECLARE_PRIVATE(Class)\
inline Class##Private* d_func() {\
return reinterpret_cast<Class##Private*>(qGetPtrHelper(d_ptr));\
}\
inline const Class##Private d_func() const {\
return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr));\
}\
friend class Class##Private;
这个宏,可以如下使用:
qlabel.h
class QLabel
{
private :
Q_DECLARE_PRIVATE ( QLabel ) ;
} ;
这种做法,其原理是,让 QLabel 提供一个函数 d_func() , 以便于让外界以某种手段来访问到它的私有内部类。 这个方法本身是私有的 (因为 这个宏位于qlabel.h 的私有区域 ) 。 但是 , d_func() 可被 QLabel 的 友元对象 (C++ 友元对象 )调用。 这种方法,其主要目的在于, 让某些无法通过公有接口访问 QLabel 中某些信息的Qt 类能够访问到对应的信息。 举个特殊例子, QLabel 可能 会记录下来,用户 在某个链接上点击了多少次 。但是 ,并没有公有的接口使得其它类能够访问到这个信息。 QStatistics 是一个工具 类,它需要这个信息。那么,某个 Qt开发 者会将 QStatistics 添加为 QLabel 的友元类, 这样, QStatistics 就可以调用 label->d_func()->linkClickCount 。
d_func 还有一个优点,它能够确保常量正确性(const-correctness): 在 MyClass 的常量 (const) 成员函数中,妳需要使用 Q_D(const MyClass) ,因而 , 就只能调用MyClassPrivate 中的常量(const)成员函数。如果使用 裸露 的 d_ptr ,那么,妳将会也能够调用非常量(non-const)函数。