PImpl机制以及Qt的D-Pointer实现

本文深入解析了C++中的PImpl(PointertoImplementation)技术及其在Qt库中的实现——D-Pointer。PImpl机制通过将类的实现细节封装在独立的实现类中,达到数据隐藏、缩短编译时间和保持ABI稳定等优点。文章详细介绍了Qt中如何使用D-Pointer,包括宏定义的使用和优化版本的实现,展示了如何通过继承QObjectPrivate来减少每个子类的实现负担。

PImpl是 Pointer to implementation的缩写, 是C++ 在构建导出库接口时特有的技术手段。 即是将类Class中所有私有变量以及私有方法,封装在一单独的实现类ClassImpl中。我们在Class中通过一指向ClassImpl的私有指针,访问这些私有数据。而ClassImpl类的具体定义和实现,我们放入cpp中。Qt中的D-Pointer技术,便是PImpl机制的一种实现方式。
优点:

使得程序接口有着稳定的ABI(应用程序二进制接口),即不会打破二进制兼容。
降低了程序编译依赖,从而缩短编译时间。
数据隐藏,使得头文件很干净,不包含实现细节,可以直接作为 API 参考。

缺点:

实现者需要做更多工作。
由于子类需要访问,此机制对protected方法不奏效。
由于数据的隐藏,多少造成了代码可读性下降。
运行时性能被轻微的连累,尤其调用的函数为虚函数时。

关于二进制兼容的问题,在C++的二进制兼容一文中有详细解释,这里不再赘述。下面我们通过介绍Qt的D-Pointer的实现细节,来理解PImpl机制。

Qt中D-Pointer的实现
Person.h

class PersonPrivate;
class Person
{
    Q_DECLARE_PRIVATE(Person)
public:
    Person();
    ~Person();
    QString name() const;
    void setName(const QString &name);
signals:
    void calcRequested();
private:
    QScopedPointer<PersonPrivate> d_ptr;
};

Person_p.h

#include "Person.h"
class PersonPrivate
{
    Q_DECLARE_PUBLIC(Person)
public:
    PersonPrivate(Person *parent);
    
    void calc();
    
    QString name;
private:
    Person * const q_ptr;
};

Person.cpp

#include "Person_p.h"
Person::Person() : d_ptr(new PersonPrivate(this))
{
}
~Person() {}
QString Person::name() const
{
    Q_D(const Person);
    return d->name;
}
void Person::setName(const QString &name)
{
    Q_D(const Person);
    d->name = name;
}
PersonPrivate::PersonPrivate(Person *parent) : q_ptr(parent)
{
  
}
void PersonPrivate::calc()
{
    Q_Q(Person);
    emit q->calcRequested();
}

相关宏定义及作用

以上所有用到的宏定义,均放在qglobal.h中。下面我们一一介绍。
Q_DECLARE_PRIVATE、Q_D

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_D(Class) Class##Private * const d = d_func()

Q_DECLARE_PRIVATE看似复杂,其实就是封装了d_func()函数,目的就是让我们在多种不同情况下,可以方便地拿到并使用私有类指针d_ptr 。Q_D宏对d_func()进行再次封装,让我们可以免去每次定义的繁琐,直接使用d指针,此指针即为我们想要的d_ptr。下面我们进行更详细的理解以及相关注意事项:

利用d_func()函数,可以避免我们每次直接拿d_ptr指针进行类型转换(因为我们有可能会在子类中使用此方法,具体我们将在后面的拓展中详述)。
在d_func()中,我们为什么不直接使用d_ptr ,而要借助qGetPtrHelper()函数呢?利用此函数,是为了适配我们使用智能指针的情况,因为此时我们要拿到真正的指针,需要调用d_ptr.data()。
在const函数中使用Q_D,此时将调用d_func()的const版本,我们必须要利用Q_D(const Person)这种写法拿到正确的const指针(否则会提示无法转换)。这样封装也间接保证了程序的正确性,而不是直接拿到d_ptr指针进行操作 。
d_ptr的定义是要放在暴露给用户的头文件中,如此命名有时会打破我们的命名规范,此时可以利用Q_DECLARE_PRIVATE_D(m_dPtr, Person)这个宏来进行自定义的命名。看到这个宏,我们不得不感慨Qt封装的细致得当。
既然上面提到了使用智能指针,这里多说几句,我们利用前置声明的方式来使用QScopedPointer时,我们必须要有非内联的构造、析构、赋值运算符。即不可以用默认生成的。具体可参见QScopedPointer文档中的Forward Declared Pointers部分。

Q_DECLARE_PUBLIC、Q_Q

#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_Q(Class) Class * const q = q_func()

同理,我们在私有类中,有时候需要调用主类的方法,这两个宏的作用就是为了可以在私有类中拿到主类的指针。我们在私有类的构造函数中传入主类指针,并赋值给q_ptr。因为这里是拿到主类的指针,并不存在智能指针的问题,所以此处并没有借助qGetPtrHelper()函数。
拓展

有了上面的讲解,我们到这里可以思考一个问题,假如我们的类有很多的子类,那么我们岂不是每一个子类都需要定义一个d_ptr。每一个private类都需要有一个q_ptr的指针么?Qt中当然不会如此实现,所以就有了下面的优化版本。

首先我们在基类QObject中将d_ptr变为protected类型,并在基类中添加一protected类型的构造函数,供子类使用。

