了解QT中引入D指针的初衷前需要了解下二进制兼容
1. 什么是二进制兼容
二进制兼容是针对动态链接库而言的,如果你的源代码已经编译成为可执行文件了,其中你调用了一些动态库,后来你为这个动态库进行了更新,并替换掉原来的动态库了,如果程序在运行时正常运行没有任何问题,不必重新编译使用这个库的可执行文件或使用这个库的其他库文件,那么这个库就是二进制兼容(二进制边界正常)。
2. 二进制不兼容会造成什么后果
如果库A升级没有能够做到二进制兼容,那么所有依赖它的程序(或库)都需要重新编译,否则会出现各种未知异常,其直接现象就是程序莫名其妙地挂掉。
3. C++如何生成可执行程序
C++的ABI
就是如何使用C++生成可执行程序的一张说明书。编译器会根据这个说明书,生成二进制代码。C++的ABI
在不同的编译器下会略有不同。C++ABI
的部分内容举例:
- 函数参数传递的方式,比如 x86-64 用寄存器来传函数的前 4 个整数参数
- 虚函数的调用方式,通常是 vptr/vtbl 然后用 vtbl[offset] 来调用
- struct 和 class 的内存布局,通过偏移量来访问数据成员
4. 哪些常见做法会破坏二进制兼容
改变了虚函数的offset或者成员的顺序
- 添加新的虚函数,会造成 vtbl 里的排列变化。(不要考虑“只在末尾增加”这种取巧行为,因为你的 class 可能已被继承。)
- 不导出或者移除一个导出类
- 改变类的继承
- 改变虚函数声明时的顺序(偏移量改变,导致调用失败)
- 添加新的非静态成员变量(类的内存布局改变,偏移量也发生变化)
- 改变非静态成员变量的声明顺序
- 增加默认模板类型参数
- 改变 enum 的值,把 enum Color { Red = 3 }; 改为 Red = 4。这会造成错位。当然,由于 enum 自动排列取值,添加 enum 项也是不安全的,除非是在末尾添加
- 给函数增加默认参数,现有的可执行文件无法传这个额外的参数
4. 哪些做法多半不会破坏二进制兼容
- 添加非虚函数(包括构造函数)
- 添加新的类
- 添加Qt中的信号槽
- 在已存在的枚举类型中添加一个枚举值
- 添加新的静态成员变量
- 修改成员变量名称(偏移量未改变)
- 添加
Q_OBJECT
,Q_PROPERTY
,Q_ENUMS
,Q_FLAGS
宏,添加这些宏都是修改了moc生成的文件,而不是类本身
一种做法是预先分配若干个保留空间,当要添加项时,使用保留项。但是这种做法很呆板,因为你不知道未来到底会有多少扩展项,少了不满足要求,多了浪费空间。
QT引入D指针就是为了最大程度上实现二进制兼容,将类A中的成员变量放入Data 中,A中放入Data的一个指针,这样的话,无论你向Data中添加多少数据,A的对象模型中Data指针始终是4个字节的大小,这个Data指针就是QT中的d_ptr指针。
QT中的D指针
QObjectData {
public:
QObject *q_ptr;
...
};
class Q_CORE_EXPORT QObject
{
...
Q_DECLARE_PRIVATE(QObject)
public:
Q_INVOKABLE explicit QObject(QObject *parent=0);
virtual ~QObject();
...
protected:
QObject(QObjectPrivate &dd, QObject *parent = 0);
...
protected:
QScopedPointer<QObjectData> d_ptr;
...
};
先看下Q_DECLARE_PRIVATE(QObject) 这个宏
#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;
展开后
inline QObjectPrivate *d_func()
{
return reinterpret_cast<QObjectPrivate *>(qGetPtrHelper(d_ptr));
}
inline const QObjectPrivate *d_func() const
{
return reinterpret_cast<const QObjectPrivate *>(qGetPtrHelper(d_ptr));
}
friend class QObjectPrivate;
其中qGetPtrHelper函数定义(d_ptr可以是智能指针(例如QScopedPointer),在这种情况下它不能只传递给reinterpret_cast:d_func()必须使用成员函数访问内部指针,qGetPtrHelper所做的是触发智能指针的隐式转换,当作为参数传递给原始指针时,从而消除了对特殊处理的需要.)
template <typename T> static inline T *qGetPtrHelper(T *ptr)
{
return ptr;
}
d_ptr在QObject的构造函数中初始化
QObject::QObject(QObject *parent)
: d_ptr(new QObjectPrivate)
{
Q_D(QObject);
d_ptr->q_ptr = this;
...
}
QObject::QObject(QObjectPrivate &dd, QObject *parent)
: d_ptr(&dd)
{
Q_D(QObject);
d_ptr->q_ptr = this;
...
}
其中Q_D宏定义如下:
#define Q_D(Class) Class##Private * const d = d_func()
Q_D(QObject)定义为:QObjectPrivate * const d = d_func();
定义了一个QObjectPrivate的常量指针,指向d_func() 的返回值,而该返回值,正是d_ptr,因此同过Q_D宏我们就可以访问d指针了。
/* */
class Widget {
public:
Widget();
...
protected:
// 只有子类会访问以下构造函数
Widget(WidgetPrivate &d);// 允许子类通过它们自己的私有结构体来初始化
WidgetPrivate *d_ptr;
};
/* widget_p.h */
struct WidgetPrivate {
WidgetPrivate(Widget *q) : q_ptr(q) { }
Widget *q_ptr;
Rect geometry;
String stylesheet;
};
/* */
Widget::Widget()
: d_ptr(new WidgetPrivate(this)) {
}
Widget::Widget(WidgetPrivate &d)
: d_ptr(&d) {
}
/* */
class Label :public Widget {
public:
Label();
...
protected:
Label(LabelPrivate &d);// 允许Label的子类通过它们自己的私有结构体来初始化
// 注意Label在这已经不需要d_ptr指针,它用了其基类的d_ptr
};
/* */
#include "widget_p.h"
class LabelPrivate :public WidgetPrivate {
public:
String text;
};
Label::Label()
: Widget(*new LabelPrivate)//用其自身的私有结构体来初始化d指针
}
Label::Label(LabelPrivate &d)
: Widget(d) {
}
为了防止每产生一个Label对象,就会为相应的LabelPrivate和WidgetPrivate分配空间,当遇到像QListWidget(此类在继承结构上有6层深度),就会为相应的Private结构体分配6次空间。以上代码部分允许使用继承类的D指针来初始化基类D指针,比如当我们建立一个Label对象时,它就会建立相应的LabelPrivate结构体(其是WidgetPrivate的子类)。它将其d指针传递给Widget的保护构造函数。这时,建立一个Label对象仅需为其私有结构体申请一次内存。Label同样也有一个保护构造函数可以被继承Label的子类使用,以提供自己对应的私有结构体。
而前面一步优化导致的副作用是Label中使用的q-ptr和d-ptr分别是Widget和WidgetPrivate类型。这就意味着d_ptr->text = text的操作是不起作用的。这个时候正是reinterpret_cast<QObjectPrivate *>(qGetPtrHelper(d_ptr))这个函数发挥作用的时候了,reinterpret_cast运算符是用来处理无关类型之间的转换,它会产生一个新的值,这个值会有与原始参数有完全相同的比特位,常用在任意指针(或引用)类型之间的转换;以及指针与足够大的整数类型之间的转换;从整数类型(包括枚举类型)到指针类型,无视大小。
QT中的Q指针
QT中有D指针来存放成员变量了,很多时候ClassPrivate可能需要访问Class类中的成员或成员函数,比如LabelPrivate可能会有getLinkTargetFromPoint()(helper函数)以当按下鼠标时去找到相应的链接目标。在很多场合,这些helper函数需要访问公有类,例如访问一些属于Label类或是其基类Widget的函数。此时ClassPrivate中需要一个指针指向Class类,这个指针就是Q指针,比如WidgetPrivate中的Widget *q_ptr;
class Q_CORE_EXPORT QObjectPrivate : public QObjectData
{
Q_DECLARE_PUBLIC(QObject)
...
};
class Q_GUI_EXPORT QWidgetPrivate : public QObjectPrivate
{
Q_DECLARE_PUBLIC(QWidget)
...
};
看下Q_DECLARE_PUBLIC(QObject) 宏
#define Q_DECLARE_PUBLIC(Class) \
inline Class* q_func() { return static_cast<Class *>(q_ptr); } \
inline const Class* q_func() const { return static_cast<const Class *>(q_ptr); } \
friend class Class;
展开后
inline QObject *q_func()
{
return static_cast<QObject *>(q_ptr);
}
inline const QObject *q_func() const
{
return static_cast<const QObject *>(q_ptr);
}
friend class QObject;
注意这里的q_ptr是在QObjectData里公有声明的,QObjectPrivate,QWidgetPrivate都派生或间接派生自QObjectData,所以可以访问q_ptr。
使用Q_Q宏就可以使用q指针了
#define Q_Q(Class) Class * const q = q_func();