我知道头文件具有各种函数,结构等的前向声明,这些声明在.c
文件中用于“调用”#include
,对吗?据我所知,“权力分立”的发生方式如下:
标题文件:func.h
包含函数的前向声明
int func(int i);
C源文件:func.c
包含实际的功能定义
#include "func.h"
int func(int i) {
return ++i ;
}
C源文件source.c
(“实际”程序):
#include <stdio.h>
#include "func.h"
int main(void) {
int res = func(3);
printf("%i", res);
}
我的问题是:看到#include
只是一个编译器指令,它复制了.h
所在文件中#include
的内容,{{1}如何? } file知道如何实际执行该函数?所有它都是.c
,那么它如何实际执行该功能呢?如何获得int func(int i);
的实际定义?标题是否包含某种“指针”,表示“这是我的定义,在那边!”?
它是如何工作的?
答案 0 :(得分:28)
Uchia Itachi给出了答案。它是链接器。
使用GNU C编译器gcc
,您将编译一个单文件程序,如
gcc hello.c -o hello # generating the executable hello
但是如您的示例中所述编译两个(或更多)文件程序,您必须执行以下操作:
gcc -c func.c # generates the object file func.o
gcc -c main.c # generates the object file main.o
gcc func.o main.o -o main # generates the executable main
每个目标文件都有外部符号(您可以将其视为公共成员)。默认情况下,函数是外部函数,而(全局)变量默认是内部函数。您可以通过定义
来更改此行为static int func(int i) { # static linkage
return ++i ;
}
或
/* global variable accessible from other modules (object files) */
extern int global_variable = 10;
当遇到未在主模块中定义的函数调用时,链接器将搜索作为定义被调用函数的模块的输入提供的所有对象文件(和库)。默认情况下,您可能有一些链接到您的程序的库,这就是您可以使用printf
的方法,它已经被编译到库中。
如果您真的感兴趣,请尝试一些汇编编程。这些名称相当于汇编代码中的标签。
答案 1 :(得分:14)
这是处理所有这些的链接器。编译器只在对象文件中发出一个特殊的序列,说“我有这个外部符号func
,请为链接器解析它”。然后链接器会看到它,并在所有其他目标文件和库中搜索符号。
答案 2 :(得分:3)
在同一编译单元中没有定义的符号声明告诉编译器使用该符号地址的占位符编译到目标文件中。
链接器将看到符号的定义是必需的,并将在库和其他目标文件中查找符号的外部定义。
如果链接器找到定义,原始对象文件中的占位符将替换为最终可执行文件中找到的地址。
答案 3 :(得分:2)
标头不仅可以访问同一程序中的其他.c
文件,还可以访问可能以二进制形式分发的库。一个.c
文件与另一个文件的关系与依赖另一个文件的库完全相同。
由于无论实现的格式如何,编程接口都需要采用文本形式,因此头文件作为关注点的分离是有意义的。
正如其他人所提到的,解析函数调用和库与源(翻译单元)之间访问的程序称为链接器。
链接器不适用于标头。它只是创建了一个包含所有翻译单元和库中定义的所有名称的大表,然后将这些名称链接到访问它们的代码行。古老的C语言使用允许在没有任何实现声明的情况下调用函数;我们假设每个未定义的类型都是int
。
答案 4 :(得分:2)
通常在编译这样的文件时:
gcc -o program program.c
你真的在调用一个驱动程序,它执行以下操作:
cpp
预处理(如果您要求它是单独的步骤)。cc1
as
(gas,GNU Assembler)进行组装。collect2
进行链接,ld
也使用.o
(GNU链接器)。通常,在前3个阶段中,您将创建一个简单的目标文件({{1}}扩展名),该文件是通过编译编译单元(即.c文件,替换了#include和其他指令)创建的。由预处理器)。
第4阶段是创建最终可执行文件的阶段。编译单元后,编译器将几段代码标记为需要链接器解析的引用。链接器的工作是在许多编译单元中搜索并解析对外部编译单元的引用。