面向对象软件设计于分析(26)C++ IMPL 模式详解
1 概念
Pimpl是C++中一种设计模式,全称为Pointer to Implementation,它可以用于隐藏类的实现细节,从而实现封装和减少编译依赖的效果。
使用Pimpl模式,开发者可以将类的具体实现封装在一个单独的类中,并通过指针或智能指针来访问它。这样一来,外部代码只需要包含“接口”类的头文件,而不需要访问实现的详细信息,从而实现了信息隐藏的效果。
Pimpl模式还带来了其他一些好处,例如:
- 减少编译依赖:由于实现细节被隐藏在单独的类中,因此只有当实现发生更改时,才需要重新编译与其相关的代码。
- 提高二进制兼容性:对于库开发者而言,使用Pimpl可以避免在公共头文件中引入私有成员变量,从而提高库的二进制兼容性。
- 提高代码可读性:将实现细节封装在一个单独的类中,可以使接口类更加清晰和易于理解。
总而言之,Pimpl模式可以用于隐藏C++类的实现细节,从而实现封装、减少编译依赖和提高代码可读性等效果。它是一个常用的C++设计模式,可以帮助开发者提高代码质量和可维护性。
2 原理解释
2.1 减少编译依赖
Pimpl(Pointer to Implementation)模式的一个主要优点是可以减少头文件的依赖性。在传统的 C++ 编程中,如果我们需要访问一个类的成员变量或者函数,通常需要包含这个类的头文件。但是,如果这个头文件发生了变化,那么所有依赖它的代码都需要重新编译。
Pimpl 模式通过将类的实现细节封装在一个单独的类中,从而避免了这种依赖关系。在使用 Pimpl 模式时,我们只需要在头文件中包含一个指向实现类的指针,而不需要包含实现类的定义。然后,在源文件中包含实现类的定义,并在构造函数和析构函数中进行初始化和清理。
这种方式的实现背后涉及到了一些编译原理。在使用 Pimpl 模式时,我们需要遵循以下步骤:
- 在头文件中声明类,并将其成员变量定义为指向实现类的指针。
- 在头文件中仅包含实现类的前向声明(forward declaration),而不包含实现类的定义。
- 在源文件中包含实现类的定义,并在实现类的构造函数和析构函数中进行初始化和清理。
- 在源文件中包含头文件,并实现头文件中声明的成员函数。
这种方式的实现原理是,由于头文件中只包含了实现类的前向声明,因此编译器在编译头文件时并不需要知道实现类的具体定义。因此,所有依赖这个头文件的代码都不需要重新编译。
而在源文件中,我们包含了实现类的定义,并在构造函数和析构函数中进行初始化和清理。由于这些代码只在源文件中出现,因此不会影响其他文件的编译。
2.2 二进制兼容
Pimpl(Pointer to Implementation)模式可以帮助解决二进制兼容性问题。当使用 Pimpl 模式时,类的公共接口被放置在头文件中,而私有实现细节则被封装在实现类中。这种分离可以有效地隔离公共接口和私有实现之间的依赖关系,从而减少对实现细节的直接访问。
通过这种方式,当需要修改类的私有实现细节时,可以避免对头文件进行任何更改。这样,只需重新编译实现类的源文件,而不需要重新编译使用该类的其他代码。因此,Pimpl 模式可以提供二进制兼容性,即在不重新编译客户端代码的情况下,可以更新类的实现细节。
具体而言,当需要对类的实现进行修改时,可以按照以下步骤进行:
- 在实现类中进行修改:根据需求,修改实现类的定义、成员函数或者成员变量等部分。
- 重新编译实现类的源文件:由于只修改了源文件,而没有修改头文件,因此只需要重新编译实现类的源文件即可生成新的目标文件。
- 生成新的库文件:将新生成的目标文件与原来的库文件进行链接,生成新的库文件。
- 使用新的库文件:用新的库文件替换原来的库文件,而无需重新编译使用该类的客户端代码。
通过这种方式,Pimpl 模式可以实现二进制兼容性。因为客户端代码只依赖于类的公共接口,而不直接依赖于实现细节,所以只要接口保持兼容,就可以无缝地切换到新的库文件,而不需要重新编译客户端代码
3 示例
当我们使用 Pimpl 时,我们需要将类的实现细节封装在一个单独的类中,并通过指针或智能指针来访问它。下面是一个使用 Pimpl 的示例:
// widget.h
#include <memory> // include the header for std::unique_ptr
class WidgetImpl;
class Widget {
public:
Widget(); // constructor
~Widget(); // destructor
public:
void DoSomething();
private:
std::unique_ptr<WidgetImpl> pImpl; // pointer to implementation
};
// widget.cpp
#include "widget.h"
// declare the implementation class
class WidgetImpl {
public:
void DoSomething();();
};
// implementation of Widget constructor
Widget::Widget() : pImpl(std::make_unique<WidgetImpl>()) {}
// implementation of Widget destructor
Widget::~Widget() = default;
// implementation of Widget doSomething method
void Widget::DoSomething() {
pImpl->DoSomething();
}
// implementation of WidgetImpl doSomething method
void WidgetImpl::DoSomething() {
// implementation details
}
在上面的示例中,我们将 Widget 类的实现细节封装在 WidgetImpl 类中。在 Widget 类中,我们使用 std::unique_ptr 来持有 WidgetImpl 实例的指针,并在 Widget 的构造函数中通过 std::make_unique 创建该实例。这样一来,Widget 类就可以通过 pImpl 指针来访问 WidgetImpl 的实现细节,从而实现了信息隐藏的效果。
在 Widget::DoSomething 方法中,我们直接调用 pImpl->DoSomething() 来执行实际的操作。这样一来,Widget 类的用户只需要包含 Widget 类的头文件即可使用它,而不需要知道 WidgetImpl 的实现细节。
总而言之,上述示例展示了如何使用 Pimpl 模式来隐藏 C++ 类的实现细节,从而实现封装和信息隐藏的效果。
如果没有WidgetImpl,Widget将充斥一堆实现细节,例如私有成员变量、私有成员函数等不想对外暴露的信息。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!