Qt源码分析之 D 指针和 Q 指针的用法
“阅读本文大概需求 8.8分钟
什么是 D 和 Q 指针
D
指针和Q
指针大量出现在 Qt
的源码中,你所看到的类似 Q_D
和 Q_Q
所代表的就是上述两个指针
Qt Wiki[1]
下面看一个简单的例子加深下理解,以人对象为例
Persion.h
class PersonPrivate;
class Person
{
public:
Person();
QString m_strName;
private:
PersonPrivate* d_ptr;
};
Persion.cpp
class PersonPrivate
{
public:
PersonPrivate(Person*q);
Person* q_ptr;
};
PersonPrivate::PersonPrivate(Person *q)
: q_ptr(q)
{
}
上述代码中 d_ptr
就是所谓的 D
指针,说白了就是数据成员指针,q_ptr
就是所谓的 Q
指针,怎么样很简单明了吧
为什么要用 D 和 Q 指针
搞清楚了什么是 D 和 Q 指针之后,现在再来看问下为什么要额外添加这些指针?
最主要的原因就是「为了二进制兼容性」,用新版本的库替换旧的库,现有的程序还能正常运行,满足这样的条件我们就称该库满足二进制兼容性
相反,如果不支持二进制兼容,那么当我们更新新的库后,程序在运行过程中可能会出现崩溃或者意想不到的错误
很多时候,当我们添加新的功能时,势必会导致该对象的模型发生改变,这就是导致二进制不兼容的罪魁祸首,那么怎么办呢?
我们把自己具体的实现用D
指针封装起来,这样就隐藏的所有的实现细节,比如我们把姓名
属性移植到 PersonPrivate
中来,这样就避免了对外暴露以及对象模型的变化了
这样修改后我们的 Persion
类就变成了下面这个样子
这样完善后,假设我们先阶段的程序库版本为 Persion1.0
,某一天来了特殊的需求,需要添加人员性别属性,我们依然照猫画虎这样修改
这次只需要修改我们的 PersonPrivate
类即可
class PersonPrivate
{
public:
PersonPrivate(Person*q);
Person* q_ptr;
QString m_strName;
QString m_strSex;
};
PersonPrivate::PersonPrivate(Person *q)
: q_ptr(q)
, m_strName("")
, m_strSex("")
{
}
在发布的时候只需要重新编译这个库即可,生成Persion1.1
版本,其它程序无需重新编译即可完美运行
D和Q指针进阶用法
在知道了为为什么要使用 D
和 Q
指针后,下面来看下一些常见的用法,看看如何在我们平时的项目中使用
下面的例子仍然在上一步的基础上进行扩展
我们现在有一个学生类,继承自 Persion
class Student: public Person
{
public:
Student();
};
这样继承后,Student
类也可以使用D
指针了,但是问题来了,我们知道,学生都有学号,我必须同样在StudentPrivate
中添加这个字段,类似下面这样
class StudentPrivate
{
public:
StudentPrivate(Person*q);
int m_nStuID;
};
StudentPrivate::StudentPrivate(Person*q)
: m_nStuID(0)
{
}
然后,是D
指针的初始化
class Student: public Person
{
public:
Student();
protected:
StudentPrivate* d_ptr;
};
class Student: public Person
{
public:
Student();
protected:
StudentPrivate* d_ptr;
};
这样就算完了,NO,NO,NO,这样的代码写完你仔细思考下就会发现有很多问题
-
每创建一个 Stuent
对象 会创建一个PersonPrivate
和StudentPrivate
对象,势必会多分配多余的空间,而且这仅仅是 2 层继承; -
D
指针中的信息没有继承过来;
好了,知道问题了,下面开始优化下,我们注意到 D
指针初始化是在构造函数初始化列表初始化的,因此,当派生类继续初始化时势必不会走派生类的 D
指针数据对象,那么就需要想办法让派生类实例化自己的 D
指针了
我们在每个类中添加一个新的构造函数
Persion.h
class Person
{
public:
Person();
protected:
Person(PersonPrivate & d);
PersonPrivate* d_ptr;
};
Persion.cpp
Person::Person()
: d_ptr(new PersonPrivate(this))
{
}
Person::Person(PersonPrivate &d)
:d_ptr(&d)
{
}
可以看到,我们通过添加保护级别的构造函数,这样派生类可以访问并且传递自己的 D
指针
下面来看看 Student
类的升级版本
Student.h
class Student: public Person
{
public:
Student();
protected:
Student(StudentPrivate& d);
};
Student.cpp
Student::Student()
: Person(*new StudentPrivate(this))
{
}
Student::Student(StudentPrivate &d)
: Person(d)
{
}
可以看到,Student
类在构造函数中初始化的时候调用的是基类的保护级别的构造函数,这样就直接能够访问到基类的d_ptr
指针,并且初始化成自己的StudentPrivate
对象
怎么样,这样是不是就避免了对象多次构造浪费空间以及多个对象继承时D
指针的初始化问题
对象调用
有了前面的基础,下面就可以直接调用对象的D
指针中的函数后者数据成员了,但是你会发现一个问题,如果我想在派生类中调用基类的怎么调用
Student::Student()
: Person(*new StudentPrivate(this))
{
d_ptr->m_nStuID;
}
这样是无法访问的,d_ptr
是基类的指针,你要调用派生类的变量,需要做类型转化
StudentPrivate* d = static_cast<StudentPrivate*>(d_ptr);
d->m_nStuID;
但是,你以为到了这里就完了么,当然不是,如果我们有很多方法都需要这样调用,那么你会看到满屏的static_cast
,作为程序员这个是不能容忍的,不能接受的,那么就出现了更优雅的解决办法
怎么办呢,Qt
采用的是宏定义,下面是简化版本
#define D_PTR(Class) Class##Private *d = static_cast<Class##Private *>(d_ptr)
#define Q_PTR(Class) Class *q = static_cast<Class *>(q_ptr)
解释一下上面宏定义表达的意思
我们知道 ##会进行前后连接,那么 Class##Private 最后表达的意思就是当前传入的对象名字+Private
有了宏定义后,我们上述实例代码就可以简化成这样
Student::Student()
: Person(*new StudentPrivate(this))
{
D_PTR(Student);
d->m_nStuID;
}
怎么样,是不是很方面,也很优雅,学习Qt
源码你会发现更多优雅的好东西
Qt 源码中是怎么实现的
前面都是我整理的简化版本,在 Qt
源码中稍微复杂一些,但是表达的意思和实际效果是一样的
template <typename T> static inline T *qGetPtrHelper(T *ptr) { return ptr; }
template <typename Wrapper> static inline typename Wrapper::pointer qGetPtrHelper(const Wrapper &p) { return p.data(); }
#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;
#define Q_DECLARE_PRIVATE_D(Dptr, Class) \
inline Class##Private* d_func() { return reinterpret_cast<Class##Private *>(Dptr); } \
inline const Class##Private* d_func() const { return reinterpret_cast<const Class##Private *>(Dptr); } \
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_D
和 Q_DECLARE_PUBLIC
总结
通过使用 D
和Q
指针避免了二进制兼容性问题,并且优雅的把我们代码的详细实现封装起来,避免对外暴露,尽可能的做到了保护
如果你还没有接触或者没有使用过,不妨可以尝试下,看看实际效果如何
参考资料
Qt Wiki: https://wiki.qt.io/D-Pointer/zh。
欢迎关注我的视频号:#视频号:devstone