我们还是从一个例子开始出发:
class Person
{
public:
Person(const std::string& name,const Date& birthday,const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
...
private:
std::string theName;
Date theBirthDate;
Address theAddress;
};
我们加上需要的头文件就可以通过编译了:
#include <string>
#include "date.h"
#include "address.h"
但是不幸的是,这么一来便在Person定义文件和其含入文件之间形成了一种编译依存关系。如果这些头文件中有一个被改变或者头文件所依赖的文件被改变,那么每一个含有Person class的文件都得重新编译。这样的编译会对许多项目造成难以形容的灾难。
一种解决办法是这么做:
namespace std{
class string;//前置声明
}
class Date; //...
class Address //...
class Person{
public:
Person(const std::string& name,const Date& birthday,const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
};
如果这么做Person客户端只需要在Person接口被修改过时才重新编译。但是这个想法存在两个问题。第一,string 不是个class,它是个typedef(basic_string<char>)。因为是标准库所以这个问题可以解决,#include就可以了。
关于前置声明的第二问题是,编译器必须知道编译期间对象的大小。考虑这个:
int main()
{
int x;
Person p(params);
...
}
编译器看到x就知道分配一个int大。编译器看到p时也需要知道Person对象的大小,所以它需要询问class的定义式。此问题在JAVA语言中将被看成下面这样:
int main()
{
int x;
Person* p;
...
}
所以我们也可以玩玩同样的游戏:把Person分成两个class,一个提供接口,一个负责实现该接口(PersonImpl)。
#include <string>
#include <memory>
class PersonImpl;
class Date;
class Address;
class Person{
public:
Person(const std::string& name,const Date& birthday,const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
...
private:
std::tr1::shared_ptr<PersonImpl> pImpl;
}
这里Person class只有一个成员指针,指向其实现类。这样就实现了接口与实现的完全分离,那些class的任何实现的修改都不需要Person客户端重新编译。这种分离的关键在于以“声明的依赖性”替换为“定义的依赖性”。这里有一些策略:
1.如果使用对象的指针或者引用完成,就不要使用对象。
2.如果能够,尽量以class的声明式替换class的定义式
3.为声明式和定义式提供不同的头文件。为了严守规则需要两个头文件,一个用于声明式一个用于定义式。当然他们得保持一致性。因此程序库客户应该总是#include一个声明文件而非前置若干函数。例如:Date的客户希望声明today和clearAppointments,不应该像先前手工的前置声明Date,而是应该#include:
#include "datefwd.h"
Date today();
void clearAppointments(Date d);
像Person这样使用实现的class被称为Handle classes,下面是Person两个成员函数的实现:
#include "Person.h"
#include "PersonImpl.h"
Person::Person(const std::string& name,const Date& birthday,const Address& addr)
:pImpl(new PersonImpl(name,birthday,addr))
{ }
std::string Perosn::name() const
{
return pImpl->name();
}
另外一种方法是Person变成一个抽象类:
class Person{
public:
virtual ~Person() = 0;
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
virtual std::string Address() const = 0;
...
};
Perosn class 的客户必须有办法为这种class创建新对象,他们通常这样实现:
static:
class Person{
public:
...
static std::tr1::shared_ptr<Person>
create(const std::string& name,const Date& birthday,const Address& addr);
...
};
客户这样使用:
std::string name;
Date dateOfBirth;
Address address;
...
std::tr1::shared_ptr<Person>pp(Person::create(name,dateOfBirth,address));
...
std::cout<<pp->name().....;
假设:
class RealPerson:public Person
{
public:
RealPerson(const std::string& name,const Date& birthday,const Address& addr)
:theName(name),theBirthDate(birthday),theAddress(addr){ }
virtual ~RealPerson();
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::string theName;
Date theBirthDate;
Address theAddress;
};
有了RealPerson之后就可以写出Person::create()了:
std::tr1::shared_ptr<Person>Person::create(const std::string& name,
const Date& birthday,const Address& addr)
{
return std::tr1::shared_ptr<Person>(new RealPerson(name,birthday,addr))
}
RealPerson 示范实现接口类最常用的机制之一:从接口类继承接口规格,然后实现出接口所覆盖的函数。