有时候我们需要提供对外的API,通常会以头文件的形式提供。举个简单的例子: 提供一个从某个指定数开始打印的接口,头文件内容如下:
//来源:公众号编程珠玑
//作者:守望先生
#ifndef _TEST_API_H
#define _TEST_API_H
//test_api.h
class TestApi{
public:
TestApi(int s):start(s){}
void TestPrint(int num);
private:
int start_ = 0;
};
#endif //_TEST_API_H
实现文件如下:
//来源:公众号编程珠玑
//作者:守望先生
#include "test_api.h"
#include <iostream>
//test_api.cc
TestApi::TestPrint(int num){
for(int i = start_; i < num; i++){
std::cout<< i <<std::endl;
}
}
类TestApi中有一个私有变量start_,头文件中是可以看到的。
#include "test_api.h"
int main(){
TestApi test_api{10};
test_api.TestPrint(15);
return 0;
}
从前面的内容来看, 一切都还正常,但是有什么问题呢?
第一点可以很明显的看出来,其中的私有变量star_能否在头文件中看到,如果实现越来越复杂,这里可能也会出现更多的私有变量。有人可能会问,私有变量外部也不能访问,暴露又何妨?
不过你只是提供几个接口,给别人看到这么多信息干啥呢?这样就会导致实现和接口耦合在了一起。
另外一方面,如果有另外一个库使用了这个库,而你的这个库实现变了,头文件就会变,而头文件一旦变动,就需要所有使用了这个库的程序都要重新编译!
这个代价是巨大的。
所以,我们应该尽可能地保证头文件不变动,或者说,尽可能隐藏实现,隐藏私有变量。
Pointer to implementation,由指针指向实现,而不过多暴露细节。废话不多说,上代码:
//来源:公众号编程珠玑
//作者:守望先生
#ifndef _TEST_API_H
#define _TEST_API_H
#include <memory>
//test_api.h
class TestApi{
public:
TestApi(int s);
~TestApi();
void TestPrint(int num);
private:
class TestImpl;
std::unique_ptr<TestImpl> test_impl_;
};
#endif //_TEST_API_H
从这个头文件中,我们可以看到:
我们再来看下具体的实现:
//来源:公众号编程珠玑
//作者:守望先生
#include "test_api.h"
#include <iostream>
//test_api.cc
class TestApi::TestImpl{
public:
void TestPrint(int num);
TestImpl(int s):start_(s){}
TestImpl() = default;
~TestImpl() = default;
private:
int start_;
};
void TestApi::TestImpl::TestPrint(int num){
for(int i = start_; i < num; i++){
std::cout<< i <<std::endl;
}
}
TestApi::TestApi(int s){
test_impl_.reset(new TestImpl(s));
}
void TestApi::TestPrint(int num){
test_impl_->TestPrint(num);
}
//注意,析构函数需要
TestApi::~TestApi() = default;
从实现中看到,TestApi中的TestPrint调用了TestImpl中的TestPrint实现,而所有的具体实现细节和私有变量都在TestImpl中,即便实现变更了,其他库不需要重新编译,而仅仅是在生成可执行文件时重新链接。
从例子中,我们可以看到PIMPL模式中有以下优点:
当然了,由于实现在另外一个类中,所以会多一次调用,会有性能的损耗,但是这点几乎可以忽略。