![C++服务器开发精髓](https://wfqqreader-1252317822.image.myqcloud.com/cover/623/39479623/b_39479623.jpg)
1.2 pimpl惯用法
这里有一个名为CSocketClient的网络通信类,定义如下:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_29_1.jpg?sign=1734438799-4mJu02ONENjPgaQrglGLbsXtneIzBYd2-0-be7e1f20a1179cd6364771143159f0a2)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_30_1.jpg?sign=1734438799-imP3GzkMXsqfnJBumllp973H3k9N9tDR-0-54e0f5b9cc1ef43b44184fe832e85972)
CSocketClient 类的 public 方法提供了对外接口供第三方使用,每个函数的具体实现都在SocketClient.cpp中,对第三方不可见。对于在Windows系统上提供给第三方使用的库,库作者一般需要提供.h、.lib和.dll文件给库使用者,对于Linux系统则需要提供.h、.a或.so文件。
不管在哪种操作系统上,提供像SocketClient.h这样的头文件给第三方使用时,库作者大多会隐隐不安——因为SocketClient.h文件中CSocketClient类的大量成员变量和私有函数都暴露了这个类的太多实现细节,很容易让使用者看出其实现原理。这样的头文件对于一些涉及核心技术实现的库和SDK,是非常敏感的。
那有没有办法既能保持对外接口不变,又能尽量不暴露一些关键的成员变量和私有函数的实现方法呢?有,我们可以将代码稍微修改一下:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_30_2.jpg?sign=1734438799-fDExeXI8Q2o3O8pUKau9b64aUA1tT9q9-0-b4b497c48c23ddc2f3ee2029e2b93496)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_31_1.jpg?sign=1734438799-VAxmzvOlwdvMqO2Ir2bjg2iBZgQNyhLu-0-4a1266948369c0cff1e1e46530ef581d)
在以上代码中,所有的关键成员变量都已经不存在了,取而代之的是一个类型为Impl的指针成员变量m_pImpl。
具体采用什么名称,读者完全可以根据自己的实际情况来定,不一定非要使用“Impl”和“m_pImpl”这样的名称。
Impl 类现在对使用者完全透明,为了在 CSocketClient 类中引用 Impl 类,我们在SocketClient.h文件中使用了一个前置声明(以上加粗代码行),然后就可以将原来属于CSocketClient类的成员变量转移到Impl类中了:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_31_2.jpg?sign=1734438799-76lEOygHjoftI4HBMKyYSicKZARX0End-0-b842cbfcefd77e5a929b905c99cd5297)
我们接着在CSocketClient构造函数中创建这个m_pImpl对象,在CSocketClient析构函数中释放这个对象:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_31_3.jpg?sign=1734438799-shBt5npE87PF6HLer7zlrLQdvE4UELIQ-0-79d5fd833b047e21ff8a89f5dffa5477)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_32_1.jpg?sign=1734438799-XDsk54QoQmiCxTJt8LTKPP63D3HM78PD-0-3d5d33ce7863770788dfec2a50b84b97)
这样,在 CSocketClient 类内部,对于我们原来直接引用的成员变量,现在可以使用m_pImpl->变量名来引用了。
这里仅以演示隐藏 CSocketClient 的成员变量为例,隐藏类的私有方法与隐藏成员变量的做法相同,即将原来属于CSocketClient的方法变成Impl的方法。
需要强调的是,在实际开发中,由于Impl类是CSocketClient的辅助类,没有独立存在的必要,所以一般会将Impl类定义成CSocketClient的内部类。即采用如下形式:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_32_2.jpg?sign=1734438799-p0VSi49zKCAMNHNsLiIJQmnB58AkUsgs-0-69c2aacd7076a3a0f65b1a1b49909ac6)
然后在ClientSocket.cpp中定义Impl类的实现:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_32_3.jpg?sign=1734438799-QBvzKXv5Rse44M5pcpCqD1uXJZ7Iallh-0-69df91b1ba692192e33378ee9900ff0b)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_33_1.jpg?sign=1734438799-aHjqyXiO4Hpgeqidrk88LHyFTLZbyOnB-0-1684f6923bf06ecb3a7a20233854c7ac)
现在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类的成员。
C++11标准引入了智能指针对象,我们可以使用std::unique_ptr对象来管理上述用于隐藏具体实现的m_pImpl指针。可以将SocketClient.h文件修改如下:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_33_2.jpg?sign=1734438799-1Uivl5jFKQZDXg0S30wVW192P8TsJTYm-0-80fd5e7b77008e66bb3f4fe0cf38e6d3)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_34_1.jpg?sign=1734438799-TGmzPtIczFZgjFEB1wfVCUMqOuyxJOj0-0-0616749a1d8fab4045323be0a49b9254)
在SocketClient.cpp中修改CSocketClient对象的构造函数和析构函数,如果编译器仅支持C++11标准,则可以这么修改:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_34_2.jpg?sign=1734438799-q9UzXmlTbajxvJvo3dRpiHU3t7rr6IBa-0-087be3b8cbd1a4ec1f5020403dd2abd9)
如果编译器支持C++14及以上标准,则可以这么修改:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_34_3.jpg?sign=1734438799-6oeL67kcwTP40Ha3ncuoTSQz3VnPLONv-0-b906233a690920a72ae03d0d19c66b0a)
由于已经使用了智能指针来管理 m_pImpl 指向的堆内存,所以在析构函数中不再需要显式地释放堆内存:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_34_4.jpg?sign=1734438799-DS3raHreKOcAhSlsRQPtPwGyXBqpjiSi-0-d3f277ce76e2a1facd5015590885e02b)
pimp惯用法是C/C++项目开发中一种非常实用的代码编写策略,建议读者掌握它。