Qt 中的私有类中个别都会蕴含 d_ptr 这样一个公有类型的指针,指针指向该类对应的公有类,引入这个指针次要是为了解决二进制兼容的问题。
什么是二进制兼容
Qt 作为一个第三方库,公布后会有很多私有类提供给第三方应用,例如 QWidget 这类控件类。如果 Lib1.0 版本中蕴含以下实现。
class Widget {
…
private:
Rect m_geometry;
};
class Label : public Widget {
…
String text() const { return m_text;}
private:
String m_text;
};
当降级为 Lib2.0 后,新增了 m_stylesheet 这样一个成员。
class Widget {
…
private:
Rect m_geometry;
String m_stylesheet; // new stylesheet member
};
class Label : public Widget {
public:
…
String text() const { return m_text;}
private:
String m_text;
};
如果第三方利用进行了这样的降级,须要从新编译 App,否则会导致解体。究其原因,通过增加了一个新的数据成员,咱们最终扭转了 Widget 和 Label 对象的大小。为什么会这样?因为当你的 C ++ 编译器生成代码的时候,他会用偏移量来拜访对象的数据。上面是一个对象在内存外面布局的一个简化版本。| Label 对象在 Lib1.0 的布局 | Label 对象在 Lib2.0 的布局 | | m_geometry < 偏移量 0> | m_geometry < 偏移量 0>| | —————- | m_text < 偏移量 1>| | m_stylesheet < 偏移量 1> | —————- | | —————- | m_text < 偏移量 2>|,在 Lib 1.0 中,Label 的 text 成员在(逻辑)偏移量为 1 的地位。在编译器生成的代码里,应用程序的办法 Label::text() 被翻译成拜访 Label 对象外面偏移量为 1 的地位。在 Lib 2.0 中,Label 的 text 成员的(逻辑)偏移量被转移到了 2 的地位!因为应用程序没有从新编译,它依然认为 text 在偏移量 1 的地位,后果却拜访了 stylesheet 变量!为什么 Label::text() 的偏移量的计算的代码会在 App 二进制文件执行,而不是 Lib 的二进制文件。答案是因为 Label::text() 的代码定义在头文件外面,最终被内联。
那么如果 Label::text() 没有定义为内联函数,状况会扭转吗?这么讲,Label::text() 被移到源文件外面?也不会。C 编译器依赖对象大小在编译时和运行时雷同。比方,堆栈的 winding/unwinding – 如果你在堆栈上创立了一个 Label 对象,编译器产生的代码会依据 Label 对象在编译时的大小在堆栈上调配空间。因为 Label 的大小在 Lib 2.0 运行时曾经不同,Label 的构造函数会笼罩曾经存在的堆栈数据,最终毁坏堆栈。
因而最好不要轻易扭转导出的 C++ 类的大小或者布局(不要挪动成员)。C++ 编译器生成的代码会假设,一个类的大小和成员的程序编译后就不会扭转。
d_ptr 与 q_ptr
须要增加新的性能而不想扭转导出类的大小和布局,就须要通过 d_ptr 的形式来实现了。窍门是通过保留惟一的一个指针而放弃一个类库所有公共类的大小不变。这个指针指向一个蕴含所有数据的公有的(外部的)数据结构。内部结构的大小能够增大或者减小,而不会对应用程序带来副作用,因为指针只会被类库外面的代码拜访,从应用程序的视角来看,对象的大小并没有扭转 – 它永远是指针的大小。这个指针被叫做 d-pointer。
d-pointer 实际上能够蕴含公有的办法(辅助函数)。例如,LabelPrivate 能够有一个 getLinkTargetFromPoint() 辅助函数,这些辅助函数须要拜访私有类,也就是 Label 或者它的父类 Widget 的一些函数。比方调用一个安顿重画 Widget 的私有办法 Widget::update()。所以,WidgetPrivate 存储了一个指向私有类的指针,称为 q -pointer。批改上边的代码引入 q -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 {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 {
// d-ptr 仅仅被类库代码拜访
return d_ptr->geometry;
}
/* label.h */
class Label : public Widget {
…
String text();
private:
// 每个类保护本人的 d-pointer
LabelPrivate *d_ptr;
};
这里间接在 cpp 中定义 LabelPrivate。
/* label.cpp */
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;}
进一步改良
当上述形式在存在继承关系的类中应用时,因为 Private 公有的类没有建设继承关系,导致 Label 类进行实例化时会调配两次内存,WidgetPrivate 和 LabelPrivate。这里通过增加一个公有类的继承关系来解决这个问题。
/* widget.h */
class Widget {
public:
Widget();
…
protected:
Widget(WidgetPrivate *d); // 容许子类通过他们本人的实体公有对象来初始化
WidgetPrivate *d_ptr;
};
因为公有类中有须要拜访公共接口的需要,这里引入 q_ptr 来指向公有类对应的公共类。
/* widget_p.h */
struct WidgetPrivate {WidgetPrivate(Widget *q) : q_ptr(q) { }
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:
LabelPrivate(Widget *q);
String text;
};
LabelPrivate::LabelPrivate(Widget *q)
: WidgetPrivate(q)
{
}
Label::Label()
: Widget(new LabelPrivate(this))) {// 用咱们本人的公有对象来初始化 d-pointer,用本人来初始化 q -pointer}
Label::Label(LabelPrivate *d)
: Widget(d) {}
上一步优化的一个副作用是 q-ptr 和 d-ptr 的类型别离是 Widget 和 WidgetPrivate。这就意味着须要子类类型时要进行 static_cast 这样的转换。
void Label::setText(const String &text) {LabelPrivate d = static_cast<LabelPrivate>(d_ptr); // cast to our private type
d->text = text;
}
能够定义这样的宏来使代码变得简洁。
#define DPTR(Class) Class##Private d = static_cast<Class##Private>(d_ptr)
#define QPTR(Class) Class q = static_cast<Class>(q_ptr)
void LabelPrivate::someHelperFunction() {QPTR(label);
q->selectAll(); // we can call functions in Label now}
void Label::setText(const String &text) {DPTR(Label);
d->text = text;
}
到这里就根本实现了 Qt 中 d_ptr 和 q_ptr 的作用。