C/C++中的PIMPL机制
我们平时在编写C/C++程序时我们都会在源文件(后缀为.c或.cpp的文件)中包含头文件,当头文件内容发生改变时,包含其的源文件在编译时也需要重新编译,也就是现在假如有如下关系图:
这里的N个cpp文件都包含了A.h,当每次修改A.h内容,再次编译时这里的N个cpp文件无论内容是否改变都将重新编译!这时就好比是牵一发而动全身,会导致编译效率低下;
那么该如何解决呢?也就是如何才能做到既能兼顾头文件内容的可扩展兼容性,又能去掉那些重复无效的编译(编译那些无需再次编译的源文件即内容并未发生改变的源文件)。这就是我们今天的主角——PIMPL机制(一种利用指针实现的数据私有化可扩展兼容机制)。我们先来看一个没有引入PIMPL机制的情况,测试代码如下:
person.h
#pragma once
class Person
{
public:
void setAge(unsigned short age);
unsigned short getAge();
private:
#ifdef WIN32
uint16 m_age;
// int m_addTest;
#else
unsigned short m_age;
#endif
};
person.cpp
#include "person.h"
void Person::setAge(unsigned short age)
{
m_age = age;
}
unsigned short Person::getAge()
{
return m_age;
}
man.h
pragma once
#include "person.h"
class Man : public Person
{
public:
void showInfo();
};
man.cpp
#include "man.h"
#include <iostream>
void Man::showInfo()
{
std::cout << getAge() << std::endl;
}
xuedaon.cpp
#include "man.h"
int main()
{
Man rose;
rose.setAge(18);
rose.showInfo();
return 0;
}
我们再编写一个Makefile文件来模拟IDE的编译过程
Makefile
FILES += person.o man.o xuedaon.o
INCLUDE += person.h man.h
APP = test
default:$(APP)
person.o:person.cpp $(INCLUDE)
g++ -c person.cpp
man.o:man.cpp $(INCLUDE)
g++ -c man.cpp
xuedaon.o:xuedaon.cpp $(INCLUDE)
g++ -c xuedaon.cpp
$(APP):$(FILES)
g++ $(FILES) -o $(APP)
clean:
rm -rf $(FILES) $(APP)
当我们执行make时效果如下:
此时若我们没做任何文件修改若直接再次执行make效果则会是:
当我们将头文件person.h中注释的测试成员取消注释:
后再执行make的效果:
此时我们会发现所有的.cpp源文件又都重新被编译,但他们都未做任何更改,那这些编译目前来说都是无效的没有必要的编译,这里增加的属性就好比是针对于windows环境做出的调整改变,本质对当前环境是没有任何影响的,当前环境下也不需要去关心这个变化,但是它却影响了当前环境下的编译,这样肯定是不太好的,所以我们就可以利用PIMPL机制来改进,即在原类对象中提供一个用于存储数据的私有对象指针,将类对象中原有的私有数据成员放入在源文件在定义的私有对象中还可以定义相关访问接口便于原对象调用,具体简单改进后代码如下:
person.h
#pragma once
#include <iostream>
#include <memory>
class Person
{
public:
Person();
void setAge(unsigned short age);
unsigned short getAge();
private:
struct PrivatePersonData; // 提供一个私有的用于存储数据的对象
std::shared_ptr<PrivatePersonData> m_pData; //利用指向私有数据对象的智能指针提供数据操作接口, 在C语言中就可以直接定义为普通指针,这里需注意的是由于PrivatePersonData对象在头文件这里是没有定义的,若使用unique_ptr将无法正常创建m_pData智能指针对象,具体改进方法就是先在头文件中定义一个私有对象的抽象父类,用该父类去定义智能指针即可
};
person.cpp
include "person.h"
struct Person::PrivatePersonData //在源文件中定义私有数据对象,这样该对象中的成员发生任何改变就只与当前文件有关
{
#ifdef WIN32
uint16 m_age;
// int m_addTest;
#else
unsigned short m_age;
#endif
};
Person::Person()
{
m_pData = std::shared_ptr<PrivatePersonData>(new PrivatePersonData); //利用智能指针实例化一个私有数据对象
}
void Person::setAge(unsigned short age)
{
m_pData->m_age = age;
}
unsigned short Person::getAge()
{
return m_pData->m_age;
}
此时当我们第一次执行make时效果与之前一样会编译所有源文件:
但当我们在私有数据对象中改变时(将现在PrivatePersonData中int m_addTest;注释去除)再make时的效果:
此时我们可以看出与修改后无关联的源代码(man.cpp与xuedaon.cpp)并没有再次重新编译,这样就既能兼顾头文件内容的可扩展兼容性,又能去掉那些重复无效的编译,并且还相当于对外隐藏了Person类中原有的数据成员,防止代码的盗用;在Qt中就是利用在源文件中定义私有对象配合定义Q_DECLARE_PRIVATE(定义获取私有对象的指针的接口函数和声明私有对象)与Q_D宏定义(调用接口获取私有对象的指针)实现的PIPML机制,有兴趣的可以看看下面的源代码:
//使用普通类型指针时调用的接口,用于返回当前私有对象的指针
template <typename T>
static inline T *getPtr(T *ptr) {
return ptr;
}
//使用智能指针类对象时调用的接口,作用同上
template <typename Wrapper>
static inline typename Wrapper::pointer getPtr(const Wrapper &p) {
return p.get(); // get()是unique_ptr中的方法,作用是获取智能管理指针
}
#define Q_DECLARE_PRIVATE(Class) \
inline Class##Private *d_func() { \
return reinterpret_cast<Class##Private *>(getPtr(d_ptr));} \
inline const Class##Private *d_func() const { \
return reinterpret_cast<const Class##Private *>(getPtr(d_ptr));} \
friend class Class##Private
#define Q_D(Class) Class##Private *const dp = d_func()
希望上面的介绍能对大家理解PIMPL机制有所帮助!祝大家在学习的道路上能更上一层楼。