C 惯用法之 PIMPL
创始人
2025-07-12 05:51:29
0

一、背景

1.概述

PIMPL 是 C++ 中的一个编程技巧,意思为指向实现的指针。具体操作是把类的实现细节放到一个单独的类中,并用一个指针进行访问。

2.二进制兼容性

(1) 概述

二进制兼容是指当库文件升级后所有使用该库的应用程序不必重新编译,其本质就是类的内存布局不变。使用 pimpl 方法设计类可以实现二进制兼容的目的。

(2) 类成员更改后的内存布局

原始类定义:

class demoClass
{
private:
  int a;
  int b;
};

内存布局如下:

类更改后的定义:

class demoClass
{
private:
  char c;
  int a;
  int b;
};

内存布局如下:

(3) pimpl 下类的内存布局

class demoClass
{
private:
  class demoClassImpl;
  demoClassImpl* impl;
};

class demoClass::demoClassImpl
{
public:
  int a;
  int b;
};

内存布局如下:

如上图所示,无论类的实现类的数据成员如何变化,类的布局始终不变。

二、pimpl 应用

1.功能实现细节隐藏

(1) 概述

作为接口的提供者,我们希望接口的使用者不必知道接口实现的更多细节,因为根据类的私有数据成员和方法一般就可以猜测出接口的设计方式。

(2) 隐藏实现细节

通过 pimp 方法设计类可以实现隐藏类的私有成员和方法的目的,仅对外暴露公有的接口。

class demoClass
{
public:
  void func();//对外接口
private:
  class demoClassImpl;
  demoClassImpl* impl;
};

class demoClass::demoClassImpl
{
private:
  int a;
  int b;
  
  void func1();
  void func2();
public:
  void func();
};

void demoClass::func()
{
  impl->func();
}

2.降低编译依赖

(1) 概述

在一个常用的头文件中如果包含了太多其他不必要的头文件会严重降低编译效率。

(2) 值类型的成员必须引用其头文件

值类型的成员因为要分配内存大小必须知道其确定的定义,需要包含其头文件

#include "A.h"

class demoClass
{
  A a;
};

如果仅有类的申明则会出错:

class A;
class demoClass
{
  A a;
};

(3) 指针或者引用类型,仅需要类的申明

class A;

class demoClass
{
  A  func(A a);
};

(4) 使用 pimpl 降低编译依赖

一般库文件使用者仅需要包含当前库对应的头文件即可,不应该再包含其他的头文件。假设库的头文件定义如下:

#include "A.h"

class demoClass
{
private:
  A a;
public:
  void func();
};

此时,若 A 为另外一个公共库,则库的使用者需要在项目中配置 A.h 的路径;若 A 为自定义类,则库的提供者还需要额外提供 A.h 文件。

使用 pimpl 方法改进则可以减少编译依赖,仅在类的实现文件中包含头文件即可:

// demoClass.h
class demoClass
{
public:
  void func();//对外接口
private:
  class demoClassImpl;
  demoClassImpl* impl;
};

// demoClass.cpp
#include "A.h"
class demoClass::demoClassImpl
{
private:
  A a;
public:
  void func();

};

2.动态配置功能的实现方法

(1) 概述

使用 pimpl 的方式把类的功能实现用另外一个独立的类来完成,可以在需要的时候动态的配置类的实现方法,而保持类的接口不变。

(2) 代码示例

公共接口类:

class demoClassImpl;
class demoClass
{
public:
    void func();//对外接口
public:
    demoClassImpl* impl;
};

void demoClass::func()
{
    impl->func();
}

功能实现抽象类:

class demoClassImpl
{
public:
    virtual void func() = 0;
};

功能实现派生类:

class demoClassImpl1 : public demoClassImpl
{
public:
    void func() { cout << "实现方式1" << endl; }
};

class demoClassImpl2 : public demoClassImpl
{
public:
    void func() { cout << "实现方式2" << endl; }
};

功能实现方式的动态配置:

demoClass* demo = new demoClass;

demoClassImpl1* impl1 = new demoClassImpl1;
demo->impl = impl1;
demo->func();

demoClassImpl2* impl2 = new demoClassImpl2;
demo->impl = impl2;
demo->func();

相关内容

热门资讯

如何允许远程连接到MySQL数... [[277004]]【51CTO.com快译】默认情况下,MySQL服务器仅侦听来自localhos...
如何利用交换机和端口设置来管理... 在网络管理中,总是有些人让管理员头疼。下面我们就将介绍一下一个网管员利用交换机以及端口设置等来进行D...
施耐德电气数据中心整体解决方案... 近日,全球能效管理专家施耐德电气正式启动大型体验活动“能效中国行——2012卡车巡展”,作为该活动的...
Windows恶意软件20年“... 在Windows的早期年代,病毒游走于系统之间,偶尔删除文件(但被删除的文件几乎都是可恢复的),并弹...
20个非常棒的扁平设计免费资源 Apple设备的平面图标PSD免费平板UI 平板UI套件24平图标Freen平板UI套件PSD径向平...
德国电信门户网站可实时显示全球... 德国电信周三推出一个门户网站,直观地实时提供其安装在全球各地的传感器网络检测到的网络攻击状况。该网站...
着眼MAC地址,解救无法享受D... 在安装了DHCP服务器的局域网环境中,每一台工作站在上网之前,都要先从DHCP服务器那里享受到地址动...
为啥国人偏爱 Mybatis,... 关于 SQL 和 ORM 的争论,永远都不会终止,我也一直在思考这个问题。昨天又跟群里的小伙伴进行...