这里发生了一些不同的事情。首先,我将介绍多个文件的基本编译是如何工作的。
如果你有多个文件,重要的是函数的声明和定义之间的区别。定义可能是你在定义函数时习惯的:你把函数的内容写出来,比如
int square(int i) {
return i*i;
}
另一方面,声明让你向编译器声明你知道一个函数存在,但你不告诉编译器它是什么。例如,你可以写
int square(int i);
编译器会期望函数“square”在别处定义。
现在,如果您想要互操作两个不同的文件(例如,假设函数“square”在 add.c 中定义,并且您想在 main.c 中调用 square(10)),您需要同时定义和声明。首先,在 add.c 中定义 square。然后,您在 main.c 的开头声明它。这让编译器在编译 main.c 时知道有一个函数“square”在别处定义。现在,您需要将 main.c 和 add.c 编译到目标文件中。你可以通过调用来做到这一点
gcc -c main.c
gcc -c add.c
这将生成文件 main.o 和 add.o。它们包含已编译的函数,但不是完全可执行的。这里要理解的重要一点是 main.o 在某种意义上是“不完整的”。在编译 main.o 时,您告诉它函数“square”存在,但函数“square”没有在 main.o 中定义。因此 main.o 对函数“square”有一种“悬空引用”。除非您将它与另一个包含“square”定义的 .o(或 .so 或 .a)文件结合起来,否则它不会编译成完整的程序。如果您只是尝试将 main.o 链接到一个程序中,即
gcc -o executable main.o
你会得到一个错误,因为编译器会尝试解决对函数“square”的悬空引用,但不会找到它的任何定义。但是,如果在链接时包含 add.o(链接是在将 .o 文件转换为可执行文件或 .so 文件时解析所有这些对未定义函数的引用的过程),则不会有任何问题。即
gcc -o executable main.o add.o
这就是功能上在 C 文件中使用函数的方法,但是风格上,我刚刚向您展示的是“不是正确的方式”。我这样做的唯一原因是因为我认为它会更好地帮助您了解正在发生的事情,而不是依赖“#include magic”。现在,您可能已经注意到,如果您必须在 main.c 的顶部重新声明要使用的每个函数,事情会变得有些混乱.标头的想法是它包含仅函数的声明,没有它们的定义。这样,为了使用 add.c 中定义的函数编译程序,您无需手动声明正在使用的每个函数,也无需在代码中 #include 整个 add.c 文件。相反,您可以#include add.h,它只包含add.c 的所有函数的声明。
现在,复习一下#include:#include 只是将一个文件的内容直接复制到另一个文件中。所以,例如,代码
abc
#include "wtf.txt"
def
完全等价于
abc
hello world
def
假设 wtf.txt 包含文本“hello world”。
所以,如果我们把 add.c 的所有声明都放在 add.h 中(即
int square(int i);
然后在main.c的顶部,我们写
#include "add.h"
这与我们刚刚在 main.c 顶部手动声明函数“square”在功能上相同。
所以使用标头的一般想法是,您可以拥有一个特殊的文件,该文件只需#include 即可自动声明您需要的所有功能。
但是,标头还有一种更常见的用途。假设 main.c 使用来自 50 个不同文件的函数。 main.c 的顶部看起来像:
#include "add.h"
#include "divide.h"
#include "multiply.h"
#include "eat-pie.h"
...
相反,人们经常将所有这些#include 移到 main.h 头文件中,而只将 #include main.h 从 main.c 移到。在这种情况下,头文件有两个的用途。它声明 main.c 中的函数以供其他文件包含时使用,并且它包含 main.c 的所有依赖项,当包含在 main.c 中时。以这种方式使用它还允许 chains 依赖项。如果你#include add.h,你不仅会得到add.c中定义的函数,而且你也隐式得到add.c使用的任何函数,以及他们使用的任何函数,等等.
另外,更巧妙的是,#include 来自它自己的 .c 文件的头文件会隐式检查您所犯的错误。例如,如果您不小心将 square 定义为
double square(int i);
在 add.h 中,您通常可能不会意识到,直到您链接 main.o 正在寻找 一个 square 的定义,而 add.o 正在提供 另一个,不兼容 em> 一。这将导致您在链接时遇到错误,因此直到构建过程的后期您才会意识到错误。但是,如果您将 add.c 中的 #include add.h 添加到编译器,您的文件看起来像
#include "add.h"
int square(int i) {
return i*i;
}
处理#include 语句后的样子
double square(int i);
int square(int i) {
return i*i;
}
编译器在编译 add.c 时会注意到什么,并告诉你。有效地,以这种方式包含您自己的标题可以防止您向其他文件错误地宣传您提供的功能类型。
为什么不用声明就可以使用函数
正如您所注意到的,在某些情况下,您实际上可以使用一个函数,而无需每次都声明它或#include 任何声明它的文件。这是愚蠢的,每个人都同意这是愚蠢的。然而,这是 C 编程语言(和 C 编译器)的一个遗留特性,如果你使用一个函数而不首先声明它,它只是假设它是一个返回类型“int”的函数。因此,实际上,使用函数隐含地将该函数声明为一个函数,如果它尚未声明,则返回“int”。如果您考虑一下,这是非常奇怪的行为,如果您这样做,编译器应该会警告您。
标题保护
另一种常见做法是使用“Header Guards”。为了解释标头守卫,让我们看一个可能的问题。假设我们有两个文件:herp.c 和derp.c,它们都想使用彼此包含的函数。按照上述指南,您可能有一个带有该行的 herp.h
#include "derp.h"
还有一个带有行的derp.h
#include "herp.h"
现在,如果你想一想,#include "derp.h" 将被转换为derp.h 的内容,而derp.h 又包含#include "herp.h" 行,这将被转换为内容herp.h 和 that contains... 等等,所以编译器将永远继续下去,只是扩展包含。类似地,如果 main.h #包括 herp.h 和 derp.h,并且 herp.h 和 derp.h 都包括 add.h,我们看到在 main.h 中,我们以 two结尾> add.h 的副本,一份是#include herp.h 的结果,一份是包含derp.h 的结果。那么,解决方案呢? “标头守卫”,即防止任何标头被#included 两次的一段代码。例如,对于 add.h,正常的做法是:
#ifndef ADD_H
#define ADD_H
int sqrt(int i);
...
#endif
这段代码本质上是告诉预处理器(编译器中处理所有“#XXX”语句的部分)检查“ADD_H”是否已经定义。如果不是 (ifndef),那么它首先定义“ADD_H”(在这种情况下,不必将 ADD_H 定义为 as 任何东西,它只是一个已定义或未定义的布尔值),然后定义标题的其余内容。但是,如果已经定义了 ADD_H,那么 #include 这个文件将执行 nothing,因为在 #ifndef 块之外没有任何内容。所以这个想法是,只有第一次包含在任何给定文件中时,它才会实际向该文件添加任何文本。之后,#include 它不会向您的文件添加任何其他文本。 ADD_H 只是您选择的任意符号,用于跟踪是否已包含 add.h。对于每个标题,您使用不同的符号来跟踪它是否已被包含。例如,herp.h 可能会使用 HERP_H 而不是 ADD_H。使用“header guard”将解决我上面列出的任何问题,其中包含文件的重复副本,或者#includes 的无限循环。