什么是D指针
如果你已经看过到Qt源码,你会发现它经常使用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构建的窗口仍应能够运行。
什么破坏了二进制兼容性?
那么,库中的更改何时需要重新编译应用程序?让我们举一个简单的例子:
class Widget
{
// ...
private:
Rect m_geometry;
};
class Label:public Widget
{
public:
// ...
string text()const
{
return m_text;
}
private:
string m_text;
};
在这里,我们有一个小部件,其中包含几何作为成员变量。我们编译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 ++编译器生成代码时,它使用偏移量来访问对象内的数据(一般项目,在头文件有过改动,都需要应用程序重新编译,免除一些奇怪的问题出现)。
这是上述POD对象在内存中的外观的简化版本。
WidgetLib 1.0中的标签对象布局 | WidgetLib 1.1中的标签对象布局 |
---|---|
m_geometry <offset 0> | m_geometry <offset 0> |
— | m_stylesheet <offset 1> |
m_text | — |
— | m_text <offset 2> |
在WidgetLib 1.0中,Label的文本成员位于(逻辑)偏移量1。由应用程序中的编译器为Label :: text()
方法生成的代码转换为访问应用程序中Label对象的偏移量1。在WidgetLib 1.1中,Label的文本成员已移至(逻辑)偏移量2!由于尚未重新编译应用程序,因此它继续认为文本
在偏移量1处,并最终访问了 样式表
变量!
我可以肯定地说,有些人想知道为什么Label :: text()
的偏移量计算代码最终出现在CuteApp二进制文件而不是WidgetLib二进制文件中。答案是在头文件中定义了Label :: text()
的代码,编译器最终对其进行了*内联*。
那么,如果没有内联Label :: text()
,情况会改变吗?说,Label :: text()
已移至源文件?好吧,不。C ++编译器依赖于在编译时和运行时相同的对象大小。例如,堆栈缠绕/展开-如果在堆栈上创建了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_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
{
//仅在库代码
return d_ptr->geometry;
}
接下来,有一个基于Widget的子类的示例。
Label.h
class Label:public Widget
{
// ...
string text();
private:
//每个类维护自己的d指针
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指针。并且由于仅在WidgetLib中访问了d指针,并且每个版本都对WidgetLib进行了重新编译,因此Private类可以自由更改,而不会影响CuteApp。
d指针的其他好处
二进制兼容性并不是全部。d指针还有其他好处:
- 隐藏实现细节-我们可以将WidgetLib与头文件和二进制文件一起提供。.cpp文件可以是封闭源。
- 头文件中没有实现细节,可以用作API参考。
- 由于实现所需的头文件已从头文件移至实现(源)文件,因此编译速度要快得多。
确实,上述好处显得微不足道。在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
{
// 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
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;
}
继承d指针进行优化
在上面的代码中,创建单个Label将导致为LabelPrivate
和WidgetPrivate
分配内存。如果我们将这种策略用于Qt,则对于类似QListWidget的
类,情况将变得更加糟糕-它在类继承层次结构中位于6级,这将导致最多6个内存分配(每一个继承都会分配一个d指针的内存分配)!
这可以通过为我们的私有类建立继承层次结构,并使实例化的类一直沿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)
{
}
你看到这么使用的优点了吗?现在,当我们创建Label
对象时,它将创建LabelPrivate
(子类WidgetPrivate
)。它将具体的d指针传递给Widget的受保护构造函数!现在,创建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的类型为WidgetPrivate,即使它
//指向LabelPrivate对象
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
// 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();
}
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 ++朋友)调用。这对于Qt类访问信息非常有用,因为Qt类无法使用公共api访问某些QLabel
信息。举一个奇怪的例子,QLabel
可能会跟踪用户单击链接的次数。但是,没有公共API可以访问此信息。QStatistics
是需要此信息的类。Qt开发人员将添加`````````QStatistics作为
QLabel的朋友,然后
QStatistics可以执行
label-> d_func()-> linkClickCount`。
该d_func
还具有的优点是执行常量正确性:在MyClass的一个const成员函数,你需要一个Q_D(常量MyClass的)
,因此你只能调用const成员函数中MyClassPrivate。使用裸露的d_ptr,您还可以调用非const函数。