1.如果一个源文件以头文件的形式包含了其他文件,则它们之间便形成了一种“编译依存关系”;一旦被包含的文件(或这些文件包含的其他文件)被修改,则每一个包含该源文件的其他文件都必须重新编译。
以“前置声明”代替“头文件包含”似乎是一个好办法,但是会遇到一些问题:
- 对诸如string这样的头文件而言,正确的前置声明比较复杂,因为会涉及模板(string是个typedef而非类)。事实上,前置声明标准库并没有必要,直接包含这些文件并不会造成太大的编译依赖性问题。
- 在编译期间,编译器必须知道对象的大小,从而必须访问它的定义式,从而使得该类必须列出实现细节。
2.支持“编译依存性最小化”的一般构想是:依赖于声明式,而非定义式。基本做法是:
- 如果使用对象的引用或者指针可以完成任务,就不要使用对象本身。
- 如果能够,尽量以类的声明式替换类的定义式:如果声明一个函数需要用到某个类(作为返回值类型或参数类型)时,并不需要它的定义式(虽然传值方式效率低下)。如果能够将“提供类的定义式”(通过#include完成)的必要性从“函数声明所在”的头文件转移到“内含函数调用”的客户文件,便可去除“并非真正必要的类型定义”与客户端之间的编译依存性去除掉。
- 为声明式和定义式提供不同的头文件:为促进严守上述准则,需要定义两个头文件:一个用于声明式,一个用于定义式。如下:
#include "datefwd.h" //该头文件内声明但未定义Date类,这样就不需要做前置声明了
Date today();
void clearAppointments(Date d);
类似datefwd的命名方式取自STL中的<iosfwd>。这同时说明本条款同样适用于模板类(这可能需要构建环境的支持)。
注:C++中提供关键字export允许模板声明式和定义式分别处于不同的文件中,但是支持的编译器很少。
3.基于此构想的两个手段是使用Handle class和Inferface class。
4.代码优化过程:比如以下项目,包含的头文件中具有完整的定义式:
#include <string>
#include "date.h"
#include "address.h"
class Person{
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthday() const;
std::string address() const;
...
private:
std::string theName;
Date theBirthDate;
Address theAddress;
};
这样编译依赖关系就十分紧密。一种思路是用前置声明方法:
namespace std{
class string; //string是个typedef:basic_string<char>,该前置声明并不正确,事实上对STL中的类做前置声明也没有必要,直接包含就可以了
}
class Date;
class Address;
class Person{
...
};
可以使用pimpl idiom(以指针指向实现)方式加以改进,可以使“接口与实现相分离”:
#include <string>
#include <memory> //STL不应当用于前置声明。此声明用于智能指针
class PersonImpl; //Person的实现类的前置声明
class Date;
class Address;
class Person{
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birtyday() const;
std::string address() const;
...
private:
std::tr1::shared_ptr<PersonImpl> pImpl; //智能指针指向实现物
这样的Person类称为handle class(句柄类?),它有两种使用方法:
(1)将所有函数转交给相应的实现类并由后者完成实际工作,如下:
#include "Person.h" //实现Person类,因此必须包含定义式
#include "PersonImpl.h" //同时包含PersonImpl的定义式,否则无法调用其成员函数。
//注意:两个类具有完全相同的成员函数,接口完全相同
Person::Person(const std::string& name, const Date& birthday, const Address& addr)
: pImpl(new PersonImpl(name, birthday, addr)) //调用PersonImpl的构造函数
{}
std::string Person::name() const
{
return pImpl->name();
}
(2)使handle class成为一种特殊的抽象基类,即接口类,这种类的目的是描述派生类的接口,通常不含有成员变量,也没有构造函数,只有一个虚析构函数以及一组纯虚函数,用来描述整个接口。可以将Person类写成这种接口类:
class Person{
public:
virtual ~Person();
virtual std::string name() const = 0;
virtual std::string birtyday() const = 0;
virtual std::string address() const = 0;
...
static std::tr1::shared_ptr<Person> create(const std::string& name, const Date& birthday, const Address& addr); //返回指针指向一个动态分配的Person“对象”
};
除非接口类的接口被修改,否则不需要重新编译。使用接口类时用引用或指针编写程序,通常的手段是使用工厂函数或虚构造函数,返回指向该类型的指针(或更为可取的智能指针)。
std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address)); //创建对象。注意静态函数的用法
注意支持该接口的派生类必须定义出来,否则没有构造函数可以被调用,也就没有真正的对象产生了。
5.实现接口类的两个最常见机制:
- 从接口类继承接口,然后实现这些接口。
- 多重继承。