C++之Pimpl惯用法

面向对象设计原则

接口隔离原则:面向对象设计之接口隔离原则-CSDN博客

设计模式

工厂模式 : 设计模式之工厂模式-CSDN博客

迭代器模式:设计模式之迭代器模式-CSDN博客

适配器模式:设计模式之适配器模式-CSDN博客

过滤器模式:设计模式之过滤器模式-CSDN博客

观察者模式:设计模式之观察者模式-CSDN博客

空对象模式:设计模式之空对象模式-CSDN博客

责任链模式:设计模式之责任链模式-CSDN博客

策略模式:设计模式之策略模式-CSDN博客

Pimpl技法:C++之Pimpl惯用法-CSDN博客

桥接模式设计模式之桥接模式-CSDN博客

组合模式:设计模式之组合模式-CSDN博客

单例模式:设计模式之单例模式-CSDN博客

目录

1.简介

2.案例分析

3.用法

4.优点


1.简介

Pimpl即“pointer to implementation”(指向实现的指针)。该技巧可以避免在头文件中暴露私有细节,是促进API接口和实现保持完全分离的重要机制, 从而减少编译依赖和提高编译速度。

如下图为impl常见的内存布局,class A只提供公有接口func1, func2,其实现细节由Impl类实现,class A通过一格Impl 指针impl来提供服务。这样做的目的在于,使用class A公有接口的用户,不必关系其实现细节,而且实现的变动,对用户也是透明的。

C++之Pimpl惯用法_第1张图片

Impl类成员函数privateFunc1、privateFunc2、privateFunc3和成员变量privateData1、privateData2都声明为private, 在头部声明friend class A既保证了class A的顺利访问Impl的所有接口和私有数据,又对外隐藏了所有的接口和私有数据,起到了很好的保护作用。

2.案例分析

        在Qt中最基础的类就是QObject, QObject是Qt对象模型的核心,所以QObject类的设计至关重要。下面看一下在Qt5.12.12源码中QObject的设计,以QWidget为例介绍,我整理了几个类之间的关系,类图如下:

C++之Pimpl惯用法_第2张图片

QObject如果说是上图的class A,那么QObjectData就是Impl。QObject和QObjectData之间的纽带就是d指针,QObjectData和QObject之间的纽带是q指针,d指针和q指针的定义如下:

template  inline T *qGetPtrHelper(T *ptr) { return ptr; }
template  inline auto qGetPtrHelper(const Ptr &ptr) -> decltype(ptr.operator->()) { return ptr.operator->(); }

// 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(qGetPtrHelper(d_ptr));) } \
    inline const Class##Private* d_func() const \
    { Q_CAST_IGNORE_ALIGN(return reinterpret_cast(qGetPtrHelper(d_ptr));) } \
    friend class Class##Private;

#define Q_DECLARE_PRIVATE_D(Dptr, Class) \
    inline Class##Private* d_func() \
    { Q_CAST_IGNORE_ALIGN(return reinterpret_cast(qGetPtrHelper(Dptr));) } \
    inline const Class##Private* d_func() const \
    { Q_CAST_IGNORE_ALIGN(return reinterpret_cast(qGetPtrHelper(Dptr));) } \
    friend class Class##Private;

