在讲解d/q指针之前,我们先了解一下什么是Pimpl,这样更有助于我们理解Qt这么做的目的。
Pimpl(Pointer to Implementation)简介
该技术是一种减少代码依赖和编译时间的C++编程技巧,Pimpl的基本思想是将类的私有数据成员指针化,并将其移动到类的实现文件中。这样,在公共头文件中定义的类只包含指向私有实现的指针,而不是私有实现本身。这使得实现的细节可以在不更改类的公共接口的情况下进行更改。
Pimpl技术通过在类中使用指向实现类的指针来隐藏类的实现细节。在使用Pimpl技术时,开发人员需要在类的头文件中声明一个私有的指向实现类的指针,并在类的实现文件中定义实现类。通过这种方式,开发人员可以将实现细节与类的接口分离开来,从而提高了代码的可读性和可维护性。
以下是一个使用Pimpl技术的示例:
// MyClass.h
class MyClass
{
public:
MyClass();
~MyClass();
void doSomething();
private:
class Impl;
Impl* m_pImpl;
};
// MyClass.cpp
class MyClass::Impl
{
public:
void doSomethingImpl();
};
MyClass::MyClass() : m_pImpl(new Impl)
{}
MyClass::~MyClass()
{
delete m_pImpl;
}
void MyClass::doSomething()
{
m_pImpl->doSomethingImpl();
}
void MyClass::Impl::doSomethingImpl()
{
// Implementation details
}
在上面的示例中,MyClass类包含一个私有的Impl指针,指向一个名为Impl的内部实现类。实现类包含doSomethingImpl()方法的实现细节,而MyClass只暴露了doSomething()方法。在MyClass的构造函数中,我们分配了一个新的Impl对象并将其分配给m_pImpl指针。在MyClass的析构函数中,我们删除了m_pImpl指针指向的对象,以避免内存泄漏。
Pimpl技术的优缺点
优点:
1.隐藏实现细节:Pimpl技术使开发人员能够将实现细节与类的接口分离开来,从而降低耦合,提高代码的可读性和可维护性。
2.减少编译依赖性:Pimpl技术可以帮助减少类的头文件中的依赖项,从而加快编译时间并减少不必要的重新编译。(此项对于大型项目来说非常有用)
3.提高二进制兼容性:由于Pimpl技术将实现细节从类的接口中分离出来,因此可以在不破坏二进制兼容性的情况下修改类的实现。
缺点:
1.增加间接调用:由于Pimpl技术使用指针来引用私有实现,因此在访问私有实现时需要进行额外的间接调用,这可能会影响性能。
2.内存开销:Pimpl技术需要在堆上分配和管理额外的内存,这会导致一些开销。
Pimpl技术我们已经了解,现在我们开始看一下Qt的实现以及原理吧:
Qt源码中的d指针/q指针
下面我们将QObject的源码作为例子进行讲解:
qobject.h
// QObjectData
class Q_CORE_EXPORT QObjectData {
Q_DISABLE_COPY(QObjectData)
public:
QObjectData() = default;
virtual ~QObjectData() = 0;
QObject *q_ptr; // q指针
QObject *parent;
QObjectList children;
....
};
// QObject
class Q_CORE_EXPORT QObject
{
Q_OBJECT
Q_PROPERTY(QString objectName READ objectName WRITE setObjectName NOTIFY objectNameChanged)
Q_DECLARE_PRIVATE(QObject)
public:
Q_INVOKABLE explicit QObject(QObject *parent=nullptr);
virtual ~QObject();
virtual bool event(QEvent *event);
virtual bool eventFilter(QObject *watched, QEvent *event);
...
protected:
QScopedPointer<QObjectData> d_ptr; // d指针
...
};
qobject.cpp
QObject::QObject(QObject *parent)
: QObject(*new QObjectPrivate, parent)
{
}
/*!
\internal
*/
QObject::QObject(QObjectPrivate &dd, QObject *parent)
: d_ptr(&dd)
{
Q_ASSERT_X(this != parent, Q_FUNC_INFO, "Cannot parent a QObject to itself");
Q_D(QObject);
d_ptr->q_ptr = this;
auto threadData = (parent && !parent->thread()) ? parent->d_func()->threadData.loadRelaxed() : QThreadData::current();
threadData->ref();
d->threadData.storeRelaxed(threadData); // 此处d变量及是Q_D宏获取的d_ptr
...
我们再来看一下相关的宏定义:
qglobal.h
// The body must be a statement:
#define Q_CAST_IGNORE_ALIGN(body) QT_WARNING_PUSH QT_WARNING_DISABLE_GCC("-Wcast-align") body QT_WARNING_POP
#define Q_DECLARE_PRIVATE(Class) \
inline Class##Private* d_func() \
{ Q_CAST_IGNORE_ALIGN(return reinterpret_cast<Class##Private *>(qGetPtrHelper(d_ptr));) } \
inline const Class##Private* d_func() const \
{ Q_CAST_IGNORE_ALIGN(return reinterpret_cast<const Class##Private *>(qGetPtrHelper(d_ptr));) } \
friend class Class##Private;
#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;
#define Q_D(Class) Class##Private * const d = d_func()
#define Q_Q(Class) Class * const q = q_func()
Q_DECLARE_PRIVATE与Q_DECLARE_PUBLIC是Qt用来在类的头文件中声明获取d指针与q指针的私有函数,其核心在于添加了强制类型转换。
Q_D与Q_Q两个宏是用来获取d/q常量指针的,在函数中可以直接使用d变量或者q变量代替d_ptr与q_ptr,因为通过它们获取的指针类型是具体的,所以是直接使用ptr变量代替不了的。
我们再来看看qGetPtrHelper这个函数的定义:
qglobal.h
template <typename T> inline T *qGetPtrHelper(T *ptr) { return ptr; }
template <typename Ptr> inline auto qGetPtrHelper(Ptr &ptr) -> decltype(ptr.operator->()) { return ptr.operator->(); }
qGetPtrHelper是一个函数模板重载,用于获取指针。
Qt中d指针与q指针的存在价值
d指针:使用了Pimpl技术,因此Pimpl的优点都是它的价值所在,我认为Qt大量使用该技术主要是为了二进制兼容以及提高编译速度。
q指针:对父类或者公有类方法的访问。
最后,Qt的d指针与q指针就先讲这么多,如果以后有更好的见解,会不断添加。