PIMPL 是什么
PIMPL = Pointer to Implementation ,也叫 d-pointer、编译期防火墙(compilation firewall) 核心思想:
把类的实现细节全部放到另外一个内部类里,对外暴露的类只保留一个指向实现类的裸指针 / 智能指针。对外头文件只放接口,不放成员变量、私有函数、依赖头文件。
解决什么痛点
- 减少编译依赖 :修改实现
.cpp里的私有成员,不需要重新编译包含这个头文件的其他代码; - 隐藏内部实现:对外头文件看不到私有数据结构,适合做 SDK、闭源库;
- 稳定 ABI:版本迭代时,对外类的内存布局不变,二进制兼容更好;
- 缺点:多一次间接访问、要小心内存管理(优先用
unique_ptr)。
一句话:头文件只留门面,实现藏在 cpp 里面。
C++ 最简示例(现代 C++,用 unique_ptr)
头文件 my_class.h(对外给别人 include 的)
cpp
#ifndef MY_CLASS_H
#define MY_CLASS_H
#include <memory>
#include <string>
// 对外类
class MyClass
{
public:
MyClass();
~MyClass();
// 接口
void setName(const std::string& name);
std::string getName() const;
void doWork();
// 拷贝、移动需要自己声明,因为unique_ptr不可拷贝
MyClass(MyClass&&) noexcept;
MyClass& operator=(MyClass&&) noexcept;
private:
// 前向声明实现类,不写定义!
struct Impl;
std::unique_ptr<Impl> pimpl;
};
#endif
✅ 重点:struct Impl 只是前向声明,头文件里看不到里面有什么,没有任何私有成员变量。
源文件 my_class.cpp(实现,不对外)
cpp
#include "my_class.h"
#include <iostream>
#include <string>
// 在这里才定义真正的实现类,所有私有成员、依赖都放这里
struct MyClass::Impl
{
std::string name;
int internal_data = 0;
void doInternalLogic()
{
internal_data++;
std::cout << "inner logic, data=" << internal_data << "\n";
}
};
MyClass::MyClass() : pimpl(std::make_unique<Impl>())
{}
MyClass::~MyClass() = default;
MyClass::MyClass(MyClass&&) noexcept = default;
MyClass& MyClass::operator=(MyClass&&) noexcept = default;
void MyClass::setName(const std::string& name)
{
pimpl->name = name;
}
std::string MyClass::getName() const
{
return pimpl->name;
}
void MyClass::doWork()
{
pimpl->doInternalLogic();
}
使用方 main.cpp
cpp
#include "my_class.h"
#include <iostream>
int main()
{
MyClass obj;
obj.setName("hello pimpl");
std::cout << obj.getName() << "\n";
obj.doWork();
return 0;
}
关键注意点(踩坑高频)
-
析构函数必须在 cpp 里定义(或者 = default 写在 cpp) 如果你把
~MyClass() = default;写在.h里:编译器在头文件实例化析构时,还不知道Impl的完整类型,unique_ptr析构会编译报错!规则:
unique_ptr<T>要求T在析构点是完整类型。 -
默认拷贝是禁用的
unique_ptr不能拷贝,所以如果你需要拷贝语义,不能直接 default,要自己在 Impl 里面实现深拷贝:cppMyClass::MyClass(const MyClass& rhs) : pimpl(std::make_unique<Impl>(*rhs.pimpl)) {} -
对比不使用 PIMPL 的普通写法: 普通写法:所有私有成员、依赖头文件全部暴露在
.h,一旦私有成员改了,所有 include 这个头的文件全部重编译。