![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=1738808689-goP8wKgpgCnRRia3sg2wA5t7HDTXAPhv-0-ce27f7bfaecab241ad1ebb5e8b24bdf2)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_30_1.jpg?sign=1738808689-toH9g0AZNNkYQCSVJC73mAjXB85CLKP5-0-5ad9ddb5772da6de15b35be7e71e6894)
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=1738808689-cR1gQwvQB2BQUkMtk5DS6TWIUGemk5Gb-0-62cef7ce4e2dc69985cbff909e3a055c)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_31_1.jpg?sign=1738808689-CmfBF7Oj6TCKJpZOLkQYP0isik69YXGR-0-97e17c43373e96b70682d707947b9fb2)
在以上代码中,所有的关键成员变量都已经不存在了,取而代之的是一个类型为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=1738808689-uTetCXRVMxTeefUb1NrVeMqeVr9iFb0z-0-4756cb79c40834bbb7b2caeb4bd68b31)
我们接着在CSocketClient构造函数中创建这个m_pImpl对象,在CSocketClient析构函数中释放这个对象:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_31_3.jpg?sign=1738808689-rjek75aaB49Pkj2buwXdDoRz7PHxCjhi-0-ab0f40e2e5207dba4544ee2c06107bab)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_32_1.jpg?sign=1738808689-W7oco33Pff0LcKILzV1MCaQbuUGVK9Pw-0-41741fcde383b0619456152da9f92a27)
这样,在 CSocketClient 类内部,对于我们原来直接引用的成员变量,现在可以使用m_pImpl->变量名来引用了。
这里仅以演示隐藏 CSocketClient 的成员变量为例,隐藏类的私有方法与隐藏成员变量的做法相同,即将原来属于CSocketClient的方法变成Impl的方法。
需要强调的是,在实际开发中,由于Impl类是CSocketClient的辅助类,没有独立存在的必要,所以一般会将Impl类定义成CSocketClient的内部类。即采用如下形式:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_32_2.jpg?sign=1738808689-tLAl4mJP3JYfQJWgfWLBKAxbp8EzAb5c-0-2730bfe20981dc50844c25dfde22316a)
然后在ClientSocket.cpp中定义Impl类的实现:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_32_3.jpg?sign=1738808689-WC7Emq25p3gZPtQ0GC7RjlZHQACG6veR-0-08c6974e34e6c15dfa54f12f363a1736)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_33_1.jpg?sign=1738808689-BDYxBbfvmQaAX4EWY5yz0hl9KnjhdzkO-0-2c783a5314d495f14f00a83eeb987a3e)
现在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=1738808689-Fuvv86kl3l4cBcBB0rOkmzcRKcoXEItl-0-7f51cc891df6b5714321e46acb4c0a1b)
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_34_1.jpg?sign=1738808689-66BTzGTqdoOMZg6QbiPx3Sx0U30inVe2-0-139b1593e3cd517a1d93bb627ff73c90)
在SocketClient.cpp中修改CSocketClient对象的构造函数和析构函数,如果编译器仅支持C++11标准,则可以这么修改:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_34_2.jpg?sign=1738808689-gIhmlBxfzJFHJ2aNN06gq9Y0c5Ef6olX-0-11bd8134e8124d829f4209d6f84b95f8)
如果编译器支持C++14及以上标准,则可以这么修改:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_34_3.jpg?sign=1738808689-tOjX9parQYy01NQzozxrLNwDD1NxZzyV-0-0622c89b0da700a8daa08b9b7c398ca3)
由于已经使用了智能指针来管理 m_pImpl 指向的堆内存,所以在析构函数中不再需要显式地释放堆内存:
![](https://epubservercos.yuewen.com/EE4394/20637464301305906/epubprivate/OEBPS/Images/41263_34_4.jpg?sign=1738808689-U60E77gnHaCYfkrhnqDlA5vZLoaq42dy-0-b65b21e1bafeb61ba9fe6fa761e1e5d0)
pimp惯用法是C/C++项目开发中一种非常实用的代码编写策略,建议读者掌握它。