面向对象软件设计于分析(26)C++ IMPL 模式详解

2023-12-16 20:50:08

1 概念

Pimpl是C++中一种设计模式,全称为Pointer to Implementation,它可以用于隐藏类的实现细节,从而实现封装和减少编译依赖的效果。

使用Pimpl模式,开发者可以将类的具体实现封装在一个单独的类中,并通过指针或智能指针来访问它。这样一来,外部代码只需要包含“接口”类的头文件,而不需要访问实现的详细信息,从而实现了信息隐藏的效果。

Pimpl模式还带来了其他一些好处,例如:

  • 减少编译依赖:由于实现细节被隐藏在单独的类中,因此只有当实现发生更改时,才需要重新编译与其相关的代码。
  • 提高二进制兼容性:对于库开发者而言,使用Pimpl可以避免在公共头文件中引入私有成员变量,从而提高库的二进制兼容性。
  • 提高代码可读性:将实现细节封装在一个单独的类中,可以使接口类更加清晰和易于理解。

总而言之,Pimpl模式可以用于隐藏C++类的实现细节,从而实现封装、减少编译依赖和提高代码可读性等效果。它是一个常用的C++设计模式,可以帮助开发者提高代码质量和可维护性。

2 原理解释

2.1 减少编译依赖

Pimpl(Pointer to Implementation)模式的一个主要优点是可以减少头文件的依赖性。在传统的 C++ 编程中,如果我们需要访问一个类的成员变量或者函数,通常需要包含这个类的头文件。但是,如果这个头文件发生了变化,那么所有依赖它的代码都需要重新编译。

Pimpl 模式通过将类的实现细节封装在一个单独的类中,从而避免了这种依赖关系。在使用 Pimpl 模式时,我们只需要在头文件中包含一个指向实现类的指针,而不需要包含实现类的定义。然后,在源文件中包含实现类的定义,并在构造函数和析构函数中进行初始化和清理。

这种方式的实现背后涉及到了一些编译原理。在使用 Pimpl 模式时,我们需要遵循以下步骤:

  1. 在头文件中声明类,并将其成员变量定义为指向实现类的指针。
  2. 在头文件中仅包含实现类的前向声明(forward declaration),而不包含实现类的定义。
  3. 在源文件中包含实现类的定义,并在实现类的构造函数和析构函数中进行初始化和清理。
  4. 在源文件中包含头文件,并实现头文件中声明的成员函数。

这种方式的实现原理是,由于头文件中只包含了实现类的前向声明,因此编译器在编译头文件时并不需要知道实现类的具体定义。因此,所有依赖这个头文件的代码都不需要重新编译。

而在源文件中,我们包含了实现类的定义,并在构造函数和析构函数中进行初始化和清理。由于这些代码只在源文件中出现,因此不会影响其他文件的编译。

2.2 二进制兼容

Pimpl(Pointer to Implementation)模式可以帮助解决二进制兼容性问题。当使用 Pimpl 模式时,类的公共接口被放置在头文件中,而私有实现细节则被封装在实现类中。这种分离可以有效地隔离公共接口和私有实现之间的依赖关系,从而减少对实现细节的直接访问。

通过这种方式,当需要修改类的私有实现细节时,可以避免对头文件进行任何更改。这样,只需重新编译实现类的源文件,而不需要重新编译使用该类的其他代码。因此,Pimpl 模式可以提供二进制兼容性,即在不重新编译客户端代码的情况下,可以更新类的实现细节。

具体而言,当需要对类的实现进行修改时,可以按照以下步骤进行:

  1. 在实现类中进行修改:根据需求,修改实现类的定义、成员函数或者成员变量等部分。
  2. 重新编译实现类的源文件:由于只修改了源文件,而没有修改头文件,因此只需要重新编译实现类的源文件即可生成新的目标文件。
  3. 生成新的库文件:将新生成的目标文件与原来的库文件进行链接,生成新的库文件。
  4. 使用新的库文件:用新的库文件替换原来的库文件,而无需重新编译使用该类的客户端代码。

通过这种方式,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将充斥一堆实现细节,例如私有成员变量、私有成员函数等不想对外暴露的信息。

文章来源:https://blog.csdn.net/HandsomeHong/article/details/135037166
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。