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

39

我有三个文件,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): 对 `hello_world' 的引用未定义
collect2: ld 返回了 1 个退出状态
make: *** [main] 错误 1

相关的,但是针对C++。https://dev59.com/0Wcs5IYBdhLWcg3wym_D - Cody Gray
这个问题与另一个问题合并了,但那个问题是针对不同的编译器(Clang,而不是 GCC)。注意:在macOS上,可执行文件'gcc'通常意味着Clang('gcc'被别名为Clang编译器),而不是GCC。 - Peter Mortensen
8个回答

57
gcc main.c hello_world.c -o main

此外,始终使用头文件保护(header guards):

#ifndef HELLO_WORLD_H
#define HELLO_WORLD_H

/* header file contents go here */

#endif /* HELLO_WORLD_H */

1
虽然头文件保护在这个例子中是不必要的,但它是一个很好的提示。 - KevinDTimm
那么不同的 .c 文件是分别编译成一个可执行文件吗? - redpix_
1
头文件保护确保多个包含相同H文件的C文件不会在同一程序中多次声明/定义相同的标识符而出现问题。编译器使用“翻译单元”,即一个C文件加上该C文件包含的所有头文件。这意味着相同的H文件可以存在于多个翻译单元中。 - Lundin
或者我们可以使用#pragma once,但它是非标准的,但被广泛支持。 - Haseeb Mir
@HaseeBMir,没有任何合理的理由可以使用非标准功能,因为这些功能与现有的标准功能完全等效。 - Lundin
显示剩余3条评论

10

您没有将文件hello_world.c包含在编译中。请使用:

gcc hello_world.c main.c  -o main

6

你没有链接到hello_world.c文件。

一个简单的方法是运行以下编译命令:

cc -o main main.c hello_world.c

更复杂的项目通常使用构建脚本或make文件来分离编译和链接命令,但上述命令(结合两个步骤)对于小型项目来说已经足够了。


是的,你绝对应该学习关于 make 的知识(或者更好的构建系统,比如 omake)。 - Basile Starynkevitch

4
你需要将第二个 .c 文件编译生成的目标文件 hello_world.c 与你的 main.o 文件链接。
请尝试以下操作:
cc -c main.c
cc -c hello_world.c
cc *.o -o hello_world

2
这是一个学习翻译单元概念的好时机。
编译器一次只处理一个翻译单元。它不会知道其他可能的翻译单元,将所有的翻译单元组合起来是链接器的工作。
你用于构建程序的命令:
gcc main.c -o out

只有 main.c 创建的那个翻译单元被编译和尝试链接。来自 hello_world.c 的翻译单元根本没有使用。
您需要传递 两个 源文件给前端程序 gcc,以便它可以构建并链接它们两个:
gcc main.c hello_world.c -o out

那么,在你的 makefile 中只放置 gcc *.c -o out 是标准做法吗?看起来这样会难以适应大型代码库(特别是考虑到在子目录中编译的复杂性...)。 - Richard Rast
2
不,Makefile的编写是一门独立的“艺术”。 - Eugene Sh.
@RichardRast,“gcc”确实是一款“标准现代编译器”,并且翻译单元的概念仍然是相关和重要的。 - Eugene Sh.
@EugeneSh。当然,我在这里使用“编译器”一词是不恰当的。但我认为,在实际的大型项目中,你会使用某种简单的包装器来处理所有相关文件的输入。毕竟人们不可能每次向项目添加文件时都要去调整他们的构建脚本。 - Richard Rast
1
@RichardRast 当然,如果您使用某种管理项目的IDE,大多数情况下可能会忽略这些事情。但在实践中,特别是对于由多个/许多开发人员使用不同个人偏好开发的大型项目,该项目并未绑定到特定的IDE,而是使用某些标准构建系统(基于make/cmake)。 - Eugene Sh.
显示剩余4条评论

1
我简要介绍了C程序的构建块,然后检查了通常隐藏在gcc调用后面的构建步骤。
传统的编译语言,例如C和C ++,是组织在源文件中的,每个源文件通常被“编译”为一个“对象文件”。每个源文件都是一个“翻译单元” - 在处理所有包含指令之后。 (因此,一个翻译单元通常由多个文件组成,并且同一个包含文件通常出现在多个翻译单元中 - 文件和翻译单元在严格意义上具有n:m关系。但实际上,可以说“翻译单元”是C文件。)
要将单个源文件编译为对象文件,需要向编译器传递-c标志:
gcc -c myfile.c
这将在相同目录中创建myfile.o或myfile.obj。
对象文件包含机器代码和数据(以及可能的调试信息,但我们在此忽略)。机器代码包含函数,数据以变量的形式出现。对象文件中的函数和变量都有名称,称为“符号”。编译器通常通过在程序中添加下划线或类似字符来转换变量和函数名称,在C ++中生成的(“缠绕”)名称包含有关类型和函数参数的信息。
某些符号,例如全局变量和普通函数的名称,可从其他对象文件中使用;它们被“导出”。
一个符号可以(仅略微简化)被认为是一个地址别名:对于函数,名称是跳转目标地址的别名;对于变量,名称是可读取和写入程序的内存位置的地址别名。
您的文件help.c包含函数herp的代码。在C中,默认情况下函数具有“外部链接”,它们可以从其他翻译单元中使用。它们的名称 - “符号” - 被导出。
在现代C中,使用在不同翻译单元中定义的名称的源文件必须声明该名称。这告诉编译器如何处理它以及在源代码中可以以哪些方式语法上使用它(例如调用函数,分配给变量,索引数组)。编译器生成从此“符号地址”读取或跳转到该“符号地址”的代码;连接器的工作是将所有这些符号地址替换为指向最终可执行文件中的现有数据和代码的“实际”内存位置,以便跳转和内存访问落在所需位置。
在使用某个名称(函数、变量)的文件中,该名称的声明可以是“手动”的,比如 void herp();,直接出现在你的文件中,在第一次使用之前。不过,更典型的情况是,在定义了其他翻译单元可以使用的名称的翻译单元中,这些名称在头文件中声明,即你的 helper.h。使用的翻译单元通过#include指令使用头文件中“罐装”的声明。这里没有任何魔法;一个包含指令只是将包含文件文本插入到文件中,就好像它是直接写到文件中一样。没有任何区别。特别地,包含头文件并不会告诉链接器链接相应的源文件。原因很简单:链接器从不知道包含的文件,因为这一信息在编译成目标文件时被抹掉了。
这意味着,在你的情况下,必须编译help.c,并告诉链接器将其与程序的其余部分(即从main.c的编译中获得的代码)结合起来。
关于如何执行此操作的讨论要更加困难,因为这个过程非常普遍,以至于典型的C编译器集成了编译和链接阶段:gcc -o myprog help.c main.c只需执行创建可执行文件myprog所需的所有操作。
当我们说“编译器”时,比如指的是gcc,我们通常实际上指的是“编译器驱动程序”,它从命令行接收命令和文件,并执行必要的步骤,以达到所需的结果,例如从源文件生成可执行程序。对于gcc而言,真正的编译器是cc1,它生成一个汇编文件,必须使用as将其汇编成一个目标文件。在编译源文件之后,gcc使用适当的选项调用链接器,生成可执行文件。
以下是详细描述这些阶段的示例会话:
$ 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_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 mangling")来查看哪些符号被导出("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”代表“未定义”。 (未定义的__maingcc和/或cygwin的怪癖。) 这里是问题的根源:除非将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做出反应;我可能错过了一些Gnu或Windows构建细节,这些细节由gcc为我们完成。
啊,Makefile。Makefile由目标定义和这些目标的依赖项组成:一行
main: help.o main.o
指定一个名为“main”的目标,其依赖于两个.o文件。 Makefile通常还包含规则,指定如何生成目标。但是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会编译两个文件。无论如何链接都需要完成。


关于 *"The actual compiler for gcc is cc1"*:这在 macOS 上是不同的(一个常见的困惑来源)。 - Peter Mortensen
在macOS上,gcc的实际编译器是cc1。这是一个常见的混淆源(source of confusion)。 - Peter Mortensen

1

是的,看起来您忘记链接 hello_world.c 了。

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


0
你需要告诉编译器你的项目包含两个源文件:
gcc -o out main.c hello_world.c

在您的水平上,我建议使用一个集成开发环境(例如Eclipse CDT)来专注于编程,而不是构建。稍后您将学习如何构建复杂的项目。但现在只需学习编程。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接