CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • 第1章C++惯用法与ModernC++篇

    • 1.1 C++ RAII惯用法
    • 1.2 pimpl 惯用法
    • 1.3 拥抱C++新变化(C++11/14/17新增的实用特性)
    • 1.4 统一的类成员初始化语法与 std::initializer_list<T>
    • 1.5 C++17注解标签(attributes)
    • 1.6 final/override/=default/=delete语法
    • 1.7 auto关键字的前尘后事
    • 1.8 Range-based 循环语法
    • 1.9 C++17结构化绑定
    • 1.10 stl容器新增的实用方法介绍
    • 1.11 stl中的智能指针类详解
  • 第2章C++开发工具与调试进阶

  • 第3章C++多线程编程从入门到进阶

  • 第4章C++网络编程重难点解析

  • 第5章网络通信故障排查常用命令

  • 第6章网络通信协议设计

  • 第7章高性能服务结构设计

  • 第8章Redis 网络通信模块源码分析

  • 第9章 服务其他模块设计

  • C++后端开发进阶
  • 第1章C++惯用法与ModernC++篇
zhangxf
2023-04-03

1.2 pimpl 惯用法

现在这里有一个名为CSocketClient的网络通信类,定义如下:

/**
 * 网络通信的基础类, SocketClient.h
 * zhangyl 2017.07.11
 */
class CSocketClient
{
public:
    CSocketClient();
    ~CSocketClient();
 
public:  
    void SetProxyWnd(HWND hProxyWnd);

    bool    Init(CNetProxy* pNetProxy);
    bool    Uninit();
    
    int Register(const char* pszUser, const char* pszPassword); 
    void GuestLogin();  
    
    BOOL    IsClosed();
    BOOL	Connect(int timeout = 3);
    void    AddData(int cmd, const std::string& strBuffer);
    void    AddData(int cmd, const char* pszBuff, int nBuffLen);
    void    Close();

    BOOL    ConnectServer(int timeout = 3);
    BOOL    SendLoginMsg();
    BOOL    RecvLoginMsg(int& nRet);
    BOOL    Login(int& nRet);

private:
    void LoadConfig();
    static UINT CALLBACK SendDataThreadProc(LPVOID lpParam);
    static UINT CALLBACK RecvDataThreadProc(LPVOID lpParam);
    bool Send();
    bool Recv();
    bool CheckReceivedData();
    void SendHeartbeatPackage();

private:
    SOCKET                          m_hSocket;
    short                           m_nPort;
    char                            m_szServer[64];
    long                            m_nLastDataTime;        //最近一次收发数据的时间
    long                            m_nHeartbeatInterval;   //心跳包时间间隔,单位秒
    CRITICAL_SECTION                m_csLastDataTime;       //保护m_nLastDataTime的互斥体 
    HANDLE                          m_hSendDataThread;      //发送数据线程
    HANDLE                          m_hRecvDataThread;      //接收数据线程
    std::string                     m_strSendBuf;
    std::string                     m_strRecvBuf;
    HANDLE                          m_hExitEvent;
    bool                            m_bConnected;
    CRITICAL_SECTION                m_csSendBuf;
    HANDLE                          m_hSemaphoreSendBuf;
    HWND                            m_hProxyWnd;
    CNetProxy*                      m_pNetProxy;
    int                             m_nReconnectTimeInterval;    //重连时间间隔
    time_t                          m_nLastReconnectTime;        //上次重连时刻
    CFlowStatistics*                m_pFlowStatistics;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

这段代码来源于笔者实际项目中开发的一个股票客户端的软件。

CSocketClient类的public方法提供对外接口供第三方使用,每个函数的具体实现在SocketClient.cpp中,对第三方使用者不可见。在Windows系统上作为提供给第三方使用的库,一般需要提供给使用者*.h、*.lib和*.dll文件,在 Linux 系统上需要提供*.h、*.a或*.so文件。

不管是在哪个操作系统平台上,像SocketClient.h这样的头文件提供给第三方使用时,都会让库的作者心里隐隐不安——因为SocketClient.h文件中SocketClient类大量的成员变量和私有函数暴露了这个类太多的实现细节,很容易让使用者看出实现原理。这样的头文件,对于一些不想对使用者暴露核心技术实现的库和 sdk,是非常不好的。

那有没有什么办法既能保持对外的接口不变,又能尽量不暴露一些关键性的成员变量和私有函数的实现方法呢?有的。我们可以将代码稍微修改一下:

/**
 * 网络通信的基础类, SocketClient.h
 * zhangyl 2017.07.11
 */
class Impl;

class CSocketClient
{
public:
    CSocketClient();
    ~CSocketClient();
 
public:
    void SetProxyWnd(HWND hProxyWnd);

