一、PIMPL机制

PIMPL ,即Private Implementation,作用是,实现 私有化,力图使得头文件对改变不透明,以达到解耦的目的

pimpl 用法背后的思想是把客户与所有关于类的私有部分的知识隔离开。由于客户是依赖于类的头文件的,头文件中的任何变化都会影响客户,即使仅是对私有节或保护节的修改。pimpl用法隐藏了这些细节,方法是将私有数据和函数放入一个单独的类中,并保存在一个实现文件中,然后在头文件中对这个类进行前向声明并保存一个指向该实现类的指针。类的构造函数分配这个pimpl类,而析构函数则释放它。这样可以消除头文件与实现细节的相关性

该句出自 超越 C++ 标准库--boost程序库 导论

该文的代码说明均忽略一些简单必须的代码,以保证示例的简洁,比如防止头文件重复包含等

(1)实例说明

假设现在有一个需求,你需要写一个类,来完成产品的信息保存和获取,这个需求看起来非常的简单,我们只需要一分钟就能写好

Product.h

class Product
{
public:
string getName() const;
void setName(const string& name);
float getPrice() const;
void setPrice(float price);
private:
string name;
float price;
};

Product.cpp

string Product::getName() const
{
return this->name;
}
void Product::setName(const string &name)
{
this->name = name;
}
float Product::getPrice() const
{
return this->price;
}
void Product::setPrice(float price)
{
this->price = price;
}

当然,你可能会说,这个简单的代码根本不需要cpp,但是我们这里只是举个例子,实际的情况肯定比这复杂的多的多。

言归正传,我们完成了我们的模块,并交付出去提供给他人调用,结果第二天,有了新的需求,你需要新增一个成员变量,用作其中某个业务逻辑的数据存储,所以你不得不在头文件中的class内新增了一个成员属性,并在cpp中修改逻辑,辛运的是对外开放的接口并没有任何变动,调用你的模块的地方不需要修改代码。完成之后,交付使用。

然后这时候问题来了,调用此模块的人向你抱怨,替换了你的模块之后,明明自己没有修改任何东西,但是整个工程重新编译了整整半个多小时(可能有些夸张)。因为整个工程代码量巨大,很多地方都使用了你的模块,包含了你的头文件,导致这些包含你的头文件的地方虽然没有变动,但是都重新编译了。

利用PIMPL机制,将私有成员隐藏起来,使得只有接口不变,那么头文件就不会改变,已达到解耦的目的。从上面例子也可以看出,PIMPL机制的好处之一就是避免头文件依赖,提高编译速度。

那利用PIMPL机制,上面的问题如何解决呢?

(2)利用PIMPL机制

基于原来的需求,代码设计如下:

Product.h

class ProductData;
class Product
{
public:
Product();
~Product(); string getName() const;
void setName(const string& name);
float getPrice() const;
void setPrice(float price); private:
ProductData* data;
};

Product.cpp

class ProductData
{
public:
string name;
float price;
}; Product::Product()
{
data = new ProductData();
}
Product::~Product()
{
delete data;
}
string Product::getName() const
{
return data->name;
}
void Product::setName(const string &name)
{
data->name = name;
}
float Product::getPrice() const
{
return data->price;
}
void Product::setPrice(float price)
{
data->price = price;
}

可以看出来,Product 类除了必要的接口函数外,就只有一个ProductData指针了,而ProductData又是使用的前置声明,在cpp中实现,这样,只要接口不变,那么内部私有成员或者逻辑改变,并不会影响client

上面的代码只是最简单的实现,其中还存在很多问题,而实际的项目中可能要复杂的多。尽管如此,我们也能看出PIMPL机制的优点:

  • 降低耦合度;
  • 隐藏模块信息;
  • 降低编译依赖,提高编译速度;
  • 接口和实现真正分离;

二、Qt源码中的d指针

了解PIMPL机制之后,我们可以看看优秀的C++库中是如何实现PIMPL机制的,以Qt框架为例。读过Qt源码的同学对Qt中的d指针想必不会陌生,我们来详细讲解一下

