什么是未定义的参考/未解析的外部符号错误?常见原因是什么?如何解决/预防?


当前回答

使用链接器帮助诊断错误

大多数现代链接器都有一个冗长的选项,可以在不同程度上打印出来;

链接调用(命令行),关于链接阶段中包括哪些库的数据,库的位置,使用的搜索路径。

用于gcc和clang;通常会在命令行中添加-v-Wl、--verbose或-v-Wl和-v。更多详情请点击此处;

Linux ld手册页。LLVM链接器页。“GCC简介”第9章。

对于MSVC,/VERBOSE(特别是/VERBOSE:LIB)将添加到链接命令行。

/VERBOSE链接器选项上的MSDN页面。

其他回答

链接在引用库的对象文件之前使用库

您正在尝试编译程序并将其与GCC工具链链接。链接指定了所有必要的库和库搜索路径如果libfoo依赖于libbar,那么链接会正确地将libfoo放在libbar之前。链接失败,对某些错误的引用未定义。但是所有未定义的东西都在你的头文件中声明#并且实际上是在您链接的库中定义的。

示例在C中。它们也可以是C++

一个涉及您自己构建的静态库的最小示例

my_lib.c文件

#include "my_lib.h"
#include <stdio.h>

void hw(void)
{
    puts("Hello World");
}

my_lib.h

#ifndef MY_LIB_H
#define MT_LIB_H

extern void hw(void);

#endif

例如1.c

#include <my_lib.h>

int main()
{
    hw();
    return 0;
}

构建静态库:

$ gcc -c -o my_lib.o my_lib.c
$ ar rcs libmy_lib.a my_lib.o

编译程序:

$ gcc -I. -c -o eg1.o eg1.c

尝试将其与libmy_lib.a链接,但失败:

$ gcc -o eg1 -L. -lmy_lib eg1.o 
eg1.o: In function `main':
eg1.c:(.text+0x5): undefined reference to `hw'
collect2: error: ld returned 1 exit status

如果您在一个步骤中编译和链接,则会得到相同的结果,例如:

$ gcc -o eg1 -I. -L. -lmy_lib eg1.c
/tmp/ccQk1tvs.o: In function `main':
eg1.c:(.text+0x5): undefined reference to `hw'
collect2: error: ld returned 1 exit status

一个涉及共享系统库的最小示例,即压缩库libz

例如2.c

#include <zlib.h>
#include <stdio.h>

int main()
{
    printf("%s\n",zlibVersion());
    return 0;
}

编译程序:

$ gcc -c -o eg2.o eg2.c

尝试将程序与libz链接并失败:

$ gcc -o eg2 -lz eg2.o 
eg2.o: In function `main':
eg2.c:(.text+0x5): undefined reference to `zlibVersion'
collect2: error: ld returned 1 exit status

如果您一次编译并链接:

$ gcc -o eg2 -I. -lz eg2.c
/tmp/ccxCiGn7.o: In function `main':
eg2.c:(.text+0x5): undefined reference to `zlibVersion'
collect2: error: ld returned 1 exit status

示例2的一个变体涉及pkg配置:

$ gcc -o eg2 $(pkg-config --libs zlib) eg2.o 
eg2.o: In function `main':
eg2.c:(.text+0x5): undefined reference to `zlibVersion'

你做错了什么?

在要链接的对象文件和库序列中程序,您将库放在引用的对象文件之前他们您需要将库放在引用对他们来说。

正确链接示例1:

$ gcc -o eg1 eg1.o -L. -lmy_lib

成功:

$ ./eg1 
Hello World

正确链接示例2:

$ gcc -o eg2 eg2.o -lz

成功:

$ ./eg2 
1.2.8

正确链接示例2 pkg配置变体:

$ gcc -o eg2 eg2.o $(pkg-config --libs zlib) 
$ ./eg2
1.2.8

解释

从这里开始,阅读是可选的。

