C 箴言:最小化文档之间的编译依赖
2008-02-23 05:24:03来源:互联网 阅读 ()
问题在于 C 没有做好从实现中剥离接口的工作。一个类定义不但指定了一个类的接口而且有相当数量的实现细节。例如:
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; // implementation detail Date theBirthDate; // implementation detail Address theAddress; // implementation detail }; |
在这里,假如不访问 Person 的实现使用到的类,也就是 string,Date 和 Address 的定义,类 Person 就无法编译。这样的定义一般通过 #include 指令提供,所以在定义 Person 类的文档中,您很可能会找到类似这样的东西:
#include <string> #include "date.h" #include "address.h" |
不幸的是,这样就建立了定义 Person 的文档和这些头文档之间的编译依赖关系。假如这些头文档中的一些发生了变化,或这些头文档所依赖的文档发生了变化,包含 Person 类的文档和使用了 Person 的文档相同必须重新编译,这样的层叠编译依赖关系为项目带来数不清的麻烦。
您也许想知道 C 为什么坚持要将一个类的实现细节放在类定义中。例如,您为什么不能这样定义 Person,单独指定这个类的实现细节呢?
namespace std { class string; // forward declaration (an incorrect } // one - see below) class Date; // forward declaration class Address; // forward declaration 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 的客户才必须重新编译。
这个主意有两个问题。第一个,string 不是个类,他是个 typedef (for basic_string<char>)。造成的结果就是,string 的前向声明(forward declaration)是不正确的。正确的前向声明要复杂得多,因为他包括另外的模板。然而,这还不是要紧的,因为您不应该试着手动声明标准库的部件。作为替代,直接使用适当的 #includes 并让他去做。标准头文档不太可能成为编译的瓶颈,特别是在您的构建环境允许您利用预编译头文档时。假如解析标准头文档真的成为一个问题。您也许需要改变您的接口设计,避免使用导致不受欢迎的 #includes 的标准库部件。
第二个(而且更重要的)难点是前向声明的每一件东西必须让编译器在编译期间知道他的对象的大小。考虑:
int main() { int x; // define an int Person p( params ); // define a Person ... } |
当编译器看到 x 的定义,他们知道他们必须为保存一个 int 分配足够的空间(一般是在栈上)。这没什么问题,每一个编译器都知道一个 int 有多大。当编译器看到 p 的定义,他们知道他们必须为一个 Person 分配足够的空间,但是他们怎么推测出一个 Person 对象有多大呢?他们得到这个信息的唯一方法是参考这个类的定义,但是假如一个省略了实现细节的类定义是合法的,编译器怎么知道要分配多大的空间呢? 这个问题在诸如 Smalltalk 和 Java 这样的语言中就不会发生,因为,在这些语言中,当一个类被定义,编译器仅仅为一个指向一个对象的指针分配足够的空间。也就是说,他们处理上面的代码就像这些代码是这样写的:
int main() { int x; // define an int Person *p; // define a pointer to a Person ... } |
当然,这是合法的 C ,所以您也能够自己来玩这种“将类的实现隐藏在一个指针后面”的游戏。对 Person 做这件事的一种方法就是将他分开到两个类中,一个仅仅提供一个接口,另一个实现这个接口。假如那个实现类名为 PersonImpl,Person 就能够如此定义:
#include <string> // standard library components // shouldn’t be forward-declared #include <memory> // for tr1::shared_ptr; see below class PersonImpl; // forward decl of Person impl. class class Date; // forward decls of classes used in class Address; // Person interface 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: // ptr to implementation; std::tr1::shared_ptr<PersonImpl> pImpl; }; // std::tr1::shared_ptr |