(1)QThread中的PIMPL机制

我们随便选取一个Qt中的模块,以QThread为例分析一下Qt中是如何实现PIMPL机制的

首先,找到QThread类的头文件 qthread.h,我们可以看到,QThread 类的什么中,除了对外的接口外,根本看不到能够猜测内部实现方法或者变量,而且其private的成员只有下面几个

class Q_CORE_EXPORT QThread : public QObject
{
...
private:
Q_DECLARE_PRIVATE(QThread) friend class QCoreApplication;
friend class QThreadData;
};

那么,QThread内部使用的方法和属性都去哪里了呢

我们先找到它的构造函数,实现如下:

QThread::QThread(QObject *parent)
: QObject(*(new QThreadPrivate), parent)
{
Q_D(QThread);
d->data->thread = this;
}

(2)Q_D宏

Q_D() 是Qt中的一个宏定义

#define Q_D(Class) Class##Private * const d = d_func()
#define Q_Q(Class) Class * const q = q_func()

Q_D(QThread); 展开如下:

QThreadPrivate * const d = d_func();

这也是上面代码中的 d 指针的由来,可以看到,d其实是一个QThreadPrivate指针,const标在d前面,类型后面,表示d指针的的指向不能改变,这点不懂的需要去复习一下const的用法,同理,q是QThread指针,且指向不能改变,所以,代码中出现下面的宏将会得到传入对象的指针

Q_D(QThread);		//QThread*
Q_D(const QThread); //const QThread*

(3)d_func()

这里有一个方法 d_func(),我们可以查看到它的声明

#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;

上面这段代码会生成方法 d_func(),而在QThread 头文件类声明中,可以看到此宏

class Q_CORE_EXPORT QThread : public QObject
{
...
private:
Q_DECLARE_PRIVATE(QThread)
...
};

(4)Q_DECLARE_PRIVATE宏

Q_DECLARE_PRIVATE(QThread) 宏展开如下:

class Q_CORE_EXPORT QThread : public QObject
{
...
private:
inline QThreadPrivate* d_func(){
return reinterpret_cast<QThreadPrivate*>(qGetPtrHelper(d_ptr));
}
inline const QThreadPrivate* d_func() const {
return reinterpret_cast<QThreadPrivate*>(qGetPtrHelper(d_ptr));
}
friend class QThreadPrivate;
};

可以看出,这个宏其实是在QThread内部定义了两个 inline 方法和一个友元类,d_func() 方法也来源于此

(5)qGetPtrHelper()方法

这里的 qGetPtrHelper() 方法可以找到,我给它重新排版一下,如下:

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();
}

这是一个模板方法,如果只是一个普通的类指针,则返回该指针;

而如果是一个类模板方法,则调用data()方法,并返回结果

所以,我们看到上面的 d_func() 方法中的,替换了 qGetPtrHelper() 方法后,如下:

class Q_CORE_EXPORT QThread : public QObject
{
...
inline QThreadPrivate* d_func(){
return reinterpret_cast<QThreadPrivate*>(d_ptr.data());
}
inline const QThreadPrivate* d_func() const {
return reinterpret_cast<QThreadPrivate*>(d_ptr.data());
}
};

(6)d_ptr

那么,这里的 d_ptr 又是哪里来的呢,它其实是在 QObject 对象内定义的

class Q_CORE_EXPORT QObject
{
...
protected:
QScopedPointer<QObjectData> d_ptr;
};

我们都知道,Qt 中所有对象都是继承自 QObject 的,所以 QThread 是可以使用 d_ptr 的

QScopedPointer 是Qt中封装的智能指针,相当于stl中的std::unique_ptr,所以,上面代码中的 d_ptr.data() 作用是获取智能指针管理的指针,等同于std::unique_ptr中的 get() 方法,也就是这里的 QObjectData 指针。

