D-Pointer/zh
This article may require cleanup to meet the Qt Wiki's quality standards. Reason: Auto-imported from ExpressionEngine. Please improve this article if you can. Remove the {{cleanup}} tag and add this page to Updated pages list after it's clean. |
Contents
什么是d-pointer
如果你曾经查看过Qt的源代码文件,例如 这个Q_D
Q_Q
Q_D
Q_Q
什么是二进制兼容
在设计像 Qt 这样的类库的时候,理想的行为应该是动态连接到 Qt 的应用程序,甚至在 Qt 类库升级或者替换到另外一个版本的时候,不需要重新编译就可以继续运行。例如,你的应用程序 CuteApp 是基于 Qt 4.5 的,你应该可以把你的 Qt 由4.5升级到 Qt 4.6 (在 Windows 下用安装程序,在 Linux 下通常有包管理器自动完成),而你用 Qt 4.5 构建的 CuteApp 应该还可以运行。
什么会打破二进制兼容
那么,什么时候类库的变化需要应用程序的重新编译呢? 我们来看一个简单的例子:
class Widget {
…
private:
Rect m_geometry;
};
class Label : public Widget {
…
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; // 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 光荣地崩溃了!
为什么会崩溃
究其原因,通过添加了一个新的数据成员,我们最终改变了 Widget 和 Label 对象的大小。为什么会这样?因为当你的C+编译器生成代码的时候,他会用`偏移量`来访问对象的数据。
下面是一个 POD 对象在内存里面布局的一个简化版本。
| Label 对象在 WidgetLib 1.0的布局| Label 对象在 WidgetLib 1.1的布局 | | m_geometry <偏移量 0> | m_geometry <偏移量 0>| | —————- | m_stylesheet <偏移量 1>| | m_text <偏移量 1> | —————- | | —————- | m_text <偏移量 2>|
在 WidgetLib 1.0中,Label 的 text 成员在(逻辑)偏移量为1的位置。在编译器生成的代码里,应用程序的方法Label::text()
text
stylesheet
Label::text()
Label::text()
Label::text()
Label::text()
不要改变导出的 C+ 类的大小
总之,一旦你的类库发布了,永远不要改变 导出的 C++ 类的大小或者布局(不要移动成员)。C++ 编译器生成的代码会假定,一个类的大小和成员的顺序 编译后 就不会改变.
那么,如何在不改变对象的大小的同时添加新的功能呢?
d-pointer
诀窍是通过保存唯一的一个指针而保持一个类库所有公共类的大小不变。这个指针指向一个包含所有数据的私有的(内部的)数据结构。内部结构的大小可以增大或者减小,而不会对应用程序带来副作用,因为指针只会被类库里面的代码访问,从应用程序的视角来看,对象的大小并没有改变 - 它永远是指针的大小。 这个指针被叫做 d-pointer 。
这个模式的精髓可以由下面的代码来概述(本文中的所有代码都没有析构函数,在实际使用的时候应该加上它)。
/* widget.h */
// 前置声明. 定义在 widget.cpp 或者
// 单独的一个文件,比如 widget_p.h
class WidgetPrivate;
class Widget {
…
Rect geometry() const;
…
private:
// d-pointer never referenced in header file.
// Since WidgetPrivate is not defined in this header,
// any access will be a compile error
WidgetPrivate '''d_ptr;
};
/''' widget_p.h */ (_p 意味着私有)
struct WidgetPrivate {
Rect geometry;
String stylesheet;
};
/''' widget.cpp */
#include "widget_p.h"
Widget::Widget()
: d_ptr(new WidgetPrivate) {
// 创建私有数据
}
Rect Widget::geoemtry() const {
// d-ptr 仅仅被类库代码访问
return d_ptr->geometry;
}
/''' label.h */
class Label : public Widget {
…
String text();
private:
// 每个类维护自己的 d-pointer
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-pointer。由于 d-pointer 只是在 WidgetLib 被访问,而 WidgetLib 在每一次发布都被重新编译,私有的类可以随意的改变而不会对 CuteApp 带来影响。
d-pointer 的其它好处
这里不全都是和二进制兼容有关。d-pointer 还有其它的好处: 隐藏了实现细节 - 我们可以只发布带有头文件和二进制文件的 WidgetLib。源文件可以是闭源代码的。
- 头文件很干净,不包含实现细节,可以直接作为 API 参考。
- 由于实施需要的包含的头文件从头文件里已到了实施(源文件)里面,编译速更快。(译:降低了编译依赖)
事实上,上边的好处是微乎其微的。Qt 使用 d-pointer 的真正原因是为了二进制兼容和 Qt 最初是封闭源代码的.(译:Qt 好像没有封闭源代码)
q-pointer
到目前为止,我们仅仅看到的是作为 C 风格的数据机构的 d-pointer。实际上,它可以包含私有的方法(辅助函数)。例如,LabelPrivate
getLinkTargetFromPoint()
setTextAndUpdateWidget()
Widget::update()
WidgetPrivate
/* widget.h */
// 前置声明. 定义在 widget.cpp
// 或者单独的一个文件,比如 widget_p.h
class WidgetPrivate;
class Widget {
…
Rect geometry() const;
…
private:
// d-pointer 从来不在头文件被引用
// 因为WidgetPrivate没有在头文件里定义,
// 所有对它的访问都会带来编译错误
WidgetPrivate '''d_ptr;
};
/''' widget_p.h */ (_p意味着私有)
struct WidgetPrivate {
// constructor that initializes the q-ptr
WidgetPrivate(Widget *q) : q_ptr(q) { }
Widget '''q_ptr; // q-ptr that 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
Widget::Widget()
: d_ptr(new WidgetPrivate(this)) {
}
Rect Widget::geoemtry() const {
// the d-ptr is only accessed in the library code
return d_ptr->geometry;
}
/''' label.h */
class Label : public Widget {
…
String text() const;
private:
LabelPrivate '''d_ptr; // each class maitains it's own d-pointer
};
/''' label.cpp */
// Unlike WidgetPrivate, we define LabelPrivate in the source file
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
QListWidget
通过对我们的 私有 类添加一个继承层次,解决了这个问题,这样类实例化时将一个 d-pointer 层层向上传递。
/''' widget.h */
class Widget {
public:
Widget();
…
protected:
// only sublasses may access the below
Widget(WidgetPrivate &d); // 允许子类通过他们自己的实体私有对象来初始化
WidgetPrivate '''d_ptr;
};
/''' widget_p.h */ (_p means private)
struct WidgetPrivate {
WidgetPrivate(Widget *q) : q_ptr(q) { } // 构造函数初始化 q-ptr
Widget '''q_ptr; // 指向API类的
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的子类传递自己的私有数据
//注意 Label 没有 d_ptr!它用了父类 Widget 的 d_ptr。
};
/''' label.cpp */
#include "widget_p.h" // 所以我们能够访问 WidgetPrivate
class LabelPrivate : public WidgetPrivate {
public:
String text;
};
Label::Label()
: Widget(*new LabelPrivate) {
// 用我们自己的私有对象来初始化 d-pointer
}
Label::Label(LabelPrivate &d)
: Widget(d) {
}
Label
LabelPrivate
WidgetPrivate
Label
把 q-ptr 和 d-ptr 转型到正确的类型(QPTR 和 DPTR)
上一步优化的一个副作用是 q-ptr 和 d-ptr 的类型分别是Widget
WidgetPrivate
这就意味着下面的代码不能工作。
void Label::setText(const String &text) {
// 不工作。虽然 d_ptr 指向 LabelPrivate 对象,但是它是 WidgetPrivate 类型
d_ptr->text = text;
}
因此,在子类里访问 d-pointer 的时候,需要用 static_cast 转型到合适的类型。
void Label::setText(const String &text) {
LabelPrivate '''d = static_cast<LabelPrivate'''>(d_ptr); // cast to our private type
d->text = text;
}
代码里到处都是 static_cast 看起来不是那么漂亮,所以我们定义了下面的宏,
// global.h (macros)
#define DPTR (Class) Class##Private '''d = static_cast<Class##Private'''>(d_ptr)
#define QPTR (Class) Class '''q = static_cast<Class'''>(q_ptr)
// label.cpp
void Label::setText(const String &text) {
DPTR (Label);
d->text = text;
}
void LabelPrivate::someHelperFunction() {
QPTR (label);
q->selectAll(); // we can call functions in Label now
}
Qt 中的 d-pointers
在 Qt 中,几乎所有的公有类都使用了 d-pointer。唯一不用的情况是如果事先知道某个类永远不会添加额外的成员变量。例如,像QPoint
QRect
- 在Qt中,所有私有对象的基类是
QObjectPrivate
- 和
Q_D
宏提供了上边讨论的 QPTR 和 DPTR 的功能.Q_Q
- Qt 的公有类有一个 的宏。这个宏的代码:
Q_DECLARE_PRIVATE
// qlabel.h
class QLabel {
private:
Q_DECLARE_PRIVATE(QLabel);
};
// 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
d_func()
d_func()
QLabel
QLabel
QLabel
QStatistics
QStatistics
QLabel
QStatistics
label->d_func()->linkClickCount
d_func