如果你已经看过到Qt源文件像QLablel,
QPicture QLabel::picture() const { Q_D(const QLabel); if (d->picture) return *(d->picture); return QPicture(); }
你会发现总体不时含有Q_D
和Q_Q
宏。本文揭示了这些宏的用途。
该Q_D
和Q_Q
宏是一个设计模式的一部分被称为d-指针(也称为不透明的指针)其中一个库的实现细节可以向用户隐藏,并且代码实现的改变编译到库可以不破坏库的二进制兼容性。
这也是最重要的原因。
在设计像 Qt 这样的库时,希望动态链接到 Qt 的应用程序继续运行而无需重新编译,即使在 Qt 库升级/替换为另一个版本之后。例如,如果您的应用程序CuteApp基于 Qt 4.5,您应该能够将 Qt 库(在随应用程序一起附带提供的 Windows 上,在 Linux 上通常由包管理器自动完成)从版本 4.5 升级到 Qt 4.6,并且您的CuteApp用Qt 4.5构建的应该还是可以运行的。
那么什么是二进制兼容性呢?
库是二进制兼容的,如果动态链接到库的先前版本的程序继续使用库的较新版本运行而无需重新编译。
记住这个定义,很容易看出:没有它,每当一个库更新时,所有依赖它的应用程序都才能工作。当然,对于像 Qt 这样的任何广泛使用的库来说,这是完全不可接受的。更多关于二进制兼容性本身的信息可以在 KDE TechBase 的那篇文章中找到,在这里我只想和你分享它是如何使用的。
那么,库中的更改何时需要重新编译应用程序?我们举一个简单的例子:
class Widget { // ... private: Rect m_geometry; }; class Label : public Widget { public: // ... String text() const { return m_text; } private: String m_text; };
这里我们有一个包含几何( geometry )作为成员变量的小部件。我们编译我们的 Widget 并将其作为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.1 并进行了上述更改,结果发现在 WidgetLib 1.0 中编译并运行得很好的 CuteApp 崩溃了!
注:
如果CuteApp重新编译是不会崩溃的。
原因是通过添加一个新的数据成员,我们最终改变了 Widget 和 Label 对象的大小。为什么这很重要?当您的 C++ 编译器生成代码时,它使用偏移量来访问对象内的数据。
这是上述POD对象在内存中的外观的过度简化版本。
Label object layout in WidgetLib 1.0 | Label object layout in WidgetLib 1.1 |
---|---|
m_geometry |
m_geometry |
- - - | m_stylesheet |
m_text |
- - - |
- - - | m_text |
在 WidgetLib 1.0 中,Label 的 text 成员位于(逻辑地址)偏移量 1。此代码段被编译器编译后,在应用程序中为Label::text()方法使用,在应用程序中,转换为访问label对象的偏移量 1。在 WidgetLib 1.1 中,Label的 text 成员已经移动到(逻辑地址)偏移量 2!由于应用程序还没有被重新编译,它继续认为 text 位于偏移量 1 处并最终访问 stylesheet
变量!
注:
在编译时,编译器为Label对象按照其大小在内存上分配了空间。而在运行时,由于Widget中m_stylesheet的加入导致Label的构造函数重写了已经存在的内存空间,导致了程序崩溃。 所以只要版本已发布,除非重新编译工程,否则就不能更改类的结构和大小。那么,为了能够为原有类方便的引入新的功能,这就是Qt引入D指针的目的。
我相信此时有一些人想知道为什么Label::text()
的偏移量计算代码最终出现在 CuteApp 二进制文件中,而不是 WidgetLib 二进制文件中。答案是Label::text()
的代码是在头文件中定义的,编译器最终将其内联。
那么,如果Label::text()
没有被内联,情况会改变吗?例如,Label::text()
被移动到源文件?嗯,不。C++ 编译器依赖于在编译时和运行时相同的对象大小。例如,堆栈 winding/unwinding (前进/展开)——如果您在堆栈上创建了一个 Label 对象,编译器会生成代码以在编译时根据 Label 的大小在堆栈上分配空间。在 WidgetLib 1.1中,由于运行时 Label 的大小不同,Label 的构造函数会写覆盖现有堆栈数据并最终破坏堆栈。
总之,一旦您的库发布,永远不要更改已导出的(即对用户可见的)C++ 类的大小或布局(不要移动数据)。在应用程序编译后,C++ 编译器生成代码,可以假定类中数据的大小或顺序不会改变。
那么,如何既不改变对象的大小又增加新的功能呢?
诀窍是通过只存储一个指针来保持库的所有公共类的大小不变。该指针指向包含所有数据的私有/内部数据结构。这个内部结构的大小可以缩小或增加,而不会对应用程序产生任何副作用,因为指针只能在库代码中访问,并且从应用程序的角度来看,对象的大小永远不会改变——它始终是对象的大小指针。这个指针称为d 指针。
下面的代码概括了这种设计模式的精神(本文中的所有代码都没有析构函数,当然您应该在实际代码中添加它们)。
widget.h
/* Since d_ptr is a pointer and is never referended in header file (it would cause a compile error) WidgetPrivate doesn't have to be included, but forward-declared instead. The definition of the class can be written in widget.cpp or in a separate file, say widget_p.h */ class WidgetPrivate; class Widget { // ... Rect geometry() const; // ... private: WidgetPrivate *d_ptr; };
widget_p.h,是widget类的私有头文件
/* widget_p.h (_p means private) */ struct WidgetPrivate { Rect geometry; String stylesheet; };
widget.cpp
// With this #include, we can access 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; }
接下来,有一个基于 Widget 的子类的示例。
label.h
class Label : public Widget { // ... String text(); private: // Each class maintains its own d-pointer LabelPrivate *d_ptr; };
label.cpp
// Unlike WidgetPrivate, the author decided LabelPrivate // to be defined in the source file itself struct LabelPrivate { String text; }; Label::Label() : d_ptr(new LabelPrivate) { } String Label::text() { return d_ptr->text; }
通过上述结构,CuteApp 不会直接访问 d 指针。由于d-pointer只在 WidgetLib 中被访问过,并且 WidgetLib 会在每次发布时重新编译,因此 Private 类可以自由更改,而不会影响 CuteApp。
这不仅仅是关于二进制兼容性。d 指针还有其他好处:
隐藏实现细节——我们可以只用头文件和二进制文件来发布 WidgetLib。.cpp 文件可以是闭源的。
头文件没有实现细节,可以作为 API 参考。
由于实现所需的头文件从头文件移动到实现(源)文件中,因此编译速度要快得多。
确实,上述好处看起来微不足道。在 Qt 中使用 d 指针的真正原因是为了二进制兼容性以及 Qt 最初是封闭源代码的事实。
到目前为止,我们只将 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 { // Constructor that initializes the q-ptr WidgetPrivate(Widget *q) : q_ptr(q) { } Widget *q_ptr; // q-ptr points to the API class Rect geometry; String stylesheet; };
widget.cpp
#include "widget_p.h" // Create private data. // Pass the 'this' pointer to initialize the q-ptr,将this指针初始化q-ptr Widget::Widget() : d_ptr(new WidgetPrivate(this)) { } Rect Widget::geometry() const { // the d-ptr is only accessed in the library code return d_ptr->geometry; }
接下来是另一个基于Widget的类。
label.h
class Label : public Widget { // ... String text() const; private: LabelPrivate *d_ptr; };
label.cpp
// Unlike WidgetPrivate, the author decided LabelPrivate // to be defined in the source file itself 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; }
在上面的代码中,创建单个 Label 会导致为LabelPrivate
和WidgetPrivate
分配内存。如果我们在 Qt 中采用这种策略,对于像QListWidget
这样的类,情况会变得更糟——它在类继承层次结构中有 6 层深,并且会导致多达 6 次内存分配!
这个问题的解决方法是通过为我们的私有类建立继承层次结构并让类被实例化一直向上传递 d 指针来解决的。
请注意,在继承 d 指针时,私有类的声明必须在单独的文件中,例如 widget_p.h。不再可能在 widget.cpp 文件中声明它。
widget.h
class Widget { public: Widget(); // ... protected: // only subclasses may access the below // allow subclasses to initialize with their own concrete Private Widget(WidgetPrivate &d); WidgetPrivate *d_ptr; };
widget_p.h
struct WidgetPrivate { WidgetPrivate(Widget *q) : q_ptr(q) { } // constructor that initializes the q-ptr Widget *q_ptr; // q-ptr that points to the API class 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); // allow Label subclasses to pass on their Private // notice how Label does not have a d_ptr! It just uses Widget's d_ptr. };
label.cpp
#include "widget_p.h" class LabelPrivate : public WidgetPrivate { public: String text; }; Label::Label() : Widget(*new LabelPrivate) // initialize the d-pointer with our own Private { } Label::Label(LabelPrivate &d) : Widget(d) { }
在 Qt 中,几乎每个公共类都使用 d 指针方法。唯一不使用它的情况是事先知道该类永远不会添加额外的成员变量。例如,对于QPoint
、QRect
等类,预计不会添加新成员,因此数据成员直接存储到类本身中,而不是使用 d 指针。
请注意,在 Qt 中,所有 Private 对象的基类都是QObjectPrivate
。
我们在上一步中进行的优化的一个副作用是 q-ptr 和 d-ptr 属于Widget
和WidgetPrivate
类型。这意味着以下内容将不起作用。
void Label::setText(const String &text) { // won't work! since d_ptr is of type WidgetPrivate even though // it points to LabelPrivate object //不起作用的,因为d_ptr是WidgetPrivate类型的,即使其指向LabelPrivate对象 d_ptr->text = text; d_ptr->text = text; }
因此,当访问子类中的 d 指针时,我们需要将 static_cast 转换为适当的类型。
void Label::setText(const String &text) { LabelPrivate *d = static_cast(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
// With Q_D you can use the members of LabelPrivate from Label void Label::setText(const String &text) { Q_D(Label); d->text = text; } // With Q_Q you can use the members of Label from LabelPrivate void LabelPrivate::someHelperFunction() { Q_Q(Label); q->selectAll(); }
qglobal.h
#define Q_DECLARE_PRIVATE(Class)\ inline Class##Private* d_func() {\ return reinterpret_cast(qGetPtrHelper(d_ptr));\ }\ inline const Class##Private* d_func() const {\ return reinterpret_cast (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++ 朋友)调用。这主要用于 Qt 类访问信息时无法使用公共 api访问某些QLabel
信息。作为一个奇怪的例子,QLabel
可能会跟踪用户点击链接的次数。但是,没有用于访问此信息的公共 API。QStatistics
是一个需要这些信息的类。Qt 开发人员将添加QStatistics
作为QLabel
的友元,然后QStatistics
可以执行label->d_func()->linkClickCount
。
该d_func
还具有的优点是执行常量,正确性:在MyClass的一个const成员函数,你需要一个 Q_D(const MyClass)
,因此你只能调用const成员函数中MyClassPrivate。使用裸d_ptr,您还可以调用非常量函数。