所以,d_func() 方法的作用是,获取 QThread 中继承的 QObject 中的 QObjectData 指针,并使用强制类型转换为 QThreadPrivate 指针类型。而为什么能转换,因为他们之间是有继承关系的 QThreadPrivate -> QObjectPrivate -> QObjectData

上面说的所有东西最终都是在分析 Q_D(QThread);,现在我们知道,这句宏定义最后会得到 QThreadPrivate 指针,而这个类的作用就是我们之前讲的 PIMPL机制中的用作保存 QThread 类私有成员,以达到解耦的目的

三、使用d指针

学完了Qt巧妙的d指针,我们在第一节中的代码中照葫芦画瓢引入d指针,最终代码如下:

global.h

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.get(); } #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()
#define Q_Q(Class) Class * const q = q_func()

base.h

class BaseData
{
public:
BaseData(){}
virtual ~BaseData(){}
}; class BasePrivate : public BaseData
{
public:
BasePrivate(){}
virtual ~BasePrivate() {}
};

product.h

#include "global.h"
#include "test_p.h"
#include <memory>
#include <string> using namespace std; class ProductPrivate;
class ProductData; class Product
{
public:
explicit Product(int num = 1);
~Product();
string getName() const;
void setName(const string& name);
float getPrice() const;
void setPrice(float price);
protected:
Product(ProductPrivate* testPrivate, int num = 1);
protected:
std::unique_ptr<BaseData> d_ptr;
private:
Q_DECLARE_PRIVATE(Product)
friend class ProductData;
};

product.cpp

#include "product.h"
#include <iostream>
using namespace std; class ProductData : public BaseData
{
public:
Product* test;
}; class ProductPrivate : public BasePrivate
{
public:
ProductPrivate(ProductData* d = 0) : data(d) {
if(!data) {
data = new ProductData();
}
}
ProductData* data; int number;
string name;
float price;
}; Product::Product(ProductPrivate *testPrivate, int num) : d_ptr(testPrivate)
{
Q_D(Product);
d->data->test = this;
} Product::Product(int num)
{
d_ptr = std::unique_ptr<ProductPrivate>(new ProductPrivate());
} string Product::getName() const
{
Q_D(const Product);
return d->name;
} void Product::setName(const string &name)
{
Q_D(Product);
d->name = name;
} float Product::getPrice() const
{
Q_D(const Product);
return d->price;
} void Product::setPrice(float price)
{
Q_D(Product);
d->price = price;
}

上面的代码其实并不是如何巧妙,里面加了一些东西,可以方便其他模块拓展,这里只是作为一个总结和参考

Qt源码解析之-从PIMPL机制到d指针的更多相关文章

  1. QT源码解析(一) QT创建窗口程序、消息循环和WinMain函数

    QT源码解析(一) QT创建窗口程序.消息循环和WinMain函数 分类: QT2009-10-28 13:33 17695人阅读 评论(13) 收藏 举报 qtapplicationwindowse ...

  2. QT源码解析(七)Qt创建窗体的过程,作者“ tingsking18 ”(真正的创建QPushButton是在show()方法中,show()方法又调用了setVisible方法)

    前言:分析Qt的代码也有一段时间了,以前在进行QT源码解析的时候总是使用ue,一个函数名在QTDIR/src目录下反复的查找,然后分析函数之间的调用关系,效率实在是太低了,最近总结出一个更简便的方法, ...

  3. Android -- 从源码解析Handle+Looper+MessageQueue机制

    1,今天和大家一起从底层看看Handle的工作机制是什么样的,那么在引入之前我们先来了解Handle是用来干什么的 handler通俗一点讲就是用来在各个线程之间发送数据的处理对象.在任何线程中,只要 ...

  4. JDK源码解析之Java SPI机制

    1. spi 是什么 SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件. 系统设计的各个抽象,往往 ...

  5. grunt源码解析:整体运行机制&grunt-cli源码解析

    前端的童鞋对grunt应该不陌生,前面也陆陆续续的写了几篇grunt入门的文章.本篇文章会更进一步,对grunt的源码进行分析.文章大体内容内容如下: grunt整体设计概览 grunt-cli源码分 ...

  6. 【Dubbo 源码解析】07_Dubbo 重试机制

    Dubbo 重试机制 通过前面 Dubbo 服务发现&引用 的分析,我们知道,Dubbo 的重试机制是通过 com.alibaba.dubbo.rpc.cluster.support.Fail ...

  7. QT源码解析笔记

    1. QT如何绘制控件的 QT的绘制控件在QStyleSheetStyle::DrawControl里面,这里会调用默认的QSS来绘制效果 2. 在设置一次QSS以后,将会触发polish事件,里面将 ...

  8. QT源码之Qt信号槽机制与事件机制的联系

    QT源码之Qt信号槽机制与事件机制的联系是本文要介绍的内容,通过解决一个问题,从中分析出的理论,先来看内容. 本文就是来解决一个问题,就是当signal和slot的连接为Qt::QueuedConne ...

  9. Android -- AsyncTask源码解析

    1,前段时间换工作的时候,关于AsyncTask源码这个点基本上大一点的公司都会问,所以今天就和大家一起来总结总结.本来早就想写这篇文章的,当时写<Android -- 从源码解析Handle+ ...

