C++ PImpl模式、指向实现的指针、PImpl Idiom、隐藏实现细节
flyfish
PImpl 全称是 “Pointer to Implementation”,在中文中通常翻译为“指向实现的指针”或者“指向实现”。PImpl 是一种编程技巧,通常用于 C++ 中,通过这种技术,可以隐藏类的实现细节,达到信息隐藏和二进制兼容性的目的。PImpl 也被称为“编译防火墙”(Compilation Firewall)。
PImpl 模式在遇到以下情况下可以使用:
如果是在简单场景下,不使用 PImpl 的实现更直接,如果遇到下面的情况,就可以使用PImpl 的方式。
1 隐藏实现细节:
如果你想要隐藏类的内部实现细节,使得用户只需要知道类的接口,而无需了解具体实现。
这在发布库或 API 时特别有用,可以避免用户代码依赖于类的具体实现,从而提高封装性。
2 减少编译依赖:
如果你的类实现可能经常改变,而不希望每次改变都导致所有依赖该类的代码重新编译。
例如,当一个类的成员变量或内部实现发生变化时,使用 PImpl 模式可以避免重新编译所有包括这个头文件的文件。
3提高二进制兼容性:
在需要保持 ABI(应用程序二进制接口)稳定的场景下,PImpl 模式可以在不改变类接口的前提下,修改类的实现细节,从而提高二进制兼容性。 如果你需要确保类的接口稳定,并希望在不改变接口的情况下能够自由地修改实现细节,PImpl 是一个合适的选择。
4 管理复杂实现:
当类的实现变得非常复杂时,将实现细节放在单独的实现类中,有助于保持代码的清晰和可维护性。
其他名字
Cheshire Cat:这个名字来源于《爱丽丝梦游仙境》中的柴郡猫,它有时会隐形,只留下笑脸。这个比喻形象地描述了 PImpl 模式隐藏实现细节的特性。
Compiler Firewall:这个名字强调了 PImpl 模式可以减少编译依赖,像防火墙一样隔离实现和接口。
d-pointer:这个名字主要在 Qt 库中使用,指的是 “指向数据(implementation)的指针”。
+---------------------+ +---------------------+
| MyClass | | MyClass::Impl |
|---------------------| |---------------------|
| - pImpl: Impl* | | - someData: int |
|---------------------| |---------------------|
| + MyClass() | | + Impl() |
| + ~MyClass() | | + ~Impl() |
| + someMethod() | | + someMethodImpl() |
+---------------------+ +---------------------+| |+-------------------+-------------+|has a ||+--------v---------+| std::unique_ptr||-------------------|| - Impl |+-------------------+
C++ 里的 “has a”
在 C++ 编程中,“has a” 关系指的是成员对象或成员指针的关系。也就是说,一个类拥有另一个类的对象或指针作为其成员。这种关系与继承(“is a”)不同:
“has a”(组合关系):一个类包含另一个类的对象或指针。
“is a”(继承关系):一个类继承自另一个类。
PImpl 模式中,“has a” 关系意味着 MyClass 包含一个 Impl 对象的指针,这个关系表示 MyClass 持有并管理 Impl 对象的生命周期。
示例代码
头文件:MyClass.h
#ifndef MYCLASS_H
#define MYCLASS_H#include <memory> // 使用智能指针需要包含这个头文件class MyClass {
public:MyClass(); // 构造函数~MyClass(); // 析构函数void someMethod(); // 公有方法private:// 前向声明一个名为 Impl 的类,这是实现细节类class Impl;// 使用 std::unique_ptr 管理 Impl 的指针std::unique_ptr<Impl> pImpl;
};#endif // MYCLASS_H
源文件:MyClass.cpp
#include "MyClass.h" // 包含头文件// 定义实现类 Impl
class MyClass::Impl {
public:// 实现方法void someMethodImpl() {// 具体的实现细节// 例如,这里我们简单地输出一段文字std::cout << "This is the implementation of someMethod." << std::endl;}
};// 构造函数
MyClass::MyClass() : pImpl(std::make_unique<Impl>()) {// 初始化 pImpl,指向一个新的 Impl 对象
}// 析构函数
MyClass::~MyClass() = default; // 使用默认析构函数,这里会自动释放 pImpl 指向的内存// 公有方法,调用 Impl 类的实现方法
void MyClass::someMethod() {pImpl->someMethodImpl(); // 通过 pImpl 调用具体实现
}
主程序文件:main.cpp
#include "MyClass.h" // 包含 MyClass 类的头文件
#include <iostream> // 用于输出int main() {MyClass myClass; // 创建 MyClass 对象myClass.someMethod(); // 调用公有方法return 0; // 程序结束
}
程序输出:
This is the implementation of someMethod.
说明
-
头文件 (MyClass.h) :
class MyClass
定义了一个类,其中有一个私有的指向实现类Impl
的指针pImpl
。
构造函数和析构函数声明,以及一个公有方法someMethod
的声明。 -
源文件 (MyClass.cpp) :
首先定义MyClass
的实现类Impl
。这是一个具体实现类,其中包含方法someMethodImpl
。
MyClass::MyClass
构造函数初始化pImpl
,使用std::make_unique
创建一个新的Impl
对象。
MyClass::~MyClass
析构函数默认删除pImpl
,因此不需要手动管理内存。
MyClass::someMethod
方法调用pImpl
指向的Impl
对象的someMethodImpl
方法。 -
主程序文件 (main.cpp) :
创建MyClass
对象并调用其someMethod
方法。