C 标头问题:#include 和“未定义的引用”

C header issue: #include and "undefined reference"

提问人:user1018501 提问时间:4/28/2012 最后编辑:ppperyuser1018501 更新时间:7/4/2022 访问量:100536

问:

我有三个文件,、 和 。无论出于何种原因,它们似乎都不能很好地编译,我真的无法弄清楚为什么......main.chello_world.chello_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 gcc 标头 头文件 undefined-reference

评论

0赞 Cody Gray - on strike 7/1/2022
相关,但对于C++stackoverflow.com/questions/12573816/......
0赞 Peter Mortensen 1/10/2023
另一个问题与这个问题合并,但那是针对不同的编译器Clang,而不是 GCC)。注意macOS 上的可执行文件“gcc”通常表示 Clang(“gcc”是 Clang 编译器的别名),而不是 GCC。

答:

4赞 vard 4/28/2012 #1

应将从第二个 .c 文件(hello_world.c)编译的目标文件与 main.o 文件链接。

试试这个:

cc -c main.c
cc -c hello_world.c
cc *.o -o hello_world
57赞 Lundin 4/28/2012 #2
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 */

评论

1赞 KevinDTimm 4/28/2012
虽然标头保护是不必要的(在本例中),但这是一个很好的提示
0赞 redpix_ 12/27/2014
那么不同的 .c 文件是单独编译以生成一个可执行文件的吗?
1赞 Lundin 3/1/2017
标头保护确保多个 C 文件(包括同一个 H 文件)不会遇到在同一程序中多次声明/定义相同标识符的问题。编译器使用“翻译单元”,这是一个 C 文件加上该 C 文件包含的所有标头。这意味着同一个 H 文件可以存在于多个翻译单元中。
0赞 HeavenHM 9/11/2021
或者我们可以使用一次 #pragma,但它是非标准的,但得到广泛支持
0赞 Lundin 9/11/2021
@HaseeBMir 没有充分的理由使用与现有标准功能100%等效的非标准功能。
10赞 P.P 4/28/2012 #3

您没有在编译中包括文件 hello_world.c。用:

gcc hello_world.c main.c  -o main
6赞 Tom 4/28/2012 #4

您没有链接到 hello_world.c.

执行此操作的简单方法是运行以下编译命令:

cc -o main main.c hello_world.c

更复杂的项目通常使用构建脚本或生成文件来分离编译和链接命令,但上述命令(结合这两个步骤)应该适用于小型项目。

评论

0赞 Basile Starynkevitch 4/28/2012
是的,你绝对应该了解(或更好的建筑系统,比如makeomake)
1赞 john 4/28/2012 #5

是的,您似乎忘记链接 hello_world.c

我会使用 .如果文件数量较少,我们可以使用这种方法,但在较大的项目中,最好使用 Make 文件或一些编译脚本。gcc hello_world.c main.c -o main

2赞 Some programmer dude 6/30/2022 #6

这是了解翻译单位概念的好时机。

编译器一次只处理一个翻译单元。它不会知道其他可能的翻译单元,链接器的工作是将所有翻译单元放在一起。

用于构建程序的命令:

gcc main.c -o out

这只会编译并尝试链接两个翻译单元之一,即从 创建的翻译单元。根本没有使用翻译单元 from。main.chello_world.c

您需要为前端程序传递两个源文件,以构建和链接它们:gcc

gcc main.c hello_world.c -o out

评论

