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


当前回答

尽管这是一个有多个公认答案的老问题,但我想分享如何解决一个晦涩的“未定义引用”错误。

不同版本的库

我使用别名来引用std::filesystem::path:filesystem自C++17以来就在标准库中,但我的程序也需要在C++14中编译,所以我决定使用变量别名:

#if (defined _GLIBCXX_EXPERIMENTAL_FILESYSTEM) //is the included filesystem library experimental? (C++14 and newer: <experimental/filesystem>)
using path_t = std::experimental::filesystem::path;
#elif (defined _GLIBCXX_FILESYSTEM) //not experimental (C++17 and newer: <filesystem>)
using path_t = std::filesystem::path;
#endif

假设我有三个文件:main.cpp、file.h、file.cpp:

file.h#include的<实验::filesystem>,并包含上面的代码file.cpp,file.h的实现,#include的“file.h”main.cpp#include的<文件系统>和“file.h”

注意main.cpp和file.h中使用的不同库。由于main.cpp#在<filesystem>之后包含了“file.h”,所以这里使用的文件系统版本是C++17版本。我曾经用以下命令编译程序:

$g++-g-std=c++17-c main.cpp->将main.cpp编译为main.o$g++-g-std=c++17-c file.cpp->将file.cpp和file.h编译为file.o$g++-g-std=c++17-o可执行文件main.o file.o-lsdc++fs->链接main.o和file.o

这样,任何包含在file.o中并在main.o中使用的需要path_t的函数都会出现“未定义的引用”错误,因为main.o引用std::filesystem::path,而file.o引用的是std::experimental::filesystem::path。

决议

为了解决这个问题,我只需要将file.h中的<experimental::filesystem>更改为<filesystem>。

其他回答

当我们在程序中引用了对象名(类、函数、变量等),并且链接器试图在所有链接的对象文件和库中搜索它时无法找到它的定义时,就会出现“Undefined Reference”错误。

因此,当链接器找不到链接对象的定义时,它会发出“未定义引用”错误。从定义中可以清楚地看出,这种错误发生在链接过程的后期。导致“未定义引用”错误的原因多种多样。

一些可能的原因(更频繁):

#1) 没有为对象提供定义

这是导致“未定义引用”错误的最简单原因。程序员只是忘记了定义对象。

考虑以下C++程序。这里我们只指定了函数的原型,然后在主函数中使用了它。

#include <iostream>
int func1();
int main()
{
     
    func1();
}

输出:

main.cpp:(.text+0x5): undefined reference to 'func1()'
collect2: error ld returned 1 exit status

因此,当我们编译此程序时,会发出链接器错误,该错误表示“未定义对‘func1()’的引用”。

为了消除这个错误,我们通过提供函数func1的定义来如下更正程序。现在程序给出了适当的输出。

#include <iostream>
using namespace std;
int func1();
 
int main()
{
     
    func1();
}
int func1(){
    cout<<"hello, world!!";
}

输出:

hello, world!!

#2) 使用的对象定义错误(签名不匹配)

“未定义引用”错误的另一个原因是我们指定了错误的定义。我们在程序中使用任何对象,其定义都不同。

考虑以下C++程序。这里我们调用了func1()。它的原型是int func1()。但其定义与原型不符。如我们所见,函数的定义包含函数的参数。

因此,当编译程序时,由于原型和函数调用匹配,编译是成功的。但是,当链接器试图将函数调用与其定义链接时,它会发现问题并将错误作为“未定义引用”发出。

#include <iostream>
using namespace std;
int func1();
int main()
{
     
    func1();
}
int func1(int n){
    cout<<"hello, world!!";
}

输出:

main.cpp:(.text+0x5): undefined reference to 'func1()'
collect2: error ld returned 1 exit status

因此,为了防止这种错误,我们只需交叉检查程序中所有对象的定义和用法是否匹配。

#3) 对象文件未正确链接

此问题还可能导致“未定义引用”错误。在这里,我们可能有多个源文件,我们可以独立编译它们。这样做时,对象链接不正确,导致“未定义引用”。

考虑以下两个C++程序。在第一个文件中,我们使用了第二个文件中定义的“print()”函数。当我们分别编译这些文件时,第一个文件为打印函数提供“未定义引用”,而第二个文件为主函数提供“不定义引用”。

int print();
int main()
{
    print();
}

输出:

main.cpp:(.text+0x5): undefined reference to 'print()'
collect2: error ld returned 1 exit status

int print() {
    return 42;
}

输出:

(.text+0x20): undefined reference to 'main'
collect2: error ld returned 1 exit status

解决此错误的方法是同时编译两个文件(例如,使用g++)。

除了已经讨论过的原因之外,“未定义的引用”也可能因为以下原因而发生。

#4) 错误的项目类型

当我们在visual studio等C++IDE中指定错误的项目类型,并尝试做项目不期望的事情时,我们就会得到“未定义的引用”。

#5) 没有库

如果程序员没有正确指定库路径,或者完全忘记指定它,那么我们将为程序从库中使用的所有引用获得一个“未定义引用”。

#6) 未编译从属文件

程序员必须确保我们事先编译项目的所有依赖项,以便在编译项目时,编译器找到所有依赖项并成功编译。如果缺少任何依赖项,那么编译器将给出“未定义的引用”。

除了上面讨论的原因之外,“未定义引用”错误还可能在许多其他情况下发生。但底线是程序员搞错了,为了防止这种错误,应该纠正这些错误。

模板实现不可见。

非专用模板的定义必须对使用它们的所有翻译单位可见。这意味着不能分离模板的定义到实现文件。如果必须分离实现,通常的解决方法是在头的末尾包含一个impl文件声明模板。常见的情况是:

template<class T>
struct X
{
    void foo();
};

int main()
{
    X<int> x;
    x.foo();
}

//differentImplementationFile.cpp
template<class T>
void X<T>::foo()
{
}

要解决这个问题,必须将X::foo的定义移动到头文件或使用它的翻译单元可见的某个位置。

专用化模板可以在实现文件中实现,并且实现不必是可见的,但是必须事先声明专用化。

有关进一步的解释和另一种可能的解决方案(显式实例化),请参阅此问题和答案。

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

您正在尝试编译程序并将其与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':

另请参见

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

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

不同的架构

您可能会看到这样的消息:

library machine type 'x64' conflicts with target machine type 'X86'

在这种情况下,这意味着可用符号用于不同于您正在编译的体系结构。

在Visual Studio上,这是由于错误的“平台”,您需要选择正确的平台或安装正确版本的库。

在Linux上,这可能是由于错误的库文件夹(例如,使用lib而不是lib64)。

在MacOS上,可以选择在同一文件中传送两种体系结构。可能是链接希望两个版本都存在,但只有一个版本存在。也可能是库所在的lib/lib64文件夹错误。

需要考虑的一些拼写错误:(我作为初学者经常遇到)

如果您使用的是类:请检查您是否没有在定义函数的cpp文件中的函数名之前忘记“classname::”。如果使用forward声明:请确保声明正确的类型。例如:如果要转发声明“结构”,请使用“结构”而不是“类”。