即使我们遵循了上面的建议,头文件的文件作用域中只出现类、结构体、联合体和内联函数的定义,如果相同的头文件在一个编译单元中被包含两次,仍然会有问题。如图2-5所示,这个问题可能出现在一个简单的包含图中。
编译组件c的.c文件时,预处理器首先包含相应的头文件c.h。c.h文件包含a.h的内容,接着c.h包含b.h文件,触发b.h又第二次包含a.h。如果a.h有任何定义(在C++中它几乎总会有),则编译器会抱怨有多个重复定义。
主要设计规则
在每个头文件的内容周围放置一个唯一且可预知的(内部)包含卫哨。
解决这个问题的传统方式是在每个头文件内容周围放置一个内部保护包。不管包含图什么样,这个包都确保类和内联函数的定义在给定的编译单元内只出现一次。注意,我们不是在企图阻止循环包含(这可能是一个设计错误),我们是在试图阻止源自一个非循环包含图中再收敛的重复包含。在之前的例子中,编译问题的一个解决方案如图2-6所示。注意,我们仍然缺少冗余(外部)包含卫哨(在2.5节中论述)。
例如,在一个编译单元中包含a.h时,预处理器将会首先检查预处理器符号INCLUDED_A是否已被定义。如果没有定义,卫哨符号INCLUDED_A将被定义一次但用于全部(针对这个编译单元),然后通过读包含在头文件剩余部分的定义继续进行预处理。这个头文件被第二次包含时,预处理器#ifndef条件从句中的内容(即文件的其他部分)将被忽略。
用于包含卫哨的实际符号并不重要,只要它不与整个系统中的任何其他符号相匹配。因为包含卫哨与一个给定的头文件绑定在一起,而该头文件名字在系统中必须是唯一的,将头文件名融入卫哨符号中的名字可以确保没有任何两个卫哨符号的名字是相同的。
预处理器不知道C++的作用域规则,因此我们必须确保包含卫哨符号不与系统中其他的任何名字冲突——甚至不能与.c文件中定义的函数名字冲突。
为此我们采用一个标准的命名规则,采用全局保留前缀(例如,INCLUDED)在头文件的根名称以大写字母方式(例如,STACK)添加,以确保卫哨名字唯一且可预知:
对可预知性的需求将在2.5节加以说明。