[这是我在 1997 年发布的关于以下两者之间区别的两篇文章中的第二篇:#include ""#include <>,以及使用它们的策略。 这篇文章首先回复了 John Winters,他评论了第一篇。 我对本文进行了一些修改,使其适用于此网页。]

发件人:scs@eskimo.com (Steve Summit)
主题:Re: Quotes vs. angle brackets in #includes
日期:1997 年 6 月 5 日星期四 14:54:42 GMT
消息 ID:<EBB5F7.BEG@eskimo.com>
新闻组:comp.lang.c,comp.unix.programmer

几周前,在 <5kve70$t89@polo.demon.co.uk> 中,John Winters 写道
> 我很难想象使用尖括号作为你自己的头文件是合理的...
>

好吧,在我的早期文章中描述了一种(您在下面引用了其中的一部分,我将详细说明)。 但这涉及到一个有点晦涩的情况,我认为我甚至可以准备好证明将尖括号用于标准头文件以外的头文件是合理的,理由是它们看起来更好或发送不同的消息。 双引号表示“这些是本地头文件”。 尖括号表示“这些是系统头文件”。 库和第三方头文件属于哪里? 正好在中间。 两种头文件包含实际上是不够的,但这是 C 提供给我们的全部。 如果你有一个共享或第三方的头文件要#include,并且你不希望它看起来像本地的,那么很容易将它放在尖括号中(可能用一个空行将它与上面真正的标准头文件隔开)。 至少,一直很想这样做; 事实上,我倾向于这样做。

现在,的确,你肯定可以决定对所有库头文件使用双引号,并且的确,如果你关注标准,尖括号不仅表示“这些是系统头文件”,还表示“这些是标准头文件”。 因此,肯定有理由强烈倾向于对除标准头文件以外的所有头文件使用双引号,但也有(我声称)一些原因——至少是体面的,如果不是必然令人信服的——倾向于对你的一些不太纯粹的本地头文件使用尖括号。

...

>> 另一方面,只有在你使用双引号时,才会出现我所说的这个问题。
>> 如果你对中央项目头文件使用尖括号
>> (特别是对于中央项目头文件的嵌套包含),
>> 预处理器永远不会从错误的“当前”目录开始,
>> 而是始终从头开始遍历搜索
>> 路径,并在你的私有
>> 包含目录中找到它,正如你所希望的那样,无论如何。
>
> 假设你心中有某种特定的实现,但在
> 一般情况下,没有理由期望你所描述的行为。

我所想到的“特定实现”不是别的,正是 Unix,以及至少一半的非 Unix 编译器,它们遵循了旧的 Unix 预处理器在处理双引号方面的领先地位。 问题是,如果一个#include指令中的双引号表示“使用当前目录”,那么它是哪个当前目录? 如果我们调用编译器时所在的目录和包含.c文件的目录相同,那么就没什么问题了。 但是,如果.c文件不在我们通常认为的“当前目录”中呢? 更糟糕的是,如果一个.c文件(在当前目录中)包含一个头文件,该头文件由于某种原因不在当前目录中,并且该头文件包含一个#include使用双引号的指令? 许多预处理器,包括传统的 Unix 预处理器,都认为在这种情况下,#include ""首先搜索的“当前目录”是包含#include 指令的文件的目录。(我知道你知道这种可能性,John,但显然你还没有处理过它的一些更令人惊讶的后果。)

你为什么要这个规则? 为什么不让“当前目录”单方面地成为唯一的当前目录? 答案是,至少在某些时候,Unix 的行为正是你想要的。 假设我试图以一种非正式的方式共享一些代码,既不是作为库,也不是作为预编译的目标文件; 假设我(在我的 Makefile、项目定义或其他构建过程中)要求一些.c文件在一些其他目录中被编译,并将生成的目标文件链接到我的程序中。(我为什么要自己编译它,而不是使用预编译的对象或库? 也许我需要用我自己的宏定义集来编译它,以选择一组特定的#ifdef分组。) 进一步假设我试图编译的这个源文件,在它自己的目录中(我正在从中编译它),有它自己的头文件,而我不需要这个头文件。 那么 Unix 的行为将“做正确的事情”:该文件的头文件(它使用双引号包含),将在它的目录中找到。

现在,可以肯定的是,还有其他方法可以解决这个问题。 如果规则是当前目录始终是唯一的当前目录,这样预处理器在这种情况下无法找到远程源文件的头文件,那么我可以将远程文件的目录添加到要搜索的头文件目录列表中。 但我认为这是促使采用我所说的“Unix”预处理器行为的原因,并且大型项目的程序员需要理解它,因为许多编译器都遵循了 Unix 的领先地位,但正如我所说,该规则确实有一些更令人惊讶的后果。

假设我正在与其他人合作一个大型项目。 假设我们有一个中央目录,/usr/project,包含项目源代码,以及中央目录的一个子目录,/usr/project/include,包含所有共享项目源文件。(也许我们选择将头文件放在它们自己的目录中,因为它们在构成该项目的许多库或模块之间共享,并且这些库或模块分布在/usr/project的其他几个子目录中。) 进一步假设我有一个自己的目录,/usr/scs/project,我在其中处理我自己的项目部分,然后在将它们发布到中央目录之前,并假设我有一个子目录/usr/scs/project/include包含我正在修改的任何项目头文件。 当然,每当我编译项目的一部分时,我都会安排(如果使用 Unix,则使用-I开关)额外搜索目录/usr/scs/project/include/usr/project/include中的头文件,并且按照该顺序搜索。

最后,假设有两个文件project.htypes.h/usr/project/include,并且project.h包含以下行

	#include "types.h"

有一天,我有机会修改types.h。 我在/usr/scs/project/include中制作一个副本,编辑它,然后安排(在我自己的目录中)重新编译将受到更改影响的项目部分,这可能几乎是所有部分。 当我午休回来,尝试重新编译的项目时,我发现我对types.h的更改没有生效! 为什么? 因为每当项目的任何.c文件包含"project.h"时,它们都在/usr/project/include中找到它,因为我没有我自己的本地副本project.h。 但是,根据“当前目录是包含者的目录”的规则,当/usr/project/include/project.h请求"types.h"时,第一个要查找的地方是/usr/project/include,所以它找到/usr/project/include/types.h而不是/usr/scs/project/include/types.h。 为了解决这个问题(假设我甚至可以弄清楚到底发生了什么),我需要制作一个project.h/usr/scs/project/include的副本,即使我无意修改它,也只是为了让所有的编译首先找到,并让它的#include"types.h"找到我的修改副本types.h。(然后,我再去享受一个漫长的下午茶点时间,再次重新编译整个项目。)

现在,这听起来可能是一个如此复杂、人为的和晦涩的情况,以至于不值得为此做出实现策略的决定。 我甚至倾向于同意你的观点,但 Larry Weiss 立即并且准确地知道我在说什么,所以他显然也遇到了这个问题。(我怀疑他和我不孤单。) 当你发现自己处于这种情况时,你可以想到 4 个选择:

  1. 使用不同的编译器,使用一组不同的规则来解决#include ""行。

  2. 不允许“嵌套 #include 文件”; 不要让project.h执行#include上的types.h

  3. 每当你想要处理project.h时,始终检出types.h(到你自己的私有工作目录中),并接受当你忘记时浪费的一些编译时间。

  4. 而是使用project.h使用
    	#include <types.h>
    
    ,以便它始终明确地在头文件搜索路径中找到第一个,而不会因为“当前目录”是什么而绊倒。

上次发生这种情况时(因为是的,正如你可能已经猜到的,并非所有这些“假设”都是假设的),我突然想到,比通常但较弱的“这使得很难找到定义事物的位置”这一论点更有力地支持禁止嵌套头文件(即选择 #2)——这一论点只对那些仍然不清楚“grep”概念的人来说才是真正令人信服的。 但是,如果你不愿意禁止嵌套#include文件,并且如果你无法切换编译器,并且如果你厌倦了大量的重新编译(或者,更糟糕的是,在修复了中央头文件后,你的修复似乎不起作用,但你却徒劳地寻找其他错误原因),那么 #4 可能是你最好或唯一的选择,尽管它可能依赖于实现。

异端建议? 也许...

Steve Summit
scs@eskimo.com