0赞 Richard Rast 6/30/2022
那么,只是放入你的 makefile 或其他东西是标准做法吗?似乎它会严重扩展到大型代码库(特别是考虑到在子目录中编译内容的复杂性......gcc *.c -o out
2赞 Eugene Sh. 6/30/2022
不,makefile 编写是一门单独的“艺术”。
0赞 Some programmer dude 6/30/2022
@RichardRast 手写或自动生成的生成文件列出了每个对象文件以及它们所依赖的源文件和头文件。然后(或其他构建系统)仅(重新)构建依赖于更改的源/头文件的目标文件。然后,它被设置为链接所有目标文件。make
0赞 Peter - Reinstate Monica 6/30/2022
现在不是谈论翻译单元的时候,因为在这里,您可以在一个大命令中完成所有操作。当您在一个命令中传递翻译单元时,现代编译器可以并且将会模糊翻译单元的线条(例如,执行整个程序优化,将代码从一个文件内联到另一个文件)。如果要讨论翻译单元和编译与链接阶段,则使用 -c 和链接阶段提供单个文件的编译阶段。
1赞 Eugene Sh. 6/30/2022
@RichardRast 当然,如果你使用某种IDE来管理你的项目,你可能会在大多数时候忽略这些事情。但在实践中,特别是对于由具有不同个人偏好的多个/多个开发人员开发的大型项目,该项目并不依赖于特定的 IDE,而是使用一些标准的构建系统(基于 make/cmake)
0赞 0___________ 6/30/2022 #7

您需要告诉编译器您的项目包含两个源文件:

gcc -o out main.c hello_world.c

同样在你的水平上,我建议使用IDE(例如,Eclipse CDT)来专注于编程,而不是构建。稍后您将学习如何构建复杂的项目。但现在只需学习编程即可。

1赞 Peter - Reinstate Monica 7/1/2022 #8

(我简要地看了一下 C 程序的构建块,然后检查了通常隐藏在 gcc 调用后面的构建步骤。

传统的编译语言,例如C和C++,被组织在源文件中,这些源文件通常一个接一个地“编译”成一个“目标文件”。每个源文件都是一个“翻译单元”——在处理完所有 include 指令之后。(因此,一个翻译单元通常由多个文件组成,而同一个包含文件通常出现在多个翻译单元中——严格来说,文件和翻译单元具有 n:m 关系。但实际上,可以说“翻译单元”是一个 C 文件。

若要将单个源文件编译为目标文件,请将标志传递给编译器:-c

gcc -c myfile.c

这将在同一目录中创建,或者可能在同一目录中创建。myfile.omyfile.obj

目标文件包含机器代码和数据(以及潜在的调试信息,但我们在这里忽略它)。机器代码包含函数,数据以变量的形式出现。目标文件中的函数和变量都具有称为“符号”的名称。编译器通常通过在程序中附加下划线等来转换变量和函数名称,在 C++ 中,生成的(“mangled”)名称包含有关类型的信息,对于函数,包含参数。

某些符号(例如全局变量和普通函数的名称)可从其他目标文件使用;它们是“导出”的。

只需稍作简化,符号就可以被视为地址别名:对于函数,名称是跳转目标地址的别名;对于变量,名称是程序可以从中读取和写入的内存位置地址的别名。

文件 help.c 包含函数的代码。C语言中的函数默认具有“外部链接”,它们可以从其他翻译单元使用。他们的名字——“符号”——被导出了。herp

在现代 C 中,使用在不同翻译单元中定义的名称的源文件必须声明该名称。这告诉编译器如何处理它,以及它可以在源代码中以哪些语法方式使用(例如,调用函数、赋值给变量、索引数组)。编译器生成的代码从这个“符号地址”读取或跳转到那个“符号地址”;链接器的工作是将所有这些符号地址替换为指向最终可执行文件中现有数据和代码的“真实”内存位置,以便跳转和内存访问落在所需位置。

在使用它的文件中声明名称(函数、变量)可以是“手动”的,例如 ,首次使用之前直接出现在文件中。但更常见的是,在翻译单元中定义的其他翻译单元可以使用的名称在头文件中声明,即 .using 翻译单元通过 -ing 来使用头文件中的“预制”声明。这里没有魔法;include 指令只是插入包含文件文本,就好像它直接写入文件中一样。完全没有区别。特别是,包含头文件不会告诉链接器与相应的源文件链接。原因很简单:链接器永远不知道包含的文件,因为在编译为对象文件的过程中会擦除该知识。void herp();helper.h#include

这意味着在您的情况下,必须对其进行编译,并且必须告诉链接器将其与程序的其余部分组合(“链接”),在您的情况下,编译的代码为 。help.cmain.c

讨论如何做到这一点有点困难,因为这个过程非常普遍,以至于典型的 C 编译器集成了编译和链接阶段:只需执行创建可执行文件所需的一切。gcc -o myprog help.c main.cmyprog

当我们说“编译器”时,例如,我们通常指的是“编译器驱动程序”,它从命令行中获取命令和文件,并执行必要的步骤来实现所需的结果,例如从我们的源代码生成可执行程序。gcc 的实际编译器是生成一个汇编文件,该文件必须“汇编”到一个目标文件中。编译源文件后,gcc 会使用适当的选项调用链接器,从而生成可执行文件。gcccc1as

下面是一个示例会话,详细介绍了这些阶段:

$ 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,可以使用汇编程序将它们组装成目标文件。但是,让我们快速浏览一下:ashelp.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_variableherp

啊,是的,我忘了我在 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 编译两者。在任何情况下都需要进行链接。

评论

0赞 Peter Mortensen 1/10/2023
Re “gcc 的实际编译器是 cc1”它在 macOS 上有所不同(常见的混淆来源)。