C头文件和编译/链接
我知道头文件具有各种函数,结构等的前向声明,这些函数,结构等在.c
文件中用于“调用” #include
,对吗? 据我了解,“权力分立”的情况如下:
头文件: func.h
-
包含函数的前向声明
int func(int i);
C源文件: func.c
-
包含实际的函数定义
#include "func.h" int func(int i) { return ++i ; }
C源文件source.c
(“实际”程序):
#include #include "func.h" int main(void) { int res = func(3); printf("%i", res); }
我的问题是:看到#include
只是一个编译器指令,它在.h
所在的文件中复制.h
的内容, .c
文件如何知道如何实际执行该函数? 所有它得到的是int func(int i);
那么它怎么能实际执行这个function呢? 它如何获得func
的实际定义? 标题是否包含某种“指针”,表示“那是我的定义,在那边!”?
它是如何工作的?
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
,它已经被编译成一个库。
如果您真的感兴趣,请尝试一些汇编编程。 这些名称相当于汇编代码中的标签。
它是处理所有这些的链接器。 编译器只在对象文件中发出一个特殊的序列,说“我有这个外部符号func
,请解析它”链接器。 然后链接器会看到它,并在所有其他目标文件和库中搜索符号。
在同一编译单元中没有定义的符号声明告诉编译器使用该符号地址的占位符编译到目标文件中。
链接器将看到符号的定义是必需的,并将在库和其他目标文件中查找符号的外部定义。
如果链接器找到定义,则原始对象文件中的占位符将替换为最终可执行文件中的找到的地址。
标题不仅提供对同一程序中的其他.c
文件的访问,而且同样提供对可以以二进制forms分发的库的访问。 一个.c
文件与另一个文件的关系与依赖另一个文件的库完全相同。
由于无论实现的格式如何,编程接口都需要采用文本forms,因此头文件作为关注点的分离是有意义的。
正如其他人所提到的,解析函数调用和库和源(转换单元)之间访问的程序称为链接器。
链接器不适用于标头。 它只是创建了一个包含所有翻译单元和库中定义的所有名称的大表,然后将这些名称链接到访问它们的代码行。 古老的C语言使用允许在没有任何实现声明的情况下调用函数; 假设每个未定义的类型都是int
。
通常在编译这样的文件时:
gcc -o program program.c
你真的在调用一个驱动程序,它执行以下操作:
- 使用
cpp
预处理(如果你要求它是一个单独的步骤)。 - 使用
cc1
编译(可以与预处理集成) - 组装,使用
as
(gas,GNU Assembler)。 - 使用
collect2
链接,它也使用ld
(GNU链接器)。
通常,在前3个阶段中,您将创建一个简单的目标文件( .o
扩展名),该文件通过编译编译单元(即.c文件,其中#include和其他指令由预处理器替换)来创建。
第四阶段是创建最终可执行文件的阶段。 编译单元后,编译器将几段代码标记为需要由链接器解析的引用。 链接器的工作是在许多编译单元中搜索并解析对外部编译单元的引用。