关于头文件的编译,在我另一篇转载的博客中有很详细的说明,在这里不再多言。
现在直接比较以下两种类的实现方式:
//头文件1 #include <string> #include "date.h" #include "address.h" using namespace std; class Person{ public: Person(const std::string& name,const Date& birthday,const Address& addr); string name()const; string birthDate()const; string address()const; ... private: string theName; Date theBirthDate; Address theAddress; }; //头文件2 #include <string> //注意,这里的string不是class,它本身其实是一个宏定义 //对C++标准库当中定义的类或函数在了解有限的情况下,个人认为不应当随意继承或更改 class Date; class Address; using namespace std; class Person{ public: Person(const std::string& name,const Date& birthday,const Address& addr); string name()const; string birthDate()const; string address()const; ... private: string theName; Date theBirthDate; Address theAddress; };
以上两个头文件,其类定义是完全相同的,不同之处在于类的声明方式,头文件1中直接在类定义包含进头文件中,头文件2则使用了前置声明的方式。对于这两种方式前者是优于后者的,原因如下:
因为采用#include方式包含其实类似于宏展开,把一个头文件里的东西copy到另一个文件当中,的确,头文件不会直接参与编译,但头文件要不要编译呢?答案是肯定的,不编译的话如果有错误谁负责?头文件的编译其实是在包含它的源文件当中,用于IDE的都知道,一旦源文件当中的某一个字符更改了,那源文件就会重新编译。一旦Date或Address中的接口或数据成员改变,那包含它的所有源文件就要重新编译。而头文件2中的方式则不会引起这种效果,因为头文件2中只有声明,声明只是告诉编译器这个符号的存在。
这就是所谓的编译依存关系,一个头文件更改会让所有包含它的源文件重新编译。
那头文件2是否就完美了呢?其实不然,当class Person的接口或成员变量改变的时候,包含头文件2的源文件同样需要重新编译。有什么办法可以进一步降低编译依存关系呢?当我只想改变Person的实现或Person的成员变量而不改变接口的时候,如何避免包含头文件2的源文件重新编译?
//头文件3 #include <string> class Date; class Address; class PersonImpl; using namespace std; class Person{ public: Person(const std::string& name,const Date& birthday,const Address& addr); string name()const; string birthDate()const; string address()const; ... private: std::tr1::shared_ptr<PersonImpl> pImpl; }; //头文件3对应的源文件 #include "Person.h" #include "PersonImpl.h" Person::Person(const std::string& name,const Date& birthday, const Address& addr) :pImpl(new PersonImpl(name,birthday,addr)) { } string Person::name()const { return pImpl->name(); }
Person和PersonImpl的内容几乎是一模一样的,我把PersonImpl理解为Person的影子,Person的所有操作都是调用PersonImpl的对应操作,会包含#include"PersonImpl"的源文件有且仅有Person类,PersonImpl的改变仅仅只会影响到Person的源文件。Person只含一个PersonImpl的指针,指针或引用的定义只需要相应类的声明式,而定义式不是必须的。只要PersonImpl的接口不变,那么Person的接口也不必改变,Person不改变,那就不会引起编译依存效应。
这种编程技法叫pimpl idion (pimpl 是“point to implementation”)的缩写。接口与实现完全分离!
另一种方法是采用抽象类的方式实现,后绪补充!
本文所有内容均来自《Effective C++》!