随机推荐

  1. Delphi Unicode转中文

    function UniCode2GB(S : String):String;Var I: Integer;beginI := Length(S);while I >=4 do begintry ...

  2. Python 3.9 性能优化:更快的 list()、dict() 和 range() 等内置类型

    Python 的 3.9.0 版本正在开发中,计划在 2020-10-05 发布 final 版本. 官方在 changelog 中披露了很多细节,其中有一项"vectorcall" ...

  3. vue2.x学习笔记(十二)

    接着前面的内容:https://www.cnblogs.com/yanggb/p/12592256.html. 组件基础 组件化是vue的一个重要特性,也是vue学习中非常重要的一个知识点. 基础示例 ...

  4. javascript实例教程使用canvas技术模仿echarts柱状图

    canvas 画布是HTML5中新增的标签,可以通过js操作 canvas 绘图 API在网页中绘制图像. 百度开发了一个开源的可视化图表库ECharts,功能非常强大,可以实现折线图.柱状图.散点图 ...

  5. ES[7.6.x]学习笔记(三)新建索引

    与ES的交互方式 与es的交互方式采用http的请求方式,请求的格式如下: curl -X<VERB> '<PROTOCOL>://<HOST>:<PORT& ...

  6. 对JavaScript中原型及原型链的理解

    什么是原型:  1,我们所创建的每一个函数,解析器都会向该函数对象添加一个属性prototype,这个属性指向一个对象,这个对象就是我们所谓的原型对象 2,如果我们将函数作为普通函数调用时,proto ...

  7. .NetCore对接各大财务软件凭证API——金蝶系列(1)

    哈喽,又和大家见面了,虽然看文章的小伙伴不多,但是我相信总有一天,自己写的这些文章或多或少会对其他人有些帮助,让他们在相关的业务开发下能少走些弯路,那我的目的就达到了,好了,今天就正式开始我们的系列了 ...

  8. ApiPost的预执行脚本和后执行脚本

    ApiPost的预执行脚本和后执行脚本主要是用来定义变量.但是它们有什么区别呢? 预执行脚本 在当前接口发送请求前执行的脚本,可以理解为beforeSend的时候执行. 一般在这里,我们可以设置一些前 ...

  9. cocos2dx新建项目

    首先你得下载好cococs2dx,还有python2.x版本,还有vs2017 然后cmd在你Cocos2dx的路径下输入 python setup.py 然后你就回车回车回车 然后重新打开cmd 这 ...

  10. 【JAVA基础】04 Java语言基础:方法

    1. 方法概述和格式说明 为什么要有方法 提高代码的复用性 什么是方法 完成特定功能的代码块. 方法的格式 修饰符 返回值类型 方法名(参数类型 参数名1,参数类型 参数名2...) {     方法 ...