![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=1739347517-TIGYmvku4LFQNTavzgFr6TehT4osCRzO-0-fb258ad31f2d6b1bc8a129abb81d829b)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_30_1.jpg?sign=1739347517-EENbBhQgZ0Ackd8Ewr3J5b203SQoqkXW-0-4a3aa1e368a628dffaa0a66a1e3f6b49)
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=1739347517-qzPPSfIVeLHnu1Zq5NYVvEUxKHMD7fMP-0-78f98193aec19841d376c2dd0b15d393)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_31_1.jpg?sign=1739347517-NLyygLsDlWhEiu9JofTFYYIxbMvNVt12-0-bf64186b22d5b3364df723c3b7643041)
在以上代码中,所有的关键成员变量都已经不存在了,取而代之的是一个类型为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=1739347517-szPjsZZgZ7PaCo7pnAz943UjFif5b5V6-0-56dac164edba53573fa8d1c4c280bc7b)
我们接着在CSocketClient构造函数中创建这个m_pImpl对象,在CSocketClient析构函数中释放这个对象:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_31_3.jpg?sign=1739347517-1Kr5JNC9kkbpPAP18WVIJKgLdt8KQ0TK-0-bbec531c7cb2344ef8406169ebaec576)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_32_1.jpg?sign=1739347517-XvBq8sOozfA9iVe3QjD06PDhUISOMlVw-0-0c8fda8da0b9a36c1089eb35745a6610)
这样,在 CSocketClient 类内部,对于我们原来直接引用的成员变量,现在可以使用m_pImpl->变量名来引用了。
这里仅以演示隐藏 CSocketClient 的成员变量为例,隐藏类的私有方法与隐藏成员变量的做法相同,即将原来属于CSocketClient的方法变成Impl的方法。
需要强调的是,在实际开发中,由于Impl类是CSocketClient的辅助类,没有独立存在的必要,所以一般会将Impl类定义成CSocketClient的内部类。即采用如下形式:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_32_2.jpg?sign=1739347517-78tIWO2YnybRss8Ee2ZEjiHDKStNWMup-0-34563ac9ae5cb7e4cf2afa095702ba38)
然后在ClientSocket.cpp中定义Impl类的实现:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_32_3.jpg?sign=1739347517-taegznhF6DI503U2KCuKCU0hDsd7FDiU-0-bafd193d0d9c1705929a2dd6126abe07)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_33_1.jpg?sign=1739347517-299Ah8cpm4Pwzo6nlEEIf3QMYWdQRZnC-0-8f30666f0a560bd5dc3bf8b91416a390)
现在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=1739347517-HsmlnAYEgTdNPAByarw5PC8kc8uQvfLE-0-bcd7b1fcfeb44ce5772d92005d54f824)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_34_1.jpg?sign=1739347517-gUnNiqCVcdzw0fN1DiEH5P9IvagwRZ6U-0-3365828a69622dd99f56358335546800)
在SocketClient.cpp中修改CSocketClient对象的构造函数和析构函数,如果编译器仅支持C++11标准,则可以这么修改:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_34_2.jpg?sign=1739347517-jQjrLIjzd8S9gdAhGcEcUOCKUvxftjaZ-0-d1725041a78eb760d69a5f782669b815)
如果编译器支持C++14及以上标准,则可以这么修改:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_34_3.jpg?sign=1739347517-lISYU4xpbZdYWH2qDSe2RNL9zmtNVoup-0-bf8bf2e47d9c199cc0b9acd996d9574c)
由于已经使用了智能指针来管理 m_pImpl 指向的堆内存,所以在析构函数中不再需要显式地释放堆内存:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_34_4.jpg?sign=1739347517-rdNnCTdEOlRj1oCyHIAHMl9B89sX4ISa-0-9e17839e9e0dbfa7df742650fd1de92b)
pimp惯用法是C/C++项目开发中一种非常实用的代码编写策略,建议读者掌握它。