默认情况下,GCC在您的发行版上生成的链接命令,在中从左到右使用链接中的文件命令行序列。当它发现一个文件引用了某个并且不包含其定义,to将搜索定义在更右边的文件中。如果它最终找到了定义解析引用。如果任何引用在结束时仍未解决,链接失败:链接器没有向后搜索。

首先,示例1,使用静态库my_lib.a

静态库是对象文件的索引存档。当链接器在链接序列中找到-lmy-lib,并指出这是指到静态库/libmy_lib.a,它想知道您的程序是否需要libmy_lib.a中的任何对象文件。

libmy_lib.a中只有一个对象文件,即my_lib.o,并且只定义了一个对象在my_lib.o中,即函数hw。

链接器将决定您的程序需要my_lib.o,如果并且仅当它已经知道您的程序在一个或多个对象文件中引用hw添加到程序中,并且没有添加任何对象文件包含hw的定义。

如果这是真的,那么链接器将从库中提取my_lib.o的副本,并将其添加到您的程序中。然后,您的程序包含hw的定义,因此其对hw的引用被解析。

当您尝试链接程序时,如:

$ gcc -o eg1 -L. -lmy_lib eg1.o

链接器在看到时没有将eg1.o添加到程序中-lmy_库。因为在这一点上,它还没有看到eg1.o。您的程序尚未引用hw:it根本没有做任何引用,因为它做的所有引用在eg1.o中。

因此,链接器不会将my_lib.o添加到程序中,并且没有其他内容用于libmy_lib.a。

接下来,它找到eg1.o,并将其添加到程序中。中的对象文件链接序列总是添加到程序中。现在,该程序使对hw的引用,并且不包含hw的定义;但是链接序列中没有任何内容可以提供缺失释义对hw的引用最终无法解析,链接失败。

第二,示例2,使用共享库libz

共享库不是对象文件或类似文件的存档更像是一个没有主功能的程序而是公开它定义的多个其他符号程序可以在运行时使用它们。

今天,许多Linux发行版配置其GCC工具链,以便其语言驱动程序(GCC、g++、gfortran等)指示系统链接器(ld)根据需要链接共享库。你有一个这样的发行版。

这意味着当链接器在链接序列中找到-lz,并发现这是指到共享库(例如)/usr/lib/x86_64-linux-gnu/libz,它想知道它添加到程序中的任何尚未定义的引用是否具有libz导出的定义

如果这是真的,那么链接器将不会从libz和将它们添加到您的程序中;相反,它只会修改程序的代码从而:-

在运行时,系统程序加载器会将libz的副本加载到无论何时加载程序的副本,都可以执行与程序相同的过程。在运行时,每当程序引用libz,该引用使用中libz副本导出的定义相同的过程。

您的程序只想引用一个由libz导出的定义,即函数zlibVersion,在eg2.c中仅引用一次。如果链接器将该引用添加到程序中,然后找到定义由libz导出,引用被解析

但当您尝试链接程序时,如:

gcc -o eg2 -lz eg2.o

事件的顺序是错误的,其方式与示例1相同。在链接器找到-lz时,没有对任何内容的引用在节目中:他们都在eg2.o中,这还没有被看到。所以链接器决定它不适用于libz。当它达到eg2.o时,将其添加到程序中,然后对zlibVersion有未定义的引用,链接序列完成;该引用未解析,链接失败。

最后,示例2的pkg配置变体现在有了一个显而易见的解释。壳体膨胀后:

gcc -o eg2 $(pkg-config --libs zlib) eg2.o

变为:

gcc -o eg2 -lz eg2.o

这再次只是示例2。

我可以重复示例1中的问题,但不能重复示例2中的问题

联动装置:

gcc -o eg2 -lz eg2.o

对你来说很好!

(或者:在Fedora 23上,这种链接很好,但在Ubuntu 16.04上失败了)

这是因为链接工作的发行版是不配置其GCC工具链以根据需要链接共享库。

过去,类unix系统链接静态和共享是很正常的不同的规则。链接序列中的静态库已链接但是共享库是无条件链接的。

