为什么Qt源码中要用d_ptr和q_ptr

为什么需要d_ptr和q_ptr

   Qt中的公有类中一般都会包含d_ptr这样一个私有类型的指针,指针指向该类对应的私有类,引入这个指针主要是为了解决二进制兼容的问题。q_ptr是和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;
}

  可以定义这样的宏来使代码变得简洁。到这里就解析清楚了Qt中d_ptr和q_ptr的作用。

#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;
}
posted @ 2024-08-24 09:43  rainInSunny  阅读(32)  评论(0编辑  收藏  举报