#define Q_DECLARE_PUBLIC(Class)                                    \
    inline Class* q_func() { return static_cast(q_ptr); } \
    inline const Class* q_func() const { return static_cast(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 在 QObject 类中使用了,下面的代码可以看出:

qobject.h

class Q_CORE_EXPORT QObject
{
    Q_OBJECT
    Q_PROPERTY(QString objectName READ objectName WRITE setObjectName NOTIFY objectNameChanged)
    Q_DECLARE_PRIVATE(QObject)

public:
    ...

protected:
    QObject(QObjectPrivate &dd, QObject *parent = nullptr);

protected:
    QScopedPointer d_ptr;
    ...
};
    

在QObject中一般直接使用d_ptr指针,但是在它的派生类QWidget中一般使用Q_D宏来访问QWidgetPrivate里面的数据,从QWidget的成员函数里面随处可以看使用Q_D的,比如:

QSizePolicy QWidget::sizePolicy() const
{
    Q_D(const QWidget);
    return d->size_policy;
}

QObjectData的定义如下:

class Q_CORE_EXPORT QObjectData {
public:
    virtual ~QObjectData() = 0;
    QObject *q_ptr;
    QObject *parent;
    QObjectList children;

    uint isWidget : 1;
    uint blockSig : 1;
    uint wasDeleted : 1;
    uint isDeletingChildren : 1;
    uint sendChildEvents : 1;
    uint receiveChildEvents : 1;
    uint isWindow : 1; //for QWindow
    uint deleteLaterCalled : 1;
    uint unused : 24;
    int postedEvents;
    QDynamicMetaObjectData *metaObject;
    QMetaObject *dynamicMetaObject() const;
};

QWidgetPrivate使用Q_DECLARE_PUBLIC,从下面的代码可以看出:

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

在QObjectPrivate中一般直接使用q_ptr指针,但是在它的派生类QWidgetPrivate中一般使用Q_Q宏来访QWidget里面的成员函数,从QWidgetPrivate的成员函数里面随处可以看使用Q_Q的,比如:

void QWidgetPrivate::create()
{
    Q_Q(QWidget);
    ...
}

总结,通过上面的分析,d指针和q指针的关系很清晰了;Qt里面的类体系设计都是Pimpl惯用法的践行者。

3.用法

使用裸指针指向Impl,既不安全,又怕程序退出时候忘记delete它,导致内存泄漏;所有此处可以借助智能指针(smart pointer)解决该问题,具体来说,可采用std::shared_ptr(共享指针),或std::unique_ptr(域指针)指向Impl类对象。

下面我工作过程中写一个部分具体实现:

CommunicationDataTask.h


class CCommunicationDataTask : public CLinkTaskAdapter, public IThread
{
public:
    explicit CCommunicationDataTask(IWaveProtocolBasePacketByS* pProtocol,
                                    CSocketBase* pComm,
                                    const QString& dataFile,
                                    int channel,
                                    quint32 signalType,
                                    quint64 packetSizeOnce = 32*1024*1024, //每包发送大小, 默认32M
                                    ILinkDataSimpleProgressNotify* pNotify = nullptr);
    virtual ~CCommunicationDataTask();

public:
    int start() override;
    int stop() override;
    int getType() const override {return eTransFileTask;}
    int getState() const override;
    void update(const void* param = nullptr) override;
    
    ...

private:
    class CCommunicationDataTaskImpl;
    std::unique_ptr d_ptr;
};

CommunicationDataTask.cpp

class CCommunicationDataTaskImpl
{
    friend class CCommunicationDataTask;
public:
    explicit CCommunicationDataTaskImpl(IWaveProtocolBasePacketByS* pProtocol,
                                        CSocketBase* pComm,
                                        const QString& dataFile,
                                        int channel,
                                        quint32 signalType,
                                        quint64 packetSizeOnce,
                                        ILinkDataSimpleProgressNotify* pNotify)
        : m_pProtocol(pProtocol)
        , m_pSocket(pComm)
        , m_file(dataFile)
        , m_channel(channel)
        , m_signalType(signalType)
        , m_packetSizeOnce(packetSizeOnce)
        , m_pNotify(pNotify)
        , m_states(eIdle)
        , m_sendedPos(0)
        , m_bWait(false)
        , m_bStop(false)
    {
    }

private:
    IWaveProtocolBasePacketByS* m_pProtocol;
    CSocketBase* m_pSocket;
    QString      m_file;
    const int  m_channel;
    const quint32  m_signalType;
    ILinkDataSimpleProgressNotify* m_pNotify;
    int m_states;
    QByteArray  m_data;
    int         m_sendedPos;
    std::atomic  m_bWait;
    bool        m_bStop;
    const quint64     m_packetSizeOnce;
};

CCommunicationDataTask::CCommunicationDataTask(IWaveProtocolBasePacketByS* pProtocol,
                                               CSocketBase* pComm,
                                               const QString& dataFile,
                                               int channel,
                                               quint32 signalType,
                                               quint64 packetSizeOnce,
                                               ILinkDataSimpleProgressNotify* pNotify)
    : d_ptr(new CCommunicationDataTaskImpl(pProtocol, pComm,dataFile, channel, signalType, packetSizeOnce, pNotify))
{

}

CCommunicationDataTask::~CCommunicationDataTask()
{
    stop();
}

int CCommunicationDataTask::start()
{
    if (d_ptr->m_states == eRunning){
        return 1;
    }

    IThread::start();

    d_ptr->m_states = eRunning;

    return 1;
}
int CCommunicationDataTask::stop()
{
    if (d_ptr->m_states == eStopped ||
        d_ptr->m_states == eFinished){
        return 1;
    }

    qDebug() << "CCommunicationDataTask::stop, run into, time: " << QDateTime::currentDateTime().toString("hh:mm:ss.zzz");
    d_ptr->m_bStop = true;
    join();
    d_ptr->m_states = eStopped;
    qDebug() << "CCommunicationDataTask::stop, after, time:" << QDateTime::currentDateTime().toString("hh:mm:ss.zzz");
    return 1;
}
int CCommunicationDataTask::getState() const
{
    return d_ptr->m_states;
}
void CCommunicationDataTask::update(const void* param)
{
    assert(param != nullptr);
    bool bEnable = *static_cast(param);
    d_ptr->m_bWait.store(bEnable);
}

...

这里的d_ptr类似Qt中的d指针,通过仅存储单个指针来使库的所有公共类的大小保持恒定。该指针指向包含所有数据的私有/内部数据结构。内部结构的大小可以缩小或增长,而对应用程序没有任何副作用,因为仅在库代码中访问指针,并且从应用程序的角度来看,对象的大小从不改变-始终是对象的大小。

4.优点

  • 二进制兼容性
  • 数据隐藏,保护核心数据和实现原理
  • 接口和实现分离,降低耦合
  • 降低编译依赖,减少编译时间

你可能感兴趣的:(#编程技巧,开发语言,c++)