    bool    Init(CNetProxy* pNetProxy);
    bool    Uninit();

    int Register(const char* pszUser, const char* pszPassword);    
    void GuestLogin();  
    
    BOOL    IsClosed();
    BOOL	Connect(int timeout = 3);
    void    AddData(int cmd, const std::string& strBuffer);
    void    AddData(int cmd, const char* pszBuff, int nBuffLen);
    void    Close();

    BOOL    ConnectServer(int timeout = 3);
    BOOL    SendLoginMsg();
    BOOL    RecvLoginMsg(int& nRet);
    BOOL    Login(int& nRet);

private:
    Impl*	m_pImpl;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

上述代码中,所有的关键性成员变量已经没有了,取而代之的是一个类型为Impl的指针成员变量m_pImpl。

具体采用什么名称,读者完全可以根据自己的实际情况来定,不一定非要使用这里的Impl和m_pImpl。

Impl类型现在是完全对使用者透明,为了在当前类中可以使用Impl,使用了一个前置声明:

//原代码第5行
class Impl;
1
2

然后我们就可以将刚才隐藏的成员变量放到这个类中去:

class Impl
{
public:
	Impl()
	{
        //TODO: 你可以在这里对成员变量做一些初始化工作
	}
	
	~Impl()
	{
        //TODO: 你可以在这里做一些清理工作
	}
	
public:
	SOCKET                          m_hSocket;
    short                           m_nPort;
    char                            m_szServer[64];
    long                            m_nLastDataTime;        //最近一次收发数据的时间
    long                            m_nHeartbeatInterval;   //心跳包时间间隔,单位秒
    CRITICAL_SECTION                m_csLastDataTime;       //保护m_nLastDataTime的互斥体 
    HANDLE                          m_hSendDataThread;      //发送数据线程
    HANDLE                          m_hRecvDataThread;      //接收数据线程
    std::string                     m_strSendBuf;
    std::string                     m_strRecvBuf;
    HANDLE                          m_hExitEvent;
    bool                            m_bConnected;
    CRITICAL_SECTION                m_csSendBuf;
    HANDLE                          m_hSemaphoreSendBuf;
    HWND                            m_hProxyWnd;
    CNetProxy*                      m_pNetProxy;
    int                             m_nReconnectTimeInterval;    //重连时间间隔
    time_t                          m_nLastReconnectTime;        //上次重连时刻
    CFlowStatistics*                m_pFlowStatistics;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

接着我们在CSocketClient的构造函数中创建这个m_pImpl对象,在CSocketClient析构函数中释放这个对象。

CSocketClient::CSocketClient()
{
	m_pImpl = new Impl();
}

CSocketClient::~CSocketClient()
{
	delete m_pImpl;
}
1
2
3
4
5
6
7
8
9

这样,原来需要引用的成员变量,可以在CSocketClient内部使用m_pImpl->变量名来引用了。

这里仅仅以演示隐藏CSocketClient的成员变量为例,隐藏其私有方法与此类似,都是变成类Impl的方法。

需要强调的是,在实际开发中,由于Impl类是CSocketClient的辅助类, Impl类没有独立存在的必要,所以一般会将Impl类定义成CSocketClient的内部类。即采用如下形式:

/**
 * 网络通信的基础类, SocketClient.h
 * zhangyl 2017.07.11
 */
class CSocketClient
{
public:
    CSocketClient();
    ~CSocketClient();

 //重复的代码省略...

private:
	class   Impl;
    Impl*	m_pImpl;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

然后在ClientSocket.cpp中定义Impl类的实现:

/**
 * 网络通信的基础类, SocketClient.cpp
 * zhangyl 2017.07.11
 */
class  CSocketClient::Impl
{
public:
    void LoadConfig()
    {
    	//方法的具体实现
    }
    
