What's the benefit for a C source file include its own header file
我知道,如果源文件需要引用其他文件中的函数,则它需要包括其头文件,但是我不明白为什么源文件包括其自己的头文件。 头文件中的内容只是在每个处理时间内作为函数声明被复制并粘贴到源文件中。 对于包含其自己的头文件的源文件,这样的"声明"在我看来似乎不是必需的,事实上,从源文件中删除头后,项目仍然可以编译并且链接没有问题,所以源文件包括它的原因是什么? 自己的标题?
-
您是否尝试过不包含它? 您收到了哪些错误消息?
-
没有给C编译器一个机会告诉您声明与函数实现之间的不匹配是一个错误。 没有这种帮助,您将损失数小时的生命,发现这种不匹配。 它发生在最糟糕的时间,即从现在开始的一年,那时您不太清楚代码,并且做出了看似无害的更改。 使您的程序以难以诊断的方式崩溃。 它确实必须以艰难的方式来学习。
主要好处是让编译器验证标头及其实现的一致性。您这样做是因为它很方便,而不是因为它是必需的。如果没有这样的包含,绝对有可能使项目正确编译并运行,但是从长远来看,这会使项目的维护复杂化。
如果文件不包含其自己的标头,则可能会偶然遇到函数的前向声明与该函数的定义不匹配的情况-可能是因为您添加或删除了一个参数,而忘记了更新标头。发生这种情况时,仍将依赖于具有不匹配功能的代码进行编译,但是调用将导致未定义的行为。最好让编译器捕获此错误,当您的源文件包含其自己的标头时,该错误会自动发生。
-
您能告诉我编译器如何验证一致性吗?在编译时对我来说是神奇的,编译器如何知道所包含的头文件是其自身的文件还是其他文件?
-
@Chen标题和.c文件之间的关系是,标题包含.c文件定义的函数和全局变量的声明。例如。对于函数,头包含原型,.c文件包含完整定义;编译器检查完整定义是否具有与先前编译包含的标头时看到的原型相同的参数和返回类型。
-
@Chen编译器不知道标头以任何方式与源文件相关。它关心的只是标头中的函数原型与源文件中的函数定义相匹配。当不匹配时,将触发错误。
-
@dasblinkenlight正是我不明白的要点:如果源文件中包含的头文件是其他文件,那么这些函数原型完全是"不匹配的",那为什么没有触发错误。
-
@Chen当您包含其他源文件的头文件时,编译器只能访问原型。只要实际参数与原型匹配,即使实现不同,编译器也不会抱怨。编译器无法检查实现,因为它在另一个文件中。当您包含具有函数实现的文件的标头时,编译器可以访问原型和实现,因此可以对照另一个进行检查。如果原型与实际定义不匹配,编译器将抱怨。
-
@dasblinkenlight我假设您的意思是"不匹配"是函数返回类型和参数。如果我使用的C编译器没有对函数进行名称处理,则原型和实现之间的唯一"不匹配"就是函数名称。这是否意味着错误检查在这种情况下不起作用?
-
@Chen原型与其实现之间的不匹配是当函数名称相同但返回类型和/或形式参数类型不同时。名称链接本可以避免这种情况下的错误,因为链接器会检测到缺少的功能,但是正如您在注释中正确指出的那样,C没有名称修饰。
-
@dasblinkenlight谢谢,这就是我需要知道的全部内容,因此,基本上,要利用此错误检查机制,需要使用C ++编译器来编译C源文件。
-
@Chen有一个完全有效的C代码,无法使用C ++编译器进行编译(例如,无需强制转换就可以在C中分配malloc结果的赋值,但C ++需要强制转换)。此外,某些构造的语义在微妙但重要的方面有所不同。但是,如果您的源代码可以使用C ++编译器进行编译,那么您当然可以利用C ++提供的名称处理功能。
头文件告诉人们源文件可以做什么。
因此,头文件的源文件需要知道其义务。这就是为什么包含它。
-
我赞成,但是我还是很羡慕您的简洁性:-)
-
我相信你可以下载它
-
嗯,我怕我没听懂-英语不是我的母语。您下载它是什么意思?
-
您可以下载我的简明扼要。这是一个非常小的文件。
您的情况似乎是一个边缘情况,但是可以将包含文件视为该源文件与可能需要这些功能的任何其他源文件之间的一种约定。
通过在头文件中编写"合同",可以确保其他源文件将知道如何调用这些函数,或者,可以确保编译器将插入正确的代码并在编译时检查其有效性。 。
但是,如果您随后(甚至无意间)更改了相应源文件中的函数原型,该怎么办?
通过在该文件中包含与其他所有人相同的标头,如果更改无意间"破坏"了合同,您将在编译时被警告。
更新(来自@tmlen的评论):即使在这种情况下不包含,包含文件也可能使用声明和编译指示,例如#defines,typedef,enum,struct和inline以及编译器宏,这在编写时是没有意义的不止一次(实际上,在两个不同的地方写会很危险,以免副本彼此之间失去同步,从而导致灾难性的结果)。其中一些(例如,结构填充实用程序)可能会成为难以跟踪的错误。
-
标头还可以包括源和使用库的其他人使用的typedef,struct,enum,inline函数,宏等。
-
很好的一点。我已将其包含在我自己的答案中,除非您更愿意提供自己的一个...?
实际示例-假设项目中包含以下文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| /* 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中的定义不匹配。返回类型不同。 main.c根据foo.h中的声明,假设它返回double来调用foo函数。
foo.c和main.c彼此分开编译。由于main.c调用foo.h中声明的foo,因此编译成功。由于foo.c不包含foo.h,因此编译器不会意识到声明和定义之间的类型不匹配,因此也可以成功编译。
将两个目标文件链接在一起时,用于函数调用的机器代码将与用于函数定义的机器代码不匹配。函数调用期望返回double值,但是函数定义返回int。这是一个问题,尤其是当两种类型的大小不同时。最好的情况是您得到垃圾结果。
通过在foo.c中包含foo.h,编译器可以在运行程序之前捕获此不匹配。
并且,正如先前的答案中指出的那样,如果foo.h定义了foo.c使用的任何类型或常量,那么您肯定需要包括它。
这很有用,因为可以在定义函数之前先声明它们。
因此,碰巧您有一个声明,然后是一个调用调用,然后是实现。
您不必,但是可以。
头文件包含声明。只要原型匹配,您就可以随时调用。并且只要编译器在完成编译之前找到实现。