提问人:user1018501 提问时间:4/28/2012 最后编辑:ppperyuser1018501 更新时间:7/4/2022 访问量:100536
C 标头问题:#include 和“未定义的引用”
C header issue: #include and "undefined reference"
问:
我有三个文件,、 和 。无论出于何种原因,它们似乎都不能很好地编译,我真的无法弄清楚为什么......main.c
hello_world.c
hello_world.h
这是我的源文件。第一个 hello_world.c:
#include <stdio.h>
#include "hello_world.h"
int hello_world(void) {
printf("Hello, Stack Overflow!\n");
return 0;
}
然后hello_world.h,很简单:
int hello_world(void);
最后是 main.c:
#include "hello_world.h"
int main() {
hello_world();
return 0;
}
当我把它放到 GCC 中时,这就是我得到的:
cc main.c -o main /tmp/ccSRLvFl.o: In function `main': main.c:(.text+0x5): undefined reference to `hello_world' collect2: ld returned 1 exit status make: *** [main] Error 1
答:
应将从第二个 .c 文件(hello_world.c)编译的目标文件与 main.o 文件链接。
试试这个:
cc -c main.c
cc -c hello_world.c
cc *.o -o hello_world
gcc main.c hello_world.c -o main
此外,请始终使用标头防护:
#ifndef HELLO_WORLD_H
#define HELLO_WORLD_H
/* header file contents go here */
#endif /* HELLO_WORLD_H */
评论
您没有在编译中包括文件 hello_world.c。用:
gcc hello_world.c main.c -o main
您没有链接到 hello_world.c.
执行此操作的简单方法是运行以下编译命令:
cc -o main main.c hello_world.c
更复杂的项目通常使用构建脚本或生成文件来分离编译和链接命令,但上述命令(结合这两个步骤)应该适用于小型项目。
评论
make
omake
)
是的,您似乎忘记链接 hello_world.c。
我会使用 .如果文件数量较少,我们可以使用这种方法,但在较大的项目中,最好使用 Make 文件或一些编译脚本。gcc hello_world.c main.c -o main
这是了解翻译单位概念的好时机。
编译器一次只处理一个翻译单元。它不会知道其他可能的翻译单元,链接器的工作是将所有翻译单元放在一起。
用于构建程序的命令:
gcc main.c -o out
这只会编译并尝试链接两个翻译单元之一,即从 创建的翻译单元。根本没有使用翻译单元 from。main.c
hello_world.c
您需要为前端程序传递两个源文件,以构建和链接它们:gcc
gcc main.c hello_world.c -o out
评论
gcc *.c -o out
make
您需要告诉编译器您的项目包含两个源文件:
gcc -o out main.c hello_world.c
同样在你的水平上,我建议使用IDE(例如,Eclipse CDT)来专注于编程,而不是构建。稍后您将学习如何构建复杂的项目。但现在只需学习编程即可。
(我简要地看了一下 C 程序的构建块,然后检查了通常隐藏在 gcc 调用后面的构建步骤。
传统的编译语言,例如C和C++,被组织在源文件中,这些源文件通常一个接一个地“编译”成一个“目标文件”。每个源文件都是一个“翻译单元”——在处理完所有 include 指令之后。(因此,一个翻译单元通常由多个文件组成,而同一个包含文件通常出现在多个翻译单元中——严格来说,文件和翻译单元具有 n:m 关系。但实际上,可以说“翻译单元”是一个 C 文件。
若要将单个源文件编译为目标文件,请将标志传递给编译器:-c
gcc -c myfile.c
这将在同一目录中创建,或者可能在同一目录中创建。myfile.o
myfile.obj
目标文件包含机器代码和数据(以及潜在的调试信息,但我们在这里忽略它)。机器代码包含函数,数据以变量的形式出现。目标文件中的函数和变量都具有称为“符号”的名称。编译器通常通过在程序中附加下划线等来转换变量和函数名称,在 C++ 中,生成的(“mangled”)名称包含有关类型的信息,对于函数,包含参数。
某些符号(例如全局变量和普通函数的名称)可从其他目标文件使用;它们是“导出”的。
只需稍作简化,符号就可以被视为地址别名:对于函数,名称是跳转目标地址的别名;对于变量,名称是程序可以从中读取和写入的内存位置地址的别名。
文件 help.c 包含函数的代码。C语言中的函数默认具有“外部链接”,它们可以从其他翻译单元使用。他们的名字——“符号”——被导出了。herp
在现代 C 中,使用在不同翻译单元中定义的名称的源文件必须声明该名称。这告诉编译器如何处理它,以及它可以在源代码中以哪些语法方式使用(例如,调用函数、赋值给变量、索引数组)。编译器生成的代码从这个“符号地址”读取或跳转到那个“符号地址”;链接器的工作是将所有这些符号地址替换为指向最终可执行文件中现有数据和代码的“真实”内存位置,以便跳转和内存访问落在所需位置。
在使用它的文件中声明名称(函数、变量)可以是“手动”的,例如 ,在首次使用之前直接出现在文件中。但更常见的是,在翻译单元中定义的其他翻译单元可以使用的名称在头文件中声明,即 .using 翻译单元通过 -ing 来使用头文件中的“预制”声明。这里没有魔法;include 指令只是插入包含文件文本,就好像它直接写入文件中一样。完全没有区别。特别是,包含头文件不会告诉链接器与相应的源文件链接。原因很简单:链接器永远不知道包含的文件,因为在编译为对象文件的过程中会擦除该知识。void herp();
helper.h
#include
这意味着在您的情况下,必须对其进行编译,并且必须告诉链接器将其与程序的其余部分组合(“链接”),在您的情况下,编译的代码为 。help.c
main.c
讨论如何做到这一点有点困难,因为这个过程非常普遍,以至于典型的 C 编译器集成了编译和链接阶段:只需执行创建可执行文件所需的一切。gcc -o myprog help.c main.c
myprog
当我们说“编译器”时,例如,我们通常指的是“编译器驱动程序”,它从命令行中获取命令和文件,并执行必要的步骤来实现所需的结果,例如从我们的源代码生成可执行程序。gcc 的实际编译器是生成一个汇编文件,该文件必须“汇编”到一个目标文件中。编译源文件后,gcc 会使用适当的选项调用链接器,从而生成可执行文件。gcc
cc1
as
下面是一个示例会话,详细介绍了这些阶段:
$ ls
Makefile help.c help.h main.c
$ /lib/gcc/x86_64-pc-cygwin/7.4.0/cc1 main.c
main
Analyzing compilation unit
Performing interprocedural optimizations
<*free_lang_data> <visibility> <build_ssa_passes> <opt_local_passes> <targetclone> <free-inline-summary> <emutls> <whole-program> <inline>Assembling functions:
<materialize-all-clones> <simdclone> main
Execution times (seconds)
phase setup : 0.00 ( 0%) usr 0.00 ( 0%) sys 0.00 (22%) wall 1184 kB (86%) ggc
TOTAL : 0.00 0.00 0.01 1374 kB
$ ls
Makefile help.c help.h main.c main.s
$ /lib/gcc/x86_64-pc-cygwin/7.4.0/cc1 help.c
herp
Analyzing compilation unit
Performing interprocedural optimizations
<*free_lang_data> <visibility> <build_ssa_passes> <opt_local_passes> <targetclone> <free-inline-summary> <emutls> <whole-program> <inline>Assembling functions:
<materialize-all-clones> <simdclone> herp
Execution times (seconds)
phase setup : 0.01 (100%) usr 0.00 ( 0%) sys 0.00 (33%) wall 1184 kB (86%) ggc
TOTAL : 0.01 0.00 0.01 1370 kB
$ ls
Makefile help.c help.h help.s main.c main.s
我们现在有两个汇编文件,main.s 和 help.s,可以使用汇编程序将它们组装成目标文件。但是,让我们快速浏览一下:as
help.s
$ cat help.s
.file "help.c"
.text
.globl some_variable
.data
.align 4
some_variable:
.long 1
.text
.globl herp
.def herp; .scl 2; .type 32; .endef
.seh_proc herp
herp:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
.seh_setframe %rbp, 0
.seh_endprologue
nop
popq %rbp
ret
.seh_endproc
.ident "GCC: (GNU) 7.4.0"
即使我们对汇编程序一无所知,我们也可以清楚地识别符号和 ,它们是汇编标签。some_variable
herp
啊,是的,我忘了我在 help.c 中添加了一个变量定义:
$ cat help.c
#include "help.h"
int some_variable = 1;
void herp() {}
我们可以用汇编器组装汇编文件:as
$ as main.s -o main.o
$ ls
Makefile help.c help.h help.s main.c main.o main.s
$ as help.s -o help.o
$ ls
Makefile help.c help.h help.o help.s main.c main.o main.s
现在我们有两个目标文件。我们可以使用实用程序 nm
(“name mangleling”) 查看导出 (“extern”) 或需要 (“undefined”) 的符号:
$ nm --extern-only help.o
0000000000000000 T herp
0000000000000000 D some_variable
$ nm --extern-only main.o
U __main
U herp
“T”表示符号位于“text”部分,其中包含代码;“D”是数据部分,“U”代表“未定义”。(undefined 是 gcc 和/或 cygwin 的怪癖。__main
这里有问题的根源:除非将 main.o 与定义该未定义符号的目标文件配对,否则链接器无法“解析”名称,也无法产生跳转。没有跳转目的地。
现在我们可以将两个目标文件链接到可执行文件。Cygwin要求我们链接到cygwin.dll;对不起,这种情况。
$ ld main.o help.o /bin/cygwin1.dll -o main
$ ls
Makefile help.c help.h help.o help.s main* main.c main.o main.s
仅此而已。我应该补充一点,该程序无法正常运行。它不会结束,也不会对 Ctrl-C 做出反应;我可能错过了 gcc 为我们所做的一些 Gnu 或 Windows 构建的复杂性。
啊,Makefiles。Makefile 由目标定义和这些目标的依赖项组成: 一行
main: help.o main.o
根据两个 .o 文件指定目标“main”。 生成文件通常还包含指定如何生成目标的规则。但是 Make 有内置的规则;它知道您调用编译器以从 .c 文件生成 .o 文件(并自动考虑此依赖关系),并且它知道您将 o 文件链接在一起以根据它们生成目标,前提是目标与其中一个 .o 文件同名。
因此,我们不需要任何规则:我们只需定义非隐式依赖关系。项目的整个 Makefile 归结为:
$ cat Makefile
CC=gcc
main: help.o main.o
help.o: help.h
main.o: help.h
CC=gcc
指定要使用的 C 编译器。CC 是一个内置的 make 变量,指定 C 编译器(CXX 将指定 C++ 编译器,例如 g++)。
我看看:
$ make
gcc -c -o main.o main.c
gcc -c -o help.o help.c
gcc main.o help.o -o main
$ ls
Makefile help.c help.h help.o main.c main.exe* main.o
依赖项有效吗?
$ make
make: 'main' is up to date.
$ touch main.c
$ make
gcc -c -o main.o main.c
gcc main.o help.o -o main
$ touch help.h
$ make
gcc -c -o main.o main.c
gcc -c -o help.o help.c
gcc main.o help.o -o main
这看起来不错:在触摸单个源文件后,make 仅编译该文件;但是触摸两个文件所依赖的标头会使 make 编译两者。在任何情况下都需要进行链接。
评论