class QObject
{
protected:
    QObject(QObjectPrivate &dd, QObject *parent = 0); 
    QScopedPointer<QObjectData> d_ptr;
    ...
};

所有的私有类均继承于QObjectPrivate,

class QWidgetPrivate : public QObjectPrivate
{
    Q_OBJECT
    Q_DECLARE_PUBLIC(QWidget)
    ...
};

下面我们在看看QWidget和QObject的构造函数:

QWidget::QWidget(QWidget *parent, Qt::WindowFlags f)     
        : QObject(*new QWidgetPrivate, 0), QPaintDevice()  
{ 
    ... 
}
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;
};

到这里,总算真相大白,QWidget中并没有出现d_ptr指针,原来是从Qbject继承而来。QObject中我们新添加的那个protected构造函数传入一个QWidgetPrivate,用此给QObject中的d_ptr赋值,而这便是我们唯一的d_ptr。现在总算真正理解之前d_func()中那些类型转换的作用,就是保证我们可以拿到当前正确类型的private指针。

那么同理,ObjectPrivate是继承于QObjectData,而在QObjectData中有着QObject *q_ptr;。 所有QObject子类的私有类,均继承于ObjectPrivate,故而子类中也不会出现q_ptr,在QObject的构造函数中,我们把this指针给其赋值,在通过使用Q_Q宏,我们同样可以拿到正确类型的主类q指针。
总结

我们完全可以不借助Qt这些宏来实现PImpl,其实只需要构建private类,将其放入cpp中,就已经实现了PImpl。不过利用这些宏,可以简单的实现出Qt风格的数据隐藏,我们可以利用上面Person类的简化版实现,当然假如我们的类需要被继承,我们也可以参考拓展中的方式,利用继承ObjectPrivate类的方式实现,不过需要注意,想要使用此类,我们需要在pro中添加QT += core-private。

参考:D-Point
https://wiki.qt.io/D-Pointer/zh

### d-pointer 模式在 C++ Qt 中的设计与实现 d-pointer 模式是 Qt 框架中用于隐藏类的实现细节的一种设计模式,通常被称为 **PimplPointer to Implementation)** 惯用法。该模式通过将类的具体实现隔离到一个私有类中,并在公共接口类中仅暴露一个指向该私有类的指针,从而实现了更好的封装性和模块解耦。 #### 核心作用 - **隐藏实现细节**:Qt 的头文件中不直接包含具体实现代码,而是通过一个指向私有实现类的指针(即 `d_ptr`)来访问内部数据和方法,避免了暴露过多的实现信息。 - **减少编译依赖**:由于实现部分被移至私有类中,修改私有类的内容不会影响使用其公共接口的代码,从而减少了重新编译的需求。 - **维护二进制兼容性(ABI)**:在库版本更新时,只要公共接口类的大小和布局未改变,就可以保证不同版本之间的二进制兼容性,这对于动态链接库尤为重要[^1]。 #### 实现原理 d-pointer 模式的核心在于引入了一个私有类(通常命名为 `ClassNamePrivate`),并将其定义放在源文件(.cpp 文件)中,而非头文件(.h 文件)。在头文件中仅声明一个指向该私有类的指针(通常命名为 `d_ptr`)[^2]。 例如,在 Qt 中一个类可能如下定义: ```cpp // MyClass.h class MyClass { public: MyClass(); ~MyClass(); void doSomething(); private: Q_DECLARE_PRIVATE(MyClass) // 声明 d-pointer MyClassPrivate* const d_ptr; }; ``` 而在对应的源文件中,则会定义私有类并实现相关功能: ```cpp // MyClass_p.h (私有类头文件) class MyClassPrivate { public: void internalMethod(); }; // MyClass.cpp #include "MyClass_p.h" MyClass::MyClass() : d_ptr(new MyClassPrivate()) {} MyClass::~MyClass() = default; void MyClass::doSomething() { Q_D(MyClass); // 获取 d-pointer d->internalMethod(); } ``` #### 与 q-pointer 的关系 与 d-pointer 配套使用的还有 q-pointer(即 `q_ptr`),它允许私有实现类反向访问其对应的公共类实例。这种双向关联使得私有类能够调用公共类的方法或访问其成员变量,从而实现更加灵活的交互机制[^3]。 例如,在私有类中可以这样获取公共类实例: ```cpp // MyClassPrivate.cpp void MyClassPrivate::internalMethod() { Q_Q(MyClass); // 获取 q-pointer q->somePublicMethod(); // 调用公共类方法 } ``` #### 应用场景与优势 - **大型项目开发**:在需要频繁更新实现细节但保持接口稳定的项目中,d-pointer 可以显著降低重构成本。 - **库开发**:对于提供给第三方使用的库,d-pointer 可确保库版本升级时不需要用户重新编译其代码。 - **提高封装性**:通过将实现完全隐藏,增强了类的封装性,使对外暴露的接口更简洁清晰。 #### 性能考量 虽然 d-pointer 模式带来了诸多好处,但也需要注意一些潜在的性能开销: - **内存分配**:每次创建对象时都需要动态分配私有类的实例。 - **间接访问**:每次访问私有成员都需要通过指针进行一次解引用操作,这可能会略微影响性能。 然而,在大多数实际应用中,这些开销是可以接受的,尤其是在对可维护性和 ABI 稳定性有较高要求的情况下。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值