C++ 实现接口与实现分离后,文件变得更多了,到底有什么好处?
这是 C++ 最常用的一种设计模式:类的接口和实现分离。最典型的实现就是Pimpl (Pointer to Implementation) 惯用法。
先说结论:除了大型项目和要发布给第三方使用的二进制库必须严格 实现类的接口和实现分离 外,小型项目或个人练习的Demo没有太大的必要去使用Pimpl惯用法。
题主在书上或网络上看到的 Person 类用四个源文件 教程的可能原因是:
写教程的人参与大量的大型项目,养成了类的接口和实现分离的习惯。
写教程的人想帮读者养成良好的 Pimpl 习惯。
Pimpl惯用法的工作流程:
接口和实现分离带来的好处
1、降低编译依赖
编译防火墙,降低编译依赖是大型项目大量应用 Pimpl 模式的主要原因。
普通的 C++ 类设计,类的私有成员和依赖的头文件都必须放在类的头文件(.h)。
如果一个类用第三方库的类型作为私有成员,那么该库的头文件必须包含在类的头文件中。
如果类的私有成员变量的类型、名称或数量发生了任何微小的变化。
结果: 任何包含这个头文件的源文件(.cpp)都要重新编译。拥有数千个源文件的大型项目一个核心头文件的改动就要数千个文件进行级联重新编译。
Pimpl 模式引入一个“实现”类(Impl)和前置声明,在编译系统之间建立一道 “防火墙” :
隔离实现细节: 所有的私有成员、私有函数和相关的头文件 #include 都移动到 Impl 类定义所在的 .cpp 文件。
前置声明: 主类的头文件只要对 Impl 类进行前置声明(class PersonImpl;),不用知道内部结构。
效果: 修改 Impl 类的私有成员,主类的头文件(Person.h)内容保持不变。所有包含 Person.h 的源文件都不会重新编译。只有 Person.cpp 文件( PersonImpl 的完整定义)要重新编译。非常好的减少编译依赖。
2、真正的封装和信息隐藏
C++ 的 private 关键字是可以阻止外部代码直接访问私有成员,但这些私有成员的声明还是会暴露在头文件。谁都能看到类的内部实现细节,不符合信息隐藏的原则。
Pimpl 模式把所有实现细节(包括内部数据结构、辅助函数、内部使用的第三方库头文件)完全隐藏在 .cpp 文件。查看主类的头文件只能看到公有接口和那个指向实现的指针。实现真正的接口与实现分离,类对外部来说就是一个“黑盒”,只暴露功能,不暴露工作方式。
3、二进制兼容性
发布给第三方使用的 C++ 库,ABI 兼容性是非常大的问题。
一个类的大小和成员的内存布局是编译器确定的。如果一个已发布的库,在后续版本修改类的私有成员,那么:
类的大小发生了变化。
所有依赖于旧版本库编译的程序,链接新版本库就会因为内存布局不匹配导致运行时崩溃。
用 Pimpl 模式后,主类的大小是固定且最小化的,因为只包含一个指针的大小。
只要主类的公有接口(函数签名)没有变化,不管 Impl 类怎么修改、添加或删除私有成员,主类本身的内存布局和大小都不会改变。
库在更新内部实现时,可以保持二进制兼容性。不用重新编译应用程序的代码,只要替换新的库文件就可以。
适用性
要不要用接口与实现分离得看:愿不愿意牺牲代码的简洁,来换取工程上的高效率和稳定。
(1)大型工程项目虽然不强制接口与实现彻底分离。但也几乎是必需品。
(2)对于库的 API 设计。必须使用,特别是在发布二进制库。
(3)小型项目、个人练习就不推荐,没有必要过度设计。 小型项目的编译时间不是问题,Pimpl 带来的编译优化收益微乎其微,甚至可以忽略不计。只会带来代码冗余、文件数量增加和调试时的逻辑跳转的负面影响,远远大于带来的好处。
例外: 小型项目如果核心目标是作为一个独立的、要保持 ABI 兼容性的库发布,也可以用 Pimpl。
建议:
初期: 重点是掌握 C++ 的基础语法、内存管理和面向对象设计,用普通两文件模式就好。不要为了用 Pimpl 而用 Pimpl。
进阶: 参与大型项目,或设计一个要发布给外部用户的库,就要认真学习 Pimpl 惯用法。
Pimpl 惯用法是 “按需使用” 。只有要解决编译时间和ABI兼容性,它的价值才能真正体现出来。

请登录后发表评论
注册
停留在世界边缘,与之惜别