为什么我们应该在声明函数的同一个文件中包含函数原型的头文件?

Why should we include header file of a function prototype in the same file that the function is declared?

这可能是一个愚蠢(而且非常简单)的问题,但我想尝试一下,因为我不知道在哪里可以找到答案。我正在读一本书,我开始用谷歌搜索一些东西——我真的很好奇为什么,如果我们有这样的文件:

file1.c

#include <stdio.h>
#include "file2.h"

int main(void){
    printf("%s:%s:%d \n", __FILE__, __FUNCTION__, __LINE__);
    foo();
    return 0;
}

file2.h

void foo(void);

file2.c

#include <stdio.h>
#include "file2.h"

void foo(void) {
    printf("%s:%s:%d \n", __FILE__, __func__, __LINE__);
    return;
}

编译它:

gcc file1.c file2.c -o file -Wall

为什么将包含 foo 函数原型的 file2.h 的头文件包含在声明 foo 的同一个文件中是一个好习惯?我完全理解将它附加到 file1.c,虽然我们应该使用头文件来定义每个模块的接口,而不是将其写成“原始”,但为什么将带有原型的头文件附加到声明它的文件中(file2.c)? -Wall 选项标志如果我不包含它也不会说明什么,那么为什么人们说这是“正确的方式”?它是否有助于避免错误,还是只是为了更清晰的代码?

这些代码示例取自此讨论: Compiling multiple C files in a program

有些用户说是'the correct way'。

要回答这个问题,您应该对编译器和linker 之间的区别有一个基本的了解。简而言之,编译器,单独编译每个翻译单元(C 文件),然后 linker 的工作是 link 所有编译文件在一起。

例如,在上面的代码中,linker 正在搜索从 main() 调用的函数 foo() 存在的位置并且 links 到它.

首先是编译器步骤,然后是 link呃。

让我们演示一个示例,其中在 file2.c 中包含 file2.h 会派上用场:

file2.h
void foo(void);
file2.c
#include <stdio.h>
#include "file2.h"

void foo(int i) {
    printf("%s:%s:%d \n", __FILE__, __func__, __LINE__);
    return;
}

此处foo()的原型与其定义不同

通过在 file2.c 中包含 file2.h 以便编译器可以检查函数的原型是否等同于它的定义,如果不等同,则会出现编译错误。

如果file2.h不包含在file2.c中会怎样?

然后编译器不会发现任何问题,我们必须等到 linking 步骤,那时 linker 会发现没有匹配函数 foo()main() 调用,它将通过错误。

如果 linker 以后无论如何都会发现错误,那何必呢?

因为在大型解决方案中,可能有数百个源代码需要花费大量时间进行编译,因此等待 linker 在最后提出错误将浪费大量时间。

C 通常不会破坏符号(也有一些例外,例如 Windows)。损坏的符号将携带类型信息。没有它,linker 相信你没有犯错。

如果不包括 header,您可以将符号声明为一回事,然后将其定义为其他任何东西。例如。在 header 中你可以将 foo 声明为一个函数,然后在源文件中你可以将它定义为一个完全不兼容的函数(不同的调用约定和签名),甚至不是函数all——比如说一个全局变量。这样的项目可能 link 但不会起作用。该错误实际上可能是隐藏的,因此如果您没有适当的可靠测试,您将无法发现它,直到客户让您知道为止。或者更糟的是,有一篇关于它的新闻报道。

在 C++ 中,符号携带有关其类型的信息,因此如果您声明一件事然后定义具有相同基本名称但类型不兼容的东西,linker 将拒绝 link项目,因为引用了特定符号但从未定义。

因此,在 C 中包含 header 以防止工具无法捕获的错误,这将导致二进制文件损坏。在 C++ 中,您这样做是为了在编译期间而不是稍后在 link 阶段出现错误。

这是唯一正确的理由TM:

如果编译器遇到没有原型的函数调用,它会从调用中派生出一个原型,请参阅标准第 6.5.2.2 章第 6 段。如果与实际函数的接口不匹配,则 在大多数情况下,未定义的行为。充其量它没有害处,但任何事情都有可能发生。

只有警告级别足够高,编译器才会发出警告或错误等诊断信息。这就是为什么您应该始终使用尽可能高的警告级别,并且 将头文件包含在实现文件中 。您一定不想错过这个让您的代码被自动检查的机会。