《Effective C++》读书笔记 —— 条款 31: 将文件间的编译依存关系降至最低

读 《Effective C++》条款 31 的时候一直模模糊糊的,因为不了解 C++ 编译依存的原理,不知道什么时候会重新编译,什么时候不会。所以读的时候很是纠结,不明白该条款的意义。后来读了这篇文章,就清晰了很多。博主用一个简短的程序例子,展示了 C++ 基本的编译依存原则。感谢。

读完条款 31 ,我也更加深刻的明白了 C 语言为什么头文件和实现文件分开。大一时老师说的结构更加清晰或许是更次要的作用。其主要作用是降低文件间的编译依存关系,加快编译速度。

下面是我读的文章,原文链接

在说这一条款之前,先要了解一下 C/C++ 的编译知识,假设有三个类 ComplexClass, SimpleClass1 和 SimpleClass2,采用头文件将类的声明与类的实现分开,这样共对应于 6 个文件,分别是 ComplexClass.h,ComplexClass.cpp,SimpleClass1.h,SimpleClass1.cpp,SimpleClass2.h,SimpleClass2.cpp。

ComplexClass 复合两个 BaseClass,SimpleClass1 与 SimpleClass2 之间是独立的,ComplexClass 的.h 是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef COMPLESS_CLASS_H
#define COMPLESS_CLASS_H

#include “SimpleClass1.h”
#include “SimpleClass2.h”

class ComplexClass
{
SimpleClass1 xx;
SimpleClass2 xxx;
};


#endif /* COMPLESS _CLASS_H */

我们来考虑以下几种情况:

Case 1:

现在 SimpleClass1.h 发生了变化,比如添加了一个新的成员变量,那么没有疑问,SimpleClass1.cpp 要重编,SimpleClass2 因为与 SimpleClass1 是独立的,所以 SimpleClass2 是不需要重编的。

那么现在的问题是,ComplexClass 需要重编吗?

答案是 “是”,因为 ComplexClass 的头文件里面包含了 SimpleClass1.h(使用了 SimpleClass1 作为成员对象的类),而且所有使用 ComplexClass 类的对象的文件,都需要重新编译!

如果把 ComplexClass 里面的 #include “SimpleClass1.h” 给去掉,当然就不会重编 ComplexClass 了,但问题是也不能通过编译了,因为 ComplexClass 里面声明了 SimpleClass1 的对象 xx。那如果把 #include “SimpleClass1.h” 换成类的声明 class SimpleClass1,会怎么样呢?能通过编译吗?

答案是 “否”,因为编译器需要知道 ComplexClass 成员变量 SimpleClass1 对象的大小,而这些信息仅由 class SimpleClass1 是不够的,但如果 SimpleClass1 作为一个函数的形参,或者是函数返回值,用 class SimpleClass1 声明就够了。如:

1
2
3
4
5
1 // ComplexClass.h
2 class SimpleClass1;
3 …
4 SimpleClass1 GetSimpleClass1() const;
5 …

但如果换成指针呢?像这样:

1
2
3
4
5
6
7
8
9
10
 1 // ComplexClass.h
2 #include “SimpleClass2.h”
3
4 class SimpleClass1;
5
6 class ComplexClass:
7 {
8 SimpleClass1* xx;
9 SimpleClass2 xxx;
10 };

这样能通过编译吗?

答案是 “是”,因为编译器视所有指针为一个字长(在 32 位机器上是 4 字节),因此 class SimpleClass1 的声明是够用了。但如果要想使用 SimpleClass1 的方法,还是要包含 SimpleClass1.h,但那是 ComplexClass.cpp 做的,因为 ComplexClass.h 只负责类变量和方法的声明。

那么还有一个问题,如果使用 SimpleClass1 * 代替 SimpleClass1 后,SimpleClass1.h 变了,ComplexClass 需要重编吗?

先看 Case2。

Case 2:

回到最初的假定上(成员变量不是指针),现在 SimpleClass1.cpp 发生了变化,比如改变了一个成员函数的实现逻辑(换了一种排序算法等),但 SimpleClass1.h 没有变,那么 SimpleClass1 一定会重编,SimpleClass2 因为独立性不需要重编,那么现在的问题是,ComplexClass 需要重编吗?

答案是 “否”,因为编译器重编的条件是发现一个变量的类型或者大小跟之前的不一样了,但现在 SimpleClass1 的接口并没有任务变化,只是改变了实现的细节,所以编译器不会重编。

Case 3:

结合 Case1 和 Case2,现在我们来看看下面的做法:

1
2
3
4
5
6
7
8
9
10
 1 // ComplexClass.h
2 #include “SimpleClass2.h”
3
4 class SimpleClass1;
5
6 class ComplexClass
7 {
8 SimpleClass1* xx;
9 SimpleClass2 xxx;
10 };
1
2
3
4
5
6
1 // ComplexClass.cpp
2
3 void ComplexClass::Fun()
4 {
5 SimpleClass1->FunMethod();
6 }

请问上面的 ComplexClass.cpp 能通过编译吗?

答案是 “否”,因为这里用到了 SimpleClass1 的具体的方法,所以需要包含 SimpleClass1 的头文件,但这个包含的行为已经从 ComplexClass 里面拿掉了(换成了 class SimpleClass1),所以不能通过编译。

如果解决这个问题呢?其实很简单,只要在 ComplexClass.cpp 里面加上 #include “SimpleClass1.h” 就可以了。换言之,我们其实做的就是将 ComplexClass.h 的 #include “SimpleClass1.h” 移至了 ComplexClass1.cpp 里面,而在原位置放置 class SimpleClass1。

这样做是为了什么?假设这时候 SimpleClass1.h 发生了变化,会有怎样的结果呢?

SimpleClass1 自身一定会重编,SimpleClass2 当然还是不用重编的,ComplexClass.cpp 因为包含了 SimpleClass1.h,所以需要重编,但换来的好处就是所有用到 ComplexClass 的其他地方,它们所在的文件不用重编了!因为 ComplexClass 的头文件没有变化,接口没有改变!

总结一下,对于 C++ 类而言,如果它的头文件变了,那么所有这个类的对象所在的文件都要重编,但如果它的实现文件(cpp 文件)变了,而头文件没有变(对外的接口不变),那么所有这个类的对象所在的文件都不会因之而重编。

因此,避免大量依赖性编译的解决方案就是:在头文件中用 class 声明外来类,用指针或引用代替变量的声明;在 cpp 文件中包含外来类的头文件。