PIMPL:隐藏接口的实现细节

小咪咪 2022-12-13 12:42 295阅读 0赞

前言

有时候我们需要提供对外的API,通常会以头文件的形式提供。举个简单的例子:
提供一个从某个指定数开始打印的接口,头文件内容如下:

  1. //来源:公众号编程珠玑
  2. //作者:守望先生
  3. #ifndef _TEST_API_H
  4. #define _TEST_API_H
  5. //test_api.h
  6. class TestApi{
  7. public:
  8. TestApi(int s):start(s){}
  9. void TestPrint(int num);
  10. private:
  11. int start_ = 0;
  12. };
  13. #endif //_TEST_API_H

实现文件如下:

  1. //来源:公众号编程珠玑
  2. //作者:守望先生
  3. #include "test_api.h"
  4. #include <iostream>
  5. //test_api.cc
  6. TestApi::TestPrint(int num){
  7. for(int i = start_; i < num; i++){
  8. std::cout<< i <<std::endl;
  9. }
  10. }

类TestApi中有一个私有变量start_,头文件中是可以看到的。

  1. #include "test_api.h"
  2. int main(){
  3. TestApi test_api{10};
  4. test_api.TestPrint(15);
  5. return 0;
  6. }

常规实现缺点

从前面的内容来看, 一切都还正常,但是有什么问题呢?

  • 头文件暴露了私有成员
  • 实现与接口耦合
  • 编译耦合

第一点可以很明显的看出来,其中的私有变量star_能否在头文件中看到,如果实现越来越复杂,这里可能也会出现更多的私有变量。有人可能会问,私有变量外部也不能访问,暴露又何妨?

不过你只是提供几个接口,给别人看到这么多信息干啥呢?这样就会导致实现和接口耦合在了一起。

另外一方面,如果有另外一个库使用了这个库,而你的这个库实现变了,头文件就会变,而头文件一旦变动,就需要所有使用了这个库的程序都要重新编译!

这个代价是巨大的。

所以,我们应该尽可能地保证头文件不变动,或者说,尽可能隐藏实现,隐藏私有变量。

PIMPL

Pointer to implementation,由指针指向实现,而不过多暴露细节。废话不多说,上代码:

  1. //来源:公众号编程珠玑
  2. //作者:守望先生
  3. #ifndef _TEST_API_H
  4. #define _TEST_API_H
  5. #include <memory>
  6. //test_api.h
  7. class TestApi{
  8. public:
  9. TestApi(int s);
  10. ~TestApi();
  11. void TestPrint(int num);
  12. private:
  13. class TestImpl;
  14. std::unique_ptr<TestImpl> test_impl_;
  15. };
  16. #endif //_TEST_API_H

从这个头文件中,我们可以看到:

  • 实现都在TestImpl中,因为只有一个私有的TestImpl变量,可以预见到,实现变化时,这个头文件是基本不需要动的
  • test_impl_是一个unique_ptr,因为我们使用的是现代C++,这里需要注意的一点是,它的构造函数和析构函数必须存在,否则会有编译问题。

我们再来看下具体的实现:

  1. //来源:公众号编程珠玑
  2. //作者:守望先生
  3. #include "test_api.h"
  4. #include <iostream>
  5. //test_api.cc
  6. class TestApi::TestImpl{
  7. public:
  8. void TestPrint(int num);
  9. TestImpl(int s):start_(s){}
  10. TestImpl() = default;
  11. ~TestImpl() = default;
  12. private:
  13. int start_;
  14. };
  15. void TestApi::TestImpl::TestPrint(int num){
  16. for(int i = start_; i < num; i++){
  17. std::cout<< i <<std::endl;
  18. }
  19. }
  20. TestApi::TestApi(int s){
  21. test_impl_.reset(new TestImpl(s));
  22. }
  23. void TestApi::TestPrint(int num){
  24. test_impl_->TestPrint(num);
  25. }
  26. //注意,析构函数需要
  27. TestApi::~TestApi() = default;

从实现中看到,TestApi中的TestPrint调用了TestImpl中的TestPrint实现,而所有的具体实现细节和私有变量都在TestImpl中,即便实现变更了,其他库不需要重新编译,而仅仅是在生成可执行文件时重新链接。

总结

从例子中,我们可以看到PIMPL模式中有以下优点:

  • 降低耦合,头文件稳定,类具体实现改变不影响其他模块的编译,只影响可执行程序的链接
  • 接口和实现分离,提高稳定性

当然了,由于实现在另外一个类中,所以会多一次调用,会有性能的损耗,但是这点几乎可以忽略。

ebd884b9359263e715b8ca03bcadfc08.png

相关精彩推荐

4dfd3025c0869da982a9a49111f27fdc.png

为何优先选用unique_ptr而不是裸指针?

认真理一理C++的构造函数

这才是现代C++单例模式简单又安全的实现

关注公众号【编程珠玑】,获取更多Linux/C/C++/数据结构与算法/计算机基础/工具等原创技术文章。后台免费获取经典电子书和视频资源

5b83b1001f804ff9f321f98aa8cc15a2.png

发表评论

表情:
评论列表 (有 0 条评论,295人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Java接口实现细节与问题案例

    Java接口(Interface)是一种完全抽象的类,它可以用来定义一组方法规范,这些方法规范可以被不同的类实现。接口是Java中实现多态的一种机制,它允许不同的类以统一的方式

    相关 SortedSet接口细节

    1.SortedSet接口   继承了Set接口, 基本类型加String类型放到排序的集合中,可以不用写实现Comparable接口,但是自定义类类型就必须实现这个接口,不

    相关 set接口细节

    1.set集合,不能加入重复的元素,而且是无序的,null只能有一个 2.set接口的子类,HashSet,TreeSet 3.set接口没有提供get()方法 ![70