【问题标题】:Why should we include header file of a function prototype in the same file that the function is declared?为什么我们应该在声明函数的同一个文件中包含函数原型的头文件?
【发布时间】:2021-12-09 20:47:42
【问题描述】:

可能是一个愚蠢(而且非常简单)的问题,但我想尝试一下,因为我不知道在哪里可以找到答案。我意识到某本书,我已经开始在谷歌上搜索一些东西 - 我实际上有点好奇为什么,如果我们有这样的文件:

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,而我们应该使用头文件来定义每个模块的接口,而不是写它“原始”,但是为什么将带有原型的头文件附加到它声明的文件(@ 987654333@)? -Wall 选项标志如果我不包含它也不会说什么,那么为什么人们说它是“正确的方法”?它是否有助于避免错误,还是只是为了更清晰的代码?

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

有些用户说这是“正确的方法”。

【问题讨论】:

  • 一个原因很容易证明:将 file2.c 更改为 void foo(int)(不更改标头),然后在包含和不包含 file2.h 的情况下进行编译。
  • 另一个原因是如果你不包含header,你只能在它定义之后调用foo()。有了标头,file2.c 中的任何函数都可以调用foo(),无论它在文件中的位置如何(当然是在标头的#include 之后)
  • @1201ProgramAlarm 我明白了,所以它对调试很有帮助,对吗?我们确实想将 int 类型的参数传递给 foo,但是编译器会显示错误,因为头文件中的原型不接受任何参数,对吗?所以基本上,我们这样做是为了更快地调试我们的代码?
  • @sj95126 哦,你完全正确。如果我想通过添加一些函数来扩展文件,那么如果某些函数会使用foo() 函数,那么foo() 函数应该在想要使用它的函数之上。完全忘记了这一点。谢谢。

标签: c compilation linker


【解决方案1】:

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

例如,在上面的代码中,链接器是搜索从main() 调用的函数foo() 存在并链接到它的地方。

首先是编译器步骤,然后是链接器。

让我们演示一个在 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.h包含在file2.c中,编译器可以检查函数的原型是否与它的定义等价,如果不等价,则会出现编译错误。

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

那么编译器不会发现任何问题,我们必须等到链接步骤,当链接器发现从main()调用的函数foo()没有匹配时,它会出错。

如果链接器稍后会发现错误,为什么还要麻烦呢?

因为在大型解决方案中可能有数百个源代码需要花费大量时间来编译,因此等待链接器在最后引发错误会浪费大量时间。

【讨论】:

    【解决方案2】:

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

    如果您不包含标题,您可以将符号声明为一件事,然后将其定义为其他任何内容。例如。在头文件中你可以将foo 声明为一个函数,然后在源文件中你可以将它定义为一个完全不兼容的函数(不同的调用约定和签名),甚至根本不是一个函数——比如一个全局变量.这样的项目可能会链接但不会起作用。错误可能实际上是隐藏的,所以如果您没有可靠的测试,在客户通知您之前您不会发现它。或者更糟糕的是,有一篇关于它的新闻报道。

    在 C++ 中,符号携带有关其类型的信息,因此如果您声明一个事物,然后定义具有相同基名但类型不兼容的事物,链接器将拒绝链接该项目,因为引用了特定符号但从未定义.

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

    【讨论】:

    • 你能举出任何二进制文件损坏的例子吗?因为当我在file2.c 中将foo() 的定义更改为void foo(int ar) 并保留在标题(void foo(void))中,然后在file1.c 中写了foo(1),然后它说这个函数的参数太多。所以它无论如何都不允许我输入这样的代码。那么我们如何通过这样做来制作损坏的二进制文件呢?你能举个例子吗?
    【解决方案3】:

    这是唯一真正的原因TM

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

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

    【讨论】:

      猜你喜欢
      • 2021-05-03
      • 2017-12-13
      • 1970-01-01
      • 1970-01-01
      • 2013-10-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多