这种行为在链接时是经济的,因为链接器不必考虑程序是否需要共享库:如果是共享库,大多数链接中的大多数库都是共享库。但也有缺点:-

这在运行时是不经济的,因为它会导致共享库与程序一起加载,即使不需要它们。静态库和共享库的不同链接规则可能会令人困惑对于不熟练的程序员,他们可能不知道-lfo是否在他们的联系中将解析为/some/where/libfoo.a或/some/wwhere/libfoo.so,并且可能不理解共享库和静态库之间的区别无论如何

这种权衡导致了今天的分裂局面。一些发行版有更改了共享库的GCC链接规则,以便根据需要这一原则适用于所有图书馆。一些发行版沿用了旧版本方法

为什么即使同时编译和链接,我仍然会遇到这个问题?

如果我这样做:

$ gcc -o eg1 -I. -L. -lmy_lib eg1.c

当然,gcc必须首先编译eg1.c,然后链接生成的带有libmy_lib.a的对象文件,那么它怎么可能不知道该对象文件在进行链接时是否需要?

因为使用单个命令编译和链接不会更改连杆顺序。

当您运行上面的命令时,gcc发现您需要编译+链接。因此,在幕后,它生成一个编译命令,并运行然后生成一个链接命令并运行它,就像您运行了两个命令:

$ gcc -I. -c -o eg1.o eg1.c
$ gcc -o eg1 -L. -lmy_lib eg1.o

因此,如果运行这两个命令,链接就会失败。这个您在失败中注意到的唯一区别是gcc生成了编译+链接情况下的临时对象文件,因为您没有告诉它使用eg1.o.我们看到:

/tmp/ccQk1tvs.o: In function `main'

而不是:

eg1.o: In function `main':

另请参见

指定相互依赖的链接库的顺序错误

将相互依赖的库按错误的顺序排列只是一种方法在其中,您可以获取需要对即将到来的事物进行定义的文件在链接中晚于提供定义的文件。将库放在引用它们的对象文件是犯同样错误的另一种方式。

我的例子:

头文件

class GameCharacter : public GamePart
{
    private:
        static vector<GameCharacter*> characterList;
...
}

.cpp文件:

vector<GameCharacter*> characterList;

这产生了“未定义”加载程序错误,因为“characterList”被声明为静态成员变量,但被定义为全局变量。

我加上这个是因为——虽然其他人在一长串需要注意的事情中列出了这个案例——但这个列表并没有给出示例。这是一个更值得寻找的例子,尤其是在C++中。

修复方法是向全局变量添加限定以定义静态数据成员:

vector<GameCharacter*> GameCharacter::characterList;

同时保持收割台相同。

假设您有一个用c++编写的大型项目,它有一千个.cpp文件和一千个.h文件。假设该项目还依赖于十个静态库。假设我们在Windows上,我们在Visual Studio 20xx中构建项目。当您按Ctrl+F7 Visual Studio开始编译整个解决方案时(假设解决方案中只有一个项目)

编译的意义是什么?

Visual Studio搜索文件.vcxproj并开始编译扩展名为.cpp的每个文件。编译顺序未定义。因此,您不能假设首先编译了main.cpp文件如果.cpp文件依赖于其他.h文件来查找符号可以在.cpp文件中定义,也可以不定义如果存在一个.cpp文件,编译器在其中找不到一个符号,则编译器时间错误将引发消息symbol x not be found对于每个扩展名为.cpp的文件,都会生成一个对象文件.o,并且Visual Studio会将输出写入名为ProjectName.cpp.Clean.txt的文件中,该文件包含链接器必须处理的所有对象文件。

编译的第二步是由Linker完成的。Linker应该合并所有对象文件并最终生成输出(可能是可执行文件或库)

链接项目的步骤

