一个C源文件包含自己的头文件有什么好处?

21

我知道如果一个源文件需要引用来自其他文件的函数,则需要包含其头文件,但我不明白为什么源文件需要包含它自己的头文件。头文件中的内容只是在编译前作为函数声明复制粘贴到源文件中。对于包含自己头文件的源文件,这样的“声明”对我来说似乎并不必要,实际上,在从其源文件中移除头文件后,该项目仍然可以编译和连接,那么源文件包含自己的头文件的原因是什么呢?


你尝试过不包含它来做吗?你得到了哪些错误信息? - πάντα ῥεῖ
9
不让C编译器告诉你函数声明和实现之间不匹配是一个错误。没有这样的帮助,你会浪费几个小时来发现这样的不匹配。这种情况通常发生在最糟糕的时候,比如一年后,当你并不太记得代码,并且进行了看似无害的更改时。这会导致程序以非常难以诊断的方式崩溃。这确实需要通过吃亏的方式学习。 - Hans Passant
5个回答

28

主要的好处是编译器可以验证您的头文件和其实现的一致性。您这样做是因为它很方便,而不是因为它是必需的。可能确实可以在不进行此类包含的情况下使项目编译和正确运行,但长期来看,这会使项目维护变得复杂。

如果您的文件没有包括自己的头文件,则可能会意外地遇到这种情况:函数的前向声明与函数的定义不匹配——可能是因为您添加或删除了参数,忘记更新头文件。当这种情况发生时,依赖于具有不匹配的函数的代码仍然可以编译,但调用将导致未定义的行为。最好让编译器捕获此错误,当源文件包含其自己的头文件时,这会自动发生。


1
你能告诉我编译器如何验证一致性吗?对我来说,在编译时编译器如何知道包含的标头文件是自己的还是其他人的,这真是神奇。 - Chen
@Chen 头文件和.c文件之间的关系是,头文件包含了 .c 文件定义的函数和全局变量的声明。例如,对于函数,头文件中包含原型,.c 文件中包含完整定义;编译器检查完整定义与编译时所见到的头文件中的原型具有相同的参数和返回类型。 - Gilles 'SO- stop being evil'
3
@Chen 编译器并不知道头文件与源文件有任何关联。它只关心头文件中的函数原型是否与源文件中的函数定义匹配。当存在不匹配时,就会触发错误。 - Sergey Kalinichenko
3
当你从其他源文件包含一个头文件时,编译器只有原型的访问权限。只要实际参数与原型匹配,即使实现不同,编译器也不会抱怨。编译器无法检查实现,因为它在另一个文件中。如果你从一个包含函数实现的文件中包含头文件,编译器就可以访问原型和实现,因此可以相互核对。如果原型与实际定义不匹配,编译器将会抱怨。 - Sergey Kalinichenko
1
@Chen 当原型和实现之间存在不匹配时,可能是函数名称相同,但返回类型和/或形式参数类型不同。在这种情况下,名称混淆可以防止错误发生,因为链接器会检测到缺少的函数,但正如您在评论中正确指出的那样,C语言没有名称混淆。 - Sergey Kalinichenko
显示剩余4条评论

8

实际例子 - 假设项目中有以下文件:

/* foo.h */
#ifndef FOO_H
#define FOO_H
double foo( int x );
#endif

/* foo.c */
int foo( int x )
{
  ...
}

/* main.c */
#include "foo.h"

int main( void )
{
  double x = foo( 1 );
  ...
}

请注意,foo.h中的声明与foo.c中的定义不匹配;返回类型不同。根据foo.h中的声明,main.c调用foo函数并假定它返回一个doublefoo.cmain.c是分别编译的。由于main.c按照foo.h中的声明调用foo,因此可以成功编译。由于foo.c没有包含foo.h,编译器不知道声明和定义之间的类型不匹配,因此也可以成功编译。
当将两个目标文件链接在一起时,函数调用的机器代码与函数定义所期望的机器代码不匹配。函数调用期望返回一个double值,但函数定义返回一个int。这是一个问题,特别是如果这两种类型的大小不同。最好的情况是得到一个垃圾结果。
通过在foo.c中包含foo.h,编译器可以在运行程序之前捕获这个不匹配。
正如早些时候的答案所指出的那样,如果foo.h定义了foo.c使用的任何类型或常量,则肯定需要包含它。

5

头文件告诉人们源文件可以做什么。

因此,包含头文件的源文件需要知道它的职责。这就是为什么要包含它的原因。


2
我给它点赞了,但你的简洁还让我有些羡慕 :-) - LSerni
我相信你可以下载它。 - Ed Heal
抱歉,我不太明白 - 英语不是我的母语。你说的“download it”是什么意思? - LSerni
7
你可以下载我的简明扼要。这是一个非常小的文件。 - Ed Heal

4
你的情况似乎有些特殊,但是一个包含文件可以被视为源文件和可能需要这些函数的任何其他源文件之间的一种“合同”。
通过在头文件中编写“契约”,您可以确保其他源文件将知道如何调用这些函数,或者换句话说,您将确信编译器将在编译时插入正确的代码并检查其有效性。
但是如果你(甚至是无意地)改变了相应源文件中的函数原型呢?
通过在该文件中包含与其他人相同的头文件,如果更改无意间“破坏”了合同,您将在编译时收到警告。
更新(来自@tmlen的评论):即使在这种情况下不会,包含文件也可以使用声明和编译指令,例如#define、typedef、enum、struct和inline,以及编译器宏,这些东西写多遍是没有意义的(实际上,在两个不同的位置写是非常危险的,以防止复制与彼此失去同步,造成灾难性后果)。其中一些(例如结构填充编译指令)可能成为难以跟踪的错误。

3
头文件可能包括typedefsstructsenumsinline函数、宏等,这些内容被源代码和使用库的其他人所使用。 - tmlen
非常好的观点。我已经将其包含在我的答案中,除非您更愿意提供自己的答案...? - LSerni

1

这很有用,因为可以在定义之前声明函数。

所以通常会先声明函数,然后调用它,最后再实现函数。你不一定非要这样做,但是可以这样做。

头文件包含了函数的声明,只要原型匹配,就可以在任何时候调用。只要编译器在完成编译之前找到了实现。


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