vlambda博客
学习文章列表

Qt源码分析之 D 指针和 Q 指针的用法

阅读本文大概需求 8.8分钟

什么是 D 和 Q 指针

D指针和Q指针大量出现在 Qt的源码中,你所看到的类似 Q_DQ_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指针进阶用法

在知道了为为什么要使用 DQ 指针后,下面来看下一些常见的用法,看看如何在我们平时的项目中使用

下面的例子仍然在上一步的基础上进行扩展

我们现在有一个学生类,继承自 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对象 会创建一个 PersonPrivateStudentPrivate对象,势必会多分配多余的空间,而且这仅仅是 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_DQ_DECLARE_PUBLIC

总结

通过使用 DQ 指针避免了二进制兼容性问题,并且优雅的把我们代码的详细实现封装起来,避免对外暴露,尽可能的做到了保护

如果你还没有接触或者没有使用过,不妨可以尝试下,看看实际效果如何

参考资料

[1]

Qt Wiki: https://wiki.qt.io/D-Pointer/zh。



欢迎关注我的视频号:#视频号:devstone