Pimpl模式
写在前面
Pimpl(Pointer to implementation,又称作“编译防火墙”) 是一种减少代码依赖和编译时间的C++编程技巧,其基本思想是将一个外部可见类(visible class)的实现细节(一般是所有私有的非虚成员)放在一个单独的实现类(implementation class)中,而在可见类中通过一个私有指针来间接访问该实现类。
下面通过一个简单示例说明为什么使用Pimpl、如何使用Pimpl。
类普通实现
这里创建一个简单的Fruit类,实现如下:
//Fruit.h
#pragma once
#include <string>class Fruit
{
public:Fruit();~Fruit();void display();void setPrice(double dbPrice);double getPrice() const;private:std::string m_sName;double m_dbPrice;
};
//Fruit.cpp
#include "Fruit.h"
#include <iostream>Fruit::Fruit() : m_sName(""), m_dbPrice(0.0)
{std::cout << "Fruit::Fruit\n";
}Fruit::~Fruit()
{std::cout << "Fruit::~Fruit\n";
}void Fruit::display()
{std::cout << "Name: " << m_sName << ", Price: " << m_dbPrice << std::endl;
}void Fruit::setPrice(double dbPrice)
{std::cout << "ruit::setPrice: " << dbPrice << std::endl;m_dbPrice = dbPrice;
}double Fruit::getPrice() const
{std::cout << "Fruit::getPrice\n";return m_dbPrice;
}
在其他文件(例main函数)中引用类:
//main.cpp
#include <iostream>
#include "Fruit.h"int main()
{Fruit fruit;fruit.setPrice(5.88);fruit.display();
}
上面是常见的类定义及使用方式,这里可以很明显的发现两个问题:
①头文件暴露了私有成员。当然对于内部开发这无关紧要,但对于一些对外的模块开发(如dll),外部使用人员有可能通过对外的头文件中的私有成员,推测内部实现,这显然不是公司所乐意见到的。
②接口和实现耦合,存在严重编译依赖性。例上面示例只实现的price成员的对外接口,若添加name成员的对外接口(setName、getName), 所有引用Fruit.h头文件的源文件(Fruit.cpp, main.cpp)都需要重新编译,在大型的项目中,这会花费很多编译时间。
因此,对于需要对外隐藏信息或想要减少编译依赖的需求,可以Pimpl模式实现类。
Pimpl实现
在上面Fruit类的基础上调整:
//Fruit.h
#pragma once
//事先声明
class FruitPrivate;
class Fruit
{
public:Fruit();~Fruit();void display();void setPrice(double dbPrice);double getPrice() const;//为避免后续对头文件进行修改,可事先预留所有成员的对外接口void setName(const std::string& sName);std::string getName() const;private://成员放至私有类//std::string m_sName;//double m_dbPrice;FruitPrivate* m_priFruit;};
//Fruit.cpp
#include "Fruit.h"
#include <iostream>
#include <string>/***********************************FruitPrivate*********************************************/
class FruitPrivate
{
public:FruitPrivate();~FruitPrivate();void display();void setPrice(double dbPrice);double getPrice() const;private:std::string m_sName;double m_dbPrice;};FruitPrivate::FruitPrivate() : m_sName(""), m_dbPrice(0.0)
{std::cout << "FruitPrivate::FruitPrivate\n";
}FruitPrivate::~FruitPrivate()
{std::cout << "PruitPrivate::~FruitPrivate\n";
}void FruitPrivate::display()
{std::cout << "FruitPrivate::display--Name: " << m_sName << ", Price: " << m_dbPrice << std::endl;
}void FruitPrivate::setPrice(double dbPrice)
{std::cout << "FruitPrivate::setPrice--price: " << dbPrice << std::endl;
}double FruitPrivate::getPrice() const
{std::cout << "FruitPrivate::getPrice";return m_dbPrice;
}/***********************************end FruitPrivate*********************************************/Fruit::Fruit() : m_priFruit(new FruitPrivate)//m_sName(""), m_dbPrice(0.0)
{std::cout << "Fruit::Fruit\n";
}Fruit::~Fruit()
{std::cout << "Fruit::~Fruit\n";if (m_priFruit != nullptr){delete m_priFruit;}
}void Fruit::display()
{//std::cout << "Name: " << m_sName << ", Price: " << m_dbPrice << std::endl;m_priFruit->display();
}void Fruit::setPrice(double dbPrice)
{//std::cout << "ruit::setPrice: " << dbPrice << std::endl;//m_dbPrice = dbPrice;m_priFruit->setPrice(dbPrice);
}double Fruit::getPrice() const
{//std::cout << "Fruit::getPrice\n";//return m_dbPrice;return m_priFruit->getPrice();
}//按需实现
void Fruit::setName(const std::string& sName)
{}std::string Fruit::getName() const
{}
在其他文件(上例main函数)中的引用不变。
可以看到上面调整后的头文件中不再对外展示私有成员,取而代之的是私有类的指针,原本的私有成员存放到私有类中,以实现隐藏。
另外,再添加name成员的对外接口(头文件已预留),只需重新编译Fruit.cpp即可,极大程度地减少编译依赖。
优点
①信息隐藏。私有成员完全可以隐藏在共有接口之外,尤其对于闭源API的设计尤其的适合。同时,很多代码会应用平台依赖相关的宏控制,这些琐碎的东西也完全可以隐藏在实现类当中,给用户一个简洁明了的使用接口。
②加速编译。这通常是用pImpl手法的最重要的收益,称之为编译防火墙(compilation firewall),主要是阻断了类的接口和类的实现两者的编译依赖性。这样,类用户不需要额外include不必要的头文件,同时实现类的成员可以随意变更,而公有类的使用者不需要重新编译。
③二进制兼容性。通常对一个类的修改,会影响到类的大小、对象的表示和布局等信息,那么任何该类的用户都需要重新编译才行。而且即使更新的是外部不可访问的private部分,虽然从访问性来说此时只有类成员和友元能否访问类的私有部分,但是私有部分的修改也会影响到类使用者的行为,这也迫使类的使用者需要重新编译。
而对于使用pImpl手法,如果实现变更被限制在实现类中,那公有类只持有一个实现类的指针,所以实现做出重大变更的情况下,pImpl也能够保证良好的二进制兼容性,这是pImpl的精髓所在。
缺点
①在私有类中对公有类的访问需另外设计实现。相较于常规实现,这显然会加大开发人员的时间成本,不过在Qt中,有提供Q指针和D指针,以支持公有类和私有类的相互访问,而无需另外实现。
②Pimpl对拷贝操作比较敏感,要么禁止拷贝操作,要么就需要自定义拷贝操作。每个类都需要对自己的所有成员的拷贝、赋值等操作负责。在公有类中虽然只有一个私有类的指针成员,但其(私有类)内部有多少成员,在外人看来不得而知,因此共有类和私有类都需担负起成员的拷贝、赋值等操作的责任。
③编译器将不再能够捕获const方法中对成员变量的修改。因为私有成员变量已经从公有类脱离到了实现类当中了,公有类的const只能保护指针值本身是否改变,而不再能进一步保护其所指向的数据。例上面对外的get接口,虽然在公有类中限制为const(不能修改私有类成员指针指向),但在调用的私有类对于接口的内部也有变动成员的可能(上例中在私用类对外的get接口后有加const限制)。
注意事项
pImpl最需要关注的就是共有类的复制语义,因为实现类是以指针的方式作为共有类的一个成员,而默认C++生成的拷贝操作只会执行对象的浅拷贝,这显然违背了pImpl的原本意图,除非是真的想要底层共享一个实现对象。针对这个问题,解决方式有:
①禁止复制操作 :将所有的复制操作定义为private的,或者继承 boost::noncopyable,或者在新标准中将这些复制操作定义为delete;
②显式定义复制语义:创建新的实现类对象,执行深拷贝。要么不定义拷贝、移动操作符,要定义就需要将他们全部重新定义。
优化
使用指针指针管理私有类指针成员,拷贝、赋值操作限制。
//Fruit.h
#pragma once
#include <string>
#include <memory>class FruitPrivate;class Fruit
{
public:Fruit();~Fruit();//拷贝、赋值操作处理Fruit(const Fruit&) = delete; //私有成员为指针,禁止浅拷贝Fruit& operator=(const Fruit&) = delete; //禁止赋值操作//可实现移动拷贝Fruit(Fruit&&) = default;Fruit& operator=(Fruit&&) = default;void display();void setPrice(double dbPrice);double getPrice() const;//为避免后续对头文件进行修改,可事先预留所有成员的对外接口void setName(const std::string& sName);std::string getName() const;private://成员放至私有类//std::string m_sName;//double m_dbPrice;//FruitPrivate* m_priFruit;//使用智能指针管理私有类指针std::unique_ptr<FruitPrivate> m_priFruit;};
//Fruit.cpp
#include "Fruit.h"
#include <iostream>/***********************************FruitPrivate*********************************************/
class FruitPrivate
{
public:FruitPrivate();~FruitPrivate();//拷贝、赋值操作和公有类保持一致FruitPrivate(const FruitPrivate&) = delete;FruitPrivate& operator=(const FruitPrivate&) = delete;FruitPrivate(FruitPrivate&&) = default;FruitPrivate& operator=(FruitPrivate&&) = default;void display();void setPrice(double dbPrice);double getPrice() const;private:std::string m_sName;double m_dbPrice;};FruitPrivate::FruitPrivate() : m_sName(""), m_dbPrice(0.0)
{std::cout << "FruitPrivate::FruitPrivate\n";
}FruitPrivate::~FruitPrivate()
{std::cout << "PruitPrivate::~FruitPrivate\n";
}void FruitPrivate::display()
{std::cout << "FruitPrivate::display--Name: " << m_sName << ", Price: " << m_dbPrice << std::endl;
}void FruitPrivate::setPrice(double dbPrice)
{std::cout << "FruitPrivate::setPrice--price: " << dbPrice << std::endl;
}double FruitPrivate::getPrice() const
{std::cout << "FruitPrivate::getPrice";return m_dbPrice;
}/***********************************end FruitPrivate*********************************************/Fruit::Fruit() : m_priFruit(std::make_unique<FruitPrivate>())//m_sName(""), m_dbPrice(0.0)
{std::cout << "Fruit::Fruit\n";
}Fruit::~Fruit()
{std::cout << "Fruit::~Fruit\n";//if (m_priFruit != nullptr)//{// delete m_priFruit;//}
}void Fruit::display()
{//std::cout << "Name: " << m_sName << ", Price: " << m_dbPrice << std::endl;m_priFruit->display();
}void Fruit::setPrice(double dbPrice)
{//std::cout << "ruit::setPrice: " << dbPrice << std::endl;//m_dbPrice = dbPrice;m_priFruit->setPrice(dbPrice);
}double Fruit::getPrice() const
{//std::cout << "Fruit::getPrice\n";//return m_dbPrice;return m_priFruit->getPrice();
}//按需实现
void Fruit::setName(const std::string& sName)
{}std::string Fruit::getName() const
{return "";
}
总结
类的常规实现和Pimpl实现各有优劣。若只是为了快速开发且没有对外隐藏需求,常规实现无疑是很好的选择,若想要减少编译依赖且不想对外展示私有成员,可选择使用Pimpl实现,代价就是开发及维护成本的提高。
相关文章:
Pimpl模式
写在前面 Pimpl(Pointer to implementation,又称作“编译防火墙”) 是一种减少代码依赖和编译时间的C编程技巧,其基本思想是将一个外部可见类(visible class)的实现细节(一般是所有私有的非虚成员)放在一个单独的实现类(implemen…...

Python 密码破解指南:5~9
协议:CC BY-NC-SA 4.0 译者:飞龙 本文来自【OpenDocCN 饱和式翻译计划】,采用译后编辑(MTPE)流程来尽可能提升效率。 收割 SB 的人会被 SB 们封神,试图唤醒 SB 的人是 SB 眼中的 SB。——SB 第三定律 五、凯…...
ARM驱动开发
驱动 以来内核编译,依赖内核执行 驱动可以同时执行多份代码 没main 驱动是依赖内核的框架和操作硬件的过程 一,Linux系统组成 app: [0-3G] ---------------------------------系统调用(软中断…...

Matlab图像处理-加法运算
加法运算 图像加法运算的一个应用是将一幅图像的内容叠加到另一幅图像上,生成叠加图像效果,或给图像中每个像素叠加常数改变图像的亮度。 在MATLAB图像处理工具箱中提供的函数imadd()可实现两幅图像的相加或者一幅图像和常量的相加。 程序代码 I1 i…...

Docker容器学习:搭建自己专属的LAMP环境
目录 编写Dockerfile 1.文件内容需求: 2.值得注意的是centos6官方源已下线,所以需要切换centos-vault源! 3.Dockerfile内容 4.进入到 lamp 开始构建镜像 推送镜像到私有仓库 1.把要上传的镜像打上合适的标签 2.登录harbor仓库 3.上传镜…...

问道管理:沪指弱势震荡跌0.38%,金融、地产等板块走弱,算力概念等活跃
21日早盘,沪指盘中弱势震荡下探,创业板指一度跌逾1%失守2100点;北向资金小幅净流出。 截至午间收盘,沪指跌0.38%报3120.18点,深成指跌0.24%,创业板指跌0.62%;两市算计成交4238亿元,…...
OpenWrt package - BuildPackage
一. 前言 该文章所涉及到的知识都来自OpenWrt Wiki官网。OpenWrt的软件编译模板系统使软件移植到OpenWrt变得非常简单,如果在一个典型的package目录下,我们可以发现3个东西:package/Makefile,package/patches,package/…...
C++三体星战小游戏
物理小游戏,懒得 写注释。 游戏代码 #include<bits/stdc.h> #include<bits/stdc.h> #include<windows.h> #include<conio.h> using namespace std; int toint(double a){return ((int)(a*105))/10;} int rand(int a){return rand()%a;} vo…...

【zip密码】修改zip压缩包密码
Zip压缩包设置了密码,想要修改密码,我们该如何操作?今天分享两个修改zip压缩包密码的方法。 方法一: 输入密码,将zip压缩包里面的文件解压出来。 然后找到解压出来的文件,将文件重新压缩,并且…...
小小讲一下Linux基本命令
Linux是一套类Unix的操作系统,这套系统最大的优点就是安全便捷,快速高效。这就为它赢得了广大的市场空间。但是呢,Linux系统虽然广为流行,它也不是那么容易就可以学会的。比如说,如果我们不懂得Linux系统的基本操作命令…...
Python数据容器(列表list、元组tuple、字符串str、字典dict、集合set)详解
一、数据容器概念 相关介绍: 一种可以容纳多份数据的数据类型,容纳的每一份数据称之为一个元素。每一个元素,可以是任意类型的数据分为五类:列表[list]、元组(tuple)、字符串(str)、集合{set}、字典{dict} 相应区别:…...

2023高教社杯数学建模思路 - 复盘:人力资源安排的最优化模型
文章目录 0 赛题思路1 描述2 问题概括3 建模过程3.1 边界说明3.2 符号约定3.3 分析3.4 模型建立3.5 模型求解 4 模型评价与推广5 实现代码 建模资料 0 赛题思路 (赛题出来以后第一时间在CSDN分享) https://blog.csdn.net/dc_sinor?typeblog 1 描述 …...

Linux 计算机网络基础概论
一、网络基本概念 1、网络 网络是由若干节点和连接这些结点的链路组成,网络中的结点可以是计算机、交换机、路由器等设备。通俗地说就是把不同的主机连接起来就构成了一个网络,构成网路的目的是为了信息交互、资源共享。 网络设备有:交换机…...

深入理解 C++ 中的 std::cref、std::ref 和 std::reference_wrapper
深入理解 C 中的 std::cref、std::ref 和 std::reference_wrapper 在 C 编程中,有时候我们需要在不进行拷贝的情况下传递引用,或者在需要引用的地方使用常量对象。为了解决这些问题,C 标准库提供了三个有用的工具:std::cref、std:…...

在其他python环境中使用jupyter notebook
1、切换到目标python环境 activate 目标python环境 2、安装notebook内核包 pip install ipykernel 3、加环境加入到notebook中 python -m ipykernel install 目标python环境 4、切换到base环境 activate base 5、打开目标项目的对应盘 如果,项目在c盘&…...

计算机网络-笔记-第二章-计算机网络概述
目录 二、第二章——物理层 1、物理层的基本概念 2、物理层下面的传输媒体 (1)光纤、同轴电缆、双绞线、电力线【导引型】 (2)无线电波、微波、红外线、可见光【非导引型】 (3)无线电【频谱的使用】 …...
Hive字符串数组json类型取某字段再列转行
一、原始数据 acctcontent1232313[{"name":"张三","code":"上海浦东新区89492jfkdaj\r\n福建的卡"...},{"name":"狂徒","code":"select * from table where aa1\r\n and a12"...},{...}]...…...

ElementUI Table 翻页缓存数据
Element UI Table 翻页保存之前的数据,网上找了一些,大部分都是用**:row-key** 和 reserve-selection,但是我觉得有bug,我明明翻页了…但是全选的的个框还是勾着的(可能是使用方法不对,要是有好使的…请cute我一下…感谢) 所以自己写了一个… 思路: 手动勾选的时候,将数据保存…...
使用 AutoGPTQ 和 transformers 让大语言模型更轻量化
大语言模型在理解和生成人类水平的文字方面所展现出的非凡能力,正在许多领域带来应用上的革新。然而,在消费级硬件上训练和部署大语言模型的需求也变得越来越难以满足。 🤗 Hugging Face 的核心使命是 让优秀的机器学习普惠化 ,而…...
AUTOSAR规范与ECU软件开发(实践篇)6.5 BswM模块概念与配置方法介绍
目录 1、BswM模块概念与配置方法介绍 (1) BswMModeRequestPort配置 (2) ModeCondition与LogicalExpression配置...
【服务器压力测试】本地PC电脑作为服务器运行时出现卡顿和资源紧张(Windows/Linux)
要让本地PC电脑作为服务器运行时出现卡顿和资源紧张的情况,可以通过以下几种方式模拟或触发: 1. 增加CPU负载 运行大量计算密集型任务,例如: 使用多线程循环执行复杂计算(如数学运算、加密解密等)。运行图…...
Pinocchio 库详解及其在足式机器人上的应用
Pinocchio 库详解及其在足式机器人上的应用 Pinocchio (Pinocchio is not only a nose) 是一个开源的 C 库,专门用于快速计算机器人模型的正向运动学、逆向运动学、雅可比矩阵、动力学和动力学导数。它主要关注效率和准确性,并提供了一个通用的框架&…...
【Go语言基础【13】】函数、闭包、方法
文章目录 零、概述一、函数基础1、函数基础概念2、参数传递机制3、返回值特性3.1. 多返回值3.2. 命名返回值3.3. 错误处理 二、函数类型与高阶函数1. 函数类型定义2. 高阶函数(函数作为参数、返回值) 三、匿名函数与闭包1. 匿名函数(Lambda函…...

springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...

【笔记】WSL 中 Rust 安装与测试完整记录
#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统:Ubuntu 24.04 LTS (WSL2)架构:x86_64 (GNU/Linux)Rust 版本:rustc 1.87.0 (2025-05-09)Cargo 版本:cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...
从面试角度回答Android中ContentProvider启动原理
Android中ContentProvider原理的面试角度解析,分为已启动和未启动两种场景: 一、ContentProvider已启动的情况 1. 核心流程 触发条件:当其他组件(如Activity、Service)通过ContentR…...
DiscuzX3.5发帖json api
参考文章:PHP实现独立Discuz站外发帖(直连操作数据库)_discuz 发帖api-CSDN博客 简单改造了一下,适配我自己的需求 有一个站点存在多个采集站,我想通过主站拿标题,采集站拿内容 使用到的sql如下 CREATE TABLE pre_forum_post_…...

Mysql故障排插与环境优化
前置知识点 最上层是一些客户端和连接服务,包含本 sock 通信和大多数jiyukehuduan/服务端工具实现的TCP/IP通信。主要完成一些简介处理、授权认证、及相关的安全方案等。在该层上引入了线程池的概念,为通过安全认证接入的客户端提供线程。同样在该层上可…...
GB/T 43887-2024 核级柔性石墨板材检测
核级柔性石墨板材是指以可膨胀石墨为原料、未经改性和增强、用于核工业的核级柔性石墨板材。 GB/T 43887-2024核级柔性石墨板材检测检测指标: 测试项目 测试标准 外观 GB/T 43887 尺寸偏差 GB/T 43887 化学成分 GB/T 43887 密度偏差 GB/T 43887 拉伸强度…...

使用ch340继电器完成随机断电测试
前言 如图所示是市面上常见的OTA压测继电器,通过ch340串口模块完成对继电器的分路控制,这里我编写了一个脚本方便对4路继电器的控制,可以设置开启时间,关闭时间,复位等功能 软件界面 在设备管理器查看串口号后&…...