分析所有对象文件,找到仅在头文件中声明的定义(例如:前面的答案中提到的类的一个方法的代码,或者初始化类内部的静态变量的事件)如果在目标文件中找不到一个符号,也会在其他库中搜索。对于将新库添加到项目的配置财产->VC++目录->库目录,您在此处指定了用于搜索库的其他文件夹,以及用于指定库名称的配置财产->链接器->输入。-如果链接器找不到您在一个.cpp中编写的符号,则会引发一个链接器时间错误,听起来可能像错误LNK2001:未解析的外部符号“void __cdecl foo(void)”(?foo@@YAXXZ)

观察

一旦链接器找到一个符号,他就不会在其他库中搜索它链接库的顺序很重要。如果Linker在一个静态库中找到一个外部符号,他会在项目的输出中包含该符号。但是,如果库是共享的(动态的),他不会在输出中包含代码(符号),但可能会发生运行时崩溃

如何解决这种错误

编译器时间错误:

确保编写的c++项目语法正确。

链接器时间错误

定义在头文件中声明的所有符号使用#pragma一次,如果编译的当前.cpp中已包含一个标头,则允许编译器不包含该标头确保外部库中不包含可能与头文件中定义的其他符号冲突的符号使用模板时,请确保在头文件中包含每个模板函数的定义,以允许编译器为任何实例化生成适当的代码。

这是每个VC++程序员一再看到的最令人困惑的错误消息之一。让我们先把事情弄清楚。

A.什么是符号?简而言之,符号就是名称。它可以是变量名、函数名、类名、typedef名,或者除了那些属于C++语言的名称和符号之外的任何名称和符号。它由用户定义或由依赖库(另一个用户定义的)引入。

B.什么是外部的?在VC++中,每个源文件(.cpp、.c等)都被视为一个翻译单元,编译器一次编译一个单元,并为当前翻译单元生成一个目标文件(.obj)。(请注意,此源文件包含的每个头文件都将被预处理,并将被视为此翻译单元的一部分)翻译单元中的所有内容都被视为内部内容,其他所有内容都视为外部内容。在C++中,可以使用关键字extern、__declspec(dllimport)等引用外部符号。

C.什么是“决心”?Resolve是一个链接时间术语。在链接时,链接器尝试为对象文件中无法在内部找到其定义的每个符号找到外部定义。此搜索过程的范围包括:

编译时生成的所有对象文件显式或隐式的所有库(.lib)指定为此生成应用程序的附加依赖项。

此搜索过程称为解析。

D.最后,为什么是未解决的外部符号?如果链接器找不到内部没有定义的符号的外部定义,则会报告“未解决的外部符号”错误。

E.LNK2019的可能原因:未解决的外部符号错误。我们已经知道,此错误是由于链接器未能找到外部符号的定义所致,可能的原因如下:

定义已存在

例如,如果我们在.cpp中定义了一个名为foo的函数:

int foo()
{
    return 0;
}

在b.cpp中,我们希望调用函数foo,因此我们添加

void foo();

要声明函数foo(),并在另一个函数体中调用它,请使用bar():

void bar()
{
    foo();
}

现在,当您构建此代码时,您将收到一个LNK2019错误,抱怨foo是一个未解析的符号。在本例中,我们知道foo()的定义在.cpp中,但与我们调用的定义不同(返回值不同)。这就是定义存在的情况。

定义不存在

如果我们想调用库中的某些函数,但导入库没有添加到项目设置的附加依赖项列表中(设置自:项目|财产|配置财产|链接器|输入|附加依赖项)。现在链接器将报告LNK2019,因为当前搜索范围中不存在该定义。

符号是在C程序中定义的,并在C++代码中使用。

函数(或变量)void foo()是在C程序中定义的,您尝试在C++程序中使用它:

void foo();
int main()
{
    foo();
}

C++链接器希望名称被损坏,因此必须将函数声明为:

extern "C" void foo();
int main()
{
    foo();
}

等效地,函数(或变量)void foo()不是在C程序中定义的,而是在C++中定义的但具有C链接:

extern "C" void foo();

并且尝试在C++链接的C++程序中使用它。

如果整个库包含在头文件中(并且编译为C代码);包括以下内容:;

extern "C" {
    #include "cheader.h"
}