    //其他方法省略...
    
public:
	SOCKET                          m_hSocket;
    short                           m_nPort;
    char                            m_szServer[64];
    long                            m_nLastDataTime;        //最近一次收发数据的时间
    long                            m_nHeartbeatInterval;   //心跳包时间间隔,单位秒
    CRITICAL_SECTION                m_csLastDataTime;       //保护m_nLastDataTime的互斥体 
    HANDLE                          m_hSendDataThread;      //发送数据线程
    HANDLE                          m_hRecvDataThread;      //接收数据线程
    std::string                     m_strSendBuf;
    std::string                     m_strRecvBuf;
    HANDLE                          m_hExitEvent;
    bool                            m_bConnected;
    CRITICAL_SECTION                m_csSendBuf;
    HANDLE                          m_hSemaphoreSendBuf;
    HWND                            m_hProxyWnd;
    CNetProxy*                      m_pNetProxy;
    int                             m_nReconnectTimeInterval;    //重连时间间隔
    time_t                          m_nLastReconnectTime;        //上次重连时刻
    CFlowStatistics*                m_pFlowStatistics;
}
 
CSocketClient::CSocketClient()
{
	m_pImpl = new Impl();
}

CSocketClient::~CSocketClient()
{
	delete m_pImpl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

现在CSocketClient这个类除了保留对外的接口以外,其内部实现用到的变量和方法基本上对使用者不可见了。C++中对类的这种封装方式,我们称之为pimpl惯用法,即Pointer to Implementation(也有人认为是Private Implementation)。

在实际的开发中,Impl类的声明和定义既可以使用class关键字也可以使用struct关键字。在C++语言中,struct类型可以定义成员方法,但struct所有成员变量和方法默认都是public的。

现在来总结一下这个方法的优点:

  • 核心数据成员被隐藏;

    核心数据成员被隐藏,不必暴露在头文件中,对使用者透明,提高了安全性。

  • 降低编译依赖,提高编译速度;

    由于原来的头文件的一些私有成员变量可能是非指针非引用类型的自定义类型,需要在当前类的头文件中包含这些类型的头文件,使用了pimpl惯用法以后,这些私有成员变量被移动到当前类的cpp文件中,因此头文件不再需要包含这些成员变量的类型头文件,当前头文件变“干净”,这样其他文件在引用这个头文件时,依赖的类型变少,加快了编译速度。

  • 接口与实现分离。

    使用了pimpl惯用法之后,即使CSocketClient或者Impl类的实现细节发生了变化,对使用者都是透明的,对外的CSocketClient类声明仍然可以保持不变。例如我们可以增删改Impl的成员变量和成员方法而保持SocketClient.h文件内容不变;如果不使用pimpl惯用法,我们做不到不改变SocketClient.h文件而增删改CSocketClient类的成员。

智能指针用于pimpl惯用法

C++11标准引入了智能指针对象,我们可以使用std::unique_ptr对象来管理上述用于隐藏具体实现的m_pImpl指针。

SocketClient.h文件可以修改成如下方式:

//for std::unique_ptr
#include <memory>

class CSocketClient
{
public:
    CSocketClient();
    ~CSocketClient();

    //重复的代码省略...

private:
    struct                  Impl;
    std::unique_ptr<Impl>   m_pImpl;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

SocketClient.cpp中修改CSocketClient对象的构造函数和析构函数的实现如下:

构造函数

如果你的编译器仅支持C++11标准,我们可以按如下修改:

CSocketClient::CSocketClient()
{
    //C++11标准并未提供std::make_unique(),该方法是C++14提供的
    m_pImpl.reset(new Impl());
}
1
2
3
4
5

如果你的编译器支持C++14及以上标准,可以这么修改:

CSocketClient::CSocketClient() : m_pImpl(std::make_unique<Impl>())
{    
}
1
2
3

由于已经使用了智能指针来管理m_pImpl指向的堆内存,因此析构函数中不再需要显式释放堆内存:

CSocketClient::~CSocketClient()
{
    //不再需要显式 delete 了 
    //delete m_pImpl;
}
1
2
3
4
5

pimp惯用法是C/C++项目开发中一种非常实用的代码编写策略,建议读者掌握它。

上次更新: 2025/04/01, 20:53:14
1.1 C++ RAII惯用法
1.3 拥抱C++新变化(C++11/14/17新增的实用特性)

← 1.1 C++ RAII惯用法 1.3 拥抱C++新变化(C++11/14/17新增的实用特性)→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式