先看一个例子:
class Person
{
public:
Person(const string& nm ,Date d):name(nm),birthday(d){}
void getBirthday()
{
cout<<birthday.getYear()<<"."<<birthday.getMonth()<<"."<<birthday.getDay()<<endl;
}
private:
string name;
Date birthday;
};
但是要想使这个类的定义编译通过,那么必须给出它的string 和Date 这两种类型的定义(不是声明)。这意味着,假如Date类里面的东西变了,那么Person也需要重新编译。这使得定义文件和他包含的文件之间形成了一种编译依赖关系。为什么这里不能像函数一样,只需要声明,而不需要定义呢?主要原因是因为编译器在定义一个类的对象时必须知道他的确切大小,要想知道Person的大小,必须知道name和birthday的大小。要想知道他们的的大小,只能通过他们的定义了。
这种问题在Java中并不存在,因为Java中定义一个对象,其实就是拿到了指向这个对象的指针而已,而指针的大小却是固定的。所以就没有上面的问题了。于是,我们可以模仿这个原理,写出:
class Date;
class String;
class PersonImpl;
class Peason
{
public:
void getBirthday();
private:
std::tr1::shared_ptr<PersonImpl> pImpl;
};
这里这几个类都没有定义,但是Peason类已经可以定义出来了。这说明,Peason类的实现已经于其他几个类完全分离了。至此,如果修改了其他几个类的定义,那么Peason并不需要重新编译。这个分离的本质在于,以声明的存在性替换定义的存在性:即让头文件尽可能自我满足,如果做不到,让他与其他文件内的声明式(而非定义式)相依,由此我们可以得出几个重要的准则:
1.若果使用对象的引用或对象指针可以完成任务,那么就不要使用对象。因为对象的引用和指针时,只用了类的名字,而不需要它的定义,而使用对象时必须要有对象的定义。
2.尽量用类声明替换类定义。比如声明一个函数时,函数的形参,返回值是一个类时,只要求类的声明就好了。但是在调用函数前,必须知道这些类的定义。
3.为声明式和定义事提供不同的头文件。你的程序只需要类的声明就能完成,那么就让他include类声明的头文件。这个思想来源于C++标准库。
下面举一个例子:
//data.h
class Date
{
public:
Date(int d, int m, int y):day(d),month(m),year(y){}
int getDay(){return day;}
int getMonth(){return month;}
int getYear(){return year;}
Date(const Date& date):day(date.day),month(date.month),year(date.year){}
private:
int day;
int month;
int year;
};
//personIplm.h
#include "date.h"
class PersonImpl
{
public:
PersonImpl(Date d):birthday(d){}
Date& getBirthday()
{
return birthday;
}
private:
Date birthday;
};
//person.h
class Date;
class PersonImpl;
class Person
{
public:
Person(Date d);
void getBirthday();
private:
std::tr1::shared_ptr<PersonImpl> pImpl;
};
//person.cpp
Person::Person(Date d):pImpl(new PersonImpl(d)){}
void Person::getBirthday()
{
cout<<pImpl->getBirthday().getYear()<<"."
<<pImpl->getBirthday().getMonth()<<"."
<<pImpl->getBirthday().getDay()<<endl;
}
//main.cpp
int main()
{
Person p(Date(12,9,2012));
p.getBirthday();
return 0;
}
其中Person的定义是不需要其他头文件的,只是在前面声明了其他类就可以了。而Person的函数却实际需要这些这些数据结构,所以它里里含“”了"personIplm.h"和"person.h"。像这种Person中使用pimpl技术的类称为句柄类。
另外一种方法是通过抽象类来实现:
//person.h
class Date;
//接口类:抽象基类描述派生类接口
//不带数据成员
class Person
{
public:
//工厂函数,返回指向这个类的指针
static std::tr1::shared_ptr<Person> creat(const Date& d);
virtual Date getBirthday() const = 0;
};
//date.h
class Date
{
public:
Date(int d, int m, int y):day(d),month(m),year(y){}
int getDay(){return day;}
int getMonth(){return month;}
int getYear(){return year;}
Date(const Date& date):day(date.day),month(date.month),year(date.year){}
private:
int day;
int month;
int year;
};
//realperson.h
#include "date.h"
#include "person.h"
//具体的类
class RealPerson:public Person
{
public:
RealPerson(const Date& d):birthday(d){}
virtual ~RealPerson(){}
Date getBirthday()const
{
return birthday;
}
private:
Date birthday;
};
//person.cpp
#include "realPerson.h"
std::tr1::shared_ptr<Person> Person::creat(const Date& d)
{
return std::tr1::shared_ptr<Person>(new RealPerson(d));
}
这样以来就解除了接口和实现之间的耦合关系,降低了编译的依存性。
总之,编译依存性最小化的一般构想是依赖于声明,而不依赖于定义。基于此构想的两个手段是:句柄类和接口类。