1. 牵一发而动全身
现在开始进入你的C++程序,你对你的类实现做了一个很小的改动。注意,不是接口,只是实现,而且是private部分。然后你需要rebuild你的程序,计算着这个build应该几秒钟就足够了。毕竟,只修改了一个类。你点击了build 或者输入了make( 或者其他方式),你被惊到了,然后羞愧难当,因为你意识到整个世界都被重新编译和重新链接了!当这些发生时你不觉的感到愤恨么?
2. 编译依赖是如何发生的
问题出在C++并不擅长将接口从实现中分离出来。类定义不仅指定了类的接口也同时指定了许多类的细节。举个例子:
1 class Person {
2 public:
3 Person(const std::string& name, const Date& birthday,
4 const Address& addr);
5 std::string name() const;
6 std::string birthDate() const;
7 std::string address() const;
8 ...
9 private:
10 std::string theName; // implementation detail
11 Date theBirthDate; // implementation detail
12
13 Address theAddress; // implementation detail
14
15 };
这里,类Person的实现需要使用一些类的定义,也就是string,Date,和Address,如果类Person对这些类的定义没有访问权,那么Person不会被编译通过。这些定义通过使用#include指令来提供,所以在定义Person类的文件中,你可能会发现像下面这样的代码:
1 #include <string>
2
3 #include "date.h"
4
5 #include "address.h"
不幸的是,定义Person类的文件和上面列出的头文件之间建立了编译依赖。任何一个头文件被修改,或者这些头文件依赖的文件被修改,包含Person类的文件就必须要重新编译,使用Person的任何文件也必须要重新编译。这样的级联编译依赖会对一个工程造成无尽的伤痛。
3. 尝试将类的实现分离出来
你可能想知道为什么C++坚持将类的实现细节放在类定义中。举个例子,你为什么不能这么定义Person类,将指定类的实现细节单独分离开来。
1 namespace std {
2 class string; // forward declaration (an incorrect
3 } // one — see below)
4 class Date; // forward declaration
5 class Address; // forward declaration
6 class Person {
7 public:
8 Person(const std::string& name, const Date& birthday,
9 const Address& addr);
10 std::string name() const;
11 std::string birthDate() const;
12 std::string address() const;
13 ...
14 };
如果这是可能的 ,Person的用户只有在类的接口被修改的时候才必须要重新编译。
这个想法有两个问题。首先,string不是类,它是一个typedef(basic_string<char>的typedef)。因此,对string的前置声明是不正确的。合适的前置声明实质上更加复杂,因为它涉及到了额外的模板。然而这没关系,因为你不应该尝试对标准库的某些部分进行手动声明。相反,简单的使用合适的#include来达到目的。标准头文件看上去不像是编译的瓶颈,特别是你的编译环境允许你利用预编译头文件。如果标准头文件的解析真的是一个问题,你可能需要修改你的接口设计来避免使用标准库的某些部分(使用标准库的某些部分需要使用不受欢迎的#includes)。
对每件事情进行前置声明的第二个难点(并且是更加明显的)是需要处理如下问题:在编译过程中编译器需要知道对象的大小。考虑:
1 int main()
2 {
3 int x; // define an int
4
5 Person p( params ); // define a Person
6
7
8 ...
9 }
当编译器看到x的定义时,它们知道必须要为一个int分配足够的空间。这没问题。编译器知道一个int有多大。当编译器看到p的定义时,它们知道必须要为一个Person分配足够的空间,但是他们如何知道一个Person对象有多大呢?唯一的方法就是通过查看类定义,但是对于一个类的定义来说,如果将其实现细节忽略掉是合法的,编译器如何知道需要分配多少空间呢?
这种问题不会出现在像Smalltalk 和Java这样的语言中,因为当在这些语言中定义一个对象时,编译器只为指向对象的指针分配足够的空间。对于上面的代码,它们会像下面这样进行处理:
1 int main()
2 {
3 int x; // define an int
4
5 Person *p; // define a pointer to a Person
6 ...
7 }
这当然是合法的C++代码,所以你可以自己玩“将对象细节隐藏在指针后面”的游戏。对于Person来说,一种实现方式就是将其分成两个类,一个只提供接口,另一个实现接口。如果实现类的被命名为PersonImpl,Person将会被定义如下:
1 #include <string> // standard library components
2 // shouldn’t be forward-declared
3
4 #include <memory> // for tr1::shared_ptr; see below
5
6 class PersonImpl; // forward decl of Person impl. class
7
8 class Date; // forward decls of classes used in
9
10
11
12 class Address; // Person interface
13
14 class Person {
15
16 public:
17
18 Person(const std::string& name, const Date& birthday,
19
20 const Address& addr);
21
22 std::string name() const;
23
24 std::string birthDate() const;
25
26 std::string address() const;
27
28 ...
29
30 private: // ptr to i