#include<stdio.h>
#include<math.h>
int main(){
double area;
double radio;
printf("Area del circulo? ");
scanf("%lf", &area);
radio = sqrt(area/M_PI);
printf("El circulo de area %g tiene un radio %g\n", area, radio);
return 0;
}
$ gcc -c area.c
area.c: In function ‘main’:
area.c:11:13: warning: implicit declaration of function ‘raiz_cuadrada’ [-Wimplicit-function-declaration]
radio = raiz_cuadrada(area/M_PI);
在编译器有机会看到代码之前,预处理器读取它并可能将其转换为其他东西,遵循源中包含的任何预处理器指令。
A
#include
是预处理器的这些指令之一,告诉它此时包含另一个文件的内容。编译器收到的内容就好像您自己打开了#include
它所指示的文件并将其复制并粘贴到主程序中一样。因此文件的内容
.h
必须是有效的 C 代码。实际上它可以是任何代码(它可以包括例如实现某些功能的源代码),但通常情况并非如此。通常,它只包含声明(数据类型或函数原型),而实现本身不在 中.h
,而是在其他.c
将单独编译并添加到主文件中或存储在库中的文件中。多亏了那些从 . 中引入的原型,编译器
.h
可以检查当你调用一个函数时,你是否传递了正确的参数和适当的类型。但是,编译器不需要函数的代码,有它们的原型就足够了。为了汇编最终的可执行文件,一旦编译器完成,就会出现链接汇编器(链接器),它编译编译器生成的机器码加上其他编译文件的机器码,加上必要的外部库的机器码,以及解决调用的所有问题到已经使用的功能。如果其中声明的任何函数
.h
(然后从程序中调用)在挂载时没有出现,那么在链接时就会出错。这些文件
.h
称为“头文件”或headers。许多人也称它们为书店。这是错误的,并不是因为像 library 这样的迂腐东西不是 library 的正确翻译(应该是 library),而是因为 a包含.h
源代码(并且被编译器使用),而 library(或 library,如果你想要) 包含机器代码并由链接构建器使用。更新
这个长更新是为了回答用户在评论中关于链接时错误的一些额外问题。
以下示例有助于说明创建可执行文件的两个阶段之间的区别:
在包含
math.h
时,预处理器在该点包含文件的全部内容math.h
。除其他事项外,在该文件中,常量是M_PI
用值定义的,3.1415926etc...
并提供函数的原型sqrt()
。实际上它稍微复杂一些,因为该函数的原型是通过宏定义的,但出于我们的目的,我们可以想象文件包含这样的一行:多亏了这个声明,当我们在程序中调用时,编译器可以验证
sqrt()
我们作为参数传递的类型是正确的,并且函数返回的内容可以分配给变量radio
,或者如果是的类型,radio
是float
什么应该进行自动转换。然而,如果你尝试编译这个例子,你会得到一个错误(至少
gcc
在 Linux 上是这样):这是一个链接时错误(
ld
错误指的是链接器名称)。如果
gcc
您告诉它“仅编译”(即执行第一阶段但不执行第二阶段),则不会发生:在这种情况下没有错误,编译结果是
area.o
. 但这不是一个完整的可执行文件。它只有函数的机器码main
,但没有其他被调用的函数的机器码,main
例如或。scanf()
printf()
sqrt()
出于这个原因,需要第二阶段,即链接阶段,我们可以手动完成,但我们再次收到错误:
请注意,由于链接器将输入
area.o
而不是area.c
,它不再查看源代码,而是查看编译器生成的机器代码。在该机器代码中,链接器的任务是“填补空白”。编译器留下标记,例如“这里调用一个函数printf
,这里调用另一个函数sqrt
。链接器必须找到这些函数的机器代码,将它们添加到可执行文件中,并在这些空白处填充CALL
.链接器从哪里获得其他功能的机器代码?嗯,基本上来自三个地方:
.o
时在命令中指定的其他文件(在这种情况下我们没有放置任何文件)-l
(在这种情况下我们没有放任何)事实证明,C 标准库有
printf
和函数的机器代码scanf
,所以这些都很好。但它没有 的代码sqrt
,因为它在数学库中,默认情况下不搜索。这就是链接器找不到它并给出错误的原因。请注意,链接器错误仅限于显示它找不到的函数的名称和调用它的函数的名称,但它无法告诉您
area.c
调用是从哪个特定行进行的,因为链接器不会读取源(事实上你可能已经在第一个编译阶段之后删除了它)。为避免此错误,您必须告诉链接器在这种情况下也要查看数学库,必须使用该选项
-lm
。然后:现在是的,该文件
area
是最终的可执行文件。我们也可以使用单个命令执行这两个阶段,而不是单独执行:
请注意,唯一的区别是我放了
area.c
(所以它gcc
会先编译然后链接)而不是area.o
(在这种情况下,编译阶段被跳过,因为它已经完成了)。还有更多
最后一个细节,有点出人意料。我们说过编译器只需要函数的原型,不需要它的代码。事实上,你甚至不需要原型!
你可以做以下实验。In将对另一个
area.c
的调用更改为,一个既未声明也不存在的函数,并尝试“单独编译”,而不链接:sqrt()
raiz_cuadrada()
如您所见,我们收到警告,但不是错误。编译器将生成一个,为链接器
.o
留下一个洞,上面写着“这里应该是函数调用”。自然,链接器不会找到它,并且会像以前一样在链接时抛出错误。raiz_cuadrada
但是当我们调用一个不存在的函数时,编译器怎么可能没有给出错误呢?因为当编译器遇到一个未声明的函数调用时,它自己组成了声明。这是隐式声明所调用的内容,也是警告警告我们的内容。编译器构成的语句基于我们所做的调用。由于您看到我们向它传递了一个类型的参数
double
,它假定声明将是raiz_cuadrada(double)
. 但是对于返回值,它总是假定int
,在这种情况下这是错误的。这就是为什么包含.h
正确的声明很重要,以防止编译器“发明”不正确的声明。