头文件(.h文件)在编译器中是否受到特殊对待,还是只是一种命名惯例?

8

我刚接触C家族编程和编译。我试图在低技术水平上理解.c/.cpp文件与.h文件之间的区别。我知道.h文件用于指定接口,而.c或.cpp文件用于实现。但我想知道这种差异是否反映在编译器的工作方式上,还是“只是”一种命名约定,以简化我们人类的操作?你能否在理论上将实现放在.h文件中并仍然进行编译?或者在.c文件中指定接口?我的问题是为了更好地理解编译器实际执行的操作。


5
简短回答 - 这只是一种约定俗成的做法。如果你想深入了解 - 尝试将一个 .c 文件重命名为 .h 并进行编译。如果你使用的不是简单的命令行编译方式(makefile/IDE),你可能会觉得这很有趣。 - Aviv Goll
5个回答

10

从技术上讲,对于编译器来说,任何扩展名之间没有区别。这就像在放置#include的位置手动键入任何文件的内容一样。您可以键入#include "foo.pdf",只要该文件包含代码(尽管扩展名不同),编译器就会成功地包含一个名为foo.pdf的文件。

现在,按照惯例,通常将声明放在.h / hpp文件中(或模板定义中),并将实现放在.c / cpp文件中。

许多库具有单个文件实现,还使用内联变量/函数进行支持。

有时候甚至连包含文件都不存在,例如当包含标准STL文件(如string)时,编译器可能根本不读取文件,而是以它想要的方式进行缓存/实现。

更多关于#include的信息请参见MSDNCPPReference


一个有趣的代码:#include "/dev/tty". 仅适用于类Unix系统;我想知道在Windows下是否有相当的代码(如果有的话) :-) - alx - recommends codidact
这将和 #include <con> 一样毫无意义。在 Windows 98 中,这甚至会导致计算机崩溃。 - Michael Chourdakis

5

编译器会按照你的指令进行操作。如果你明确地指定文件输入,编译器会将这些文件进行编译,并在其中包含其他文件,前提是你使用 #include 预处理指示告诉它这样做。

如果你让编译器编译一个 .h 文件(并通过命令行选项使其作为源文件进行处理,例如对于 gcc 使用 -x c),编译器会正常编译它。如果你使用 #include "a.cpp",它会被正常包含。


指定文件名是“你告诉它要做什么”的一部分。正如这个答案所暗示的那样,命令行上的文件名根据其扩展名而有所不同——当在命令行上给出一个.h文件时,需要使用-x c,因为编译器不会将其视为C源文件由于扩展名。因此,是的,GCC会以不同的方式处理.h文件和.c文件。 - Eric Postpischil

2
这只是一种命名约定,帮助我们组织代码。对于用户定义的头文件而言,至少在低级技术方面没有任何区别(某些实现可能具有“预编译”头文件,因此没有可读的源文本)。所有内容都必须是有效的C或C++代码。 C语言标准使用.h扩展名命名所有标准库头文件,因此这是大多数人遵循的惯例。 C++语言标准使用扩展名命名所有标准库头文件(例如iostreamstring等),但大多数人遵循.h(或.hpp)命名惯例,主要是为了使搜索更容易。
个别工具可能会关注——gcc将以不同的方式处理.c文件和.cpp文件,IDE可能会以不同的方式显示.h文件和.c文件——但这是特定工具的功能,而不是语言的功能。
一些(尽管古老的)系统使用了一种文件命名约定,根本不允许使用.h.c扩展名——HP3000上的MPE使用约定filename.groupname.accountname。 MPE上的C编译器能够正确映射标准头文件名称,例如stdio.hstdlib.h,但用户定义的头文件必须遵循filename.groupname.accountname格式(所有这些都必须适合35个字符或更少,包括分隔符,导致出现非常易读的名称,例如MYCODEHDR.DEVELOP.BODE)。

1

这确实只是编译器的透明约定,对于让大型项目更有组织性的接口/实现二元性非常有用。

在C++设计中,分离实践尤其有助于减少耦合,从而方便未来进行(接口/实现)自定义。

对于抽象化,即在库的情况下隐藏实现细节以使用户更方便使用接口,这种分离也非常有用。用户仅能访问接口,不需要理会实现细节。


0

这只是一个惯例,针对头文件而言。

您还可以包含一些其他代码,旧的模式是:

file.inc:

  MACRO("-a", "--append", "this will append text"),
  MACRO("-b", "--bottom", "something for bottom"),

在主文件中:

char *options[] = {
#define MACRO(short, long, help) (short)
#include "file.inc"
#undef MACRO
NULL
};

char *help[] = {
#define MACRO(short, long, help) (help)
#include "file.inc"
#undef MACRO
NULL
};

现在这样的构造不再那么频繁了。我认为在书籍《20世纪C》中仍然有一些这样的技巧,但是个人而言,我更喜欢使用外部预处理器。

Linux内核有时会包含其他*.c文件,例如使用宏修改几个函数。我认为这通常不是一个好的编码风格,但内核使用它来并行构建驱动程序等,这些驱动程序共享99.9%的代码。

注意:#include <include.h>是不同的。在这种情况下,include.h可以被解释为编译器的标签(编译器可以将其用作标志)。系统中没有必要拥有标准库头文件,但现代通用编译器也有标准库的头文件。

h文件可以为空,或者应该包含最后一行换行符\n。没有其他要求。对于上下文,它应该是有效的C语言。

历史上,预处理器cpp和编译器cc是两个不同的程序。因此,预处理器对文件名惯例和结构一无所知。编译器将编译其余部分,就像单个文件一样。[有关标准库的可能异常,请参见上文]注意:我还看到在shell脚本中使用C预处理器处理非C文件。(gcc -E)

最后一点。某些编译器(如gcc)使用文件扩展名来选择要使用的语言。您可以使用命令行选项覆盖它。但是,gcc a.h可能无法按您期望的方式编译文件。例如,在我的系统上touch a.h b.c;gcc a.h; gcc b.c会得到两个不同的结果。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接