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


当前回答

当包含路径不同时

当头文件及其关联的共享库(.lib文件)不同步时,可能会发生链接器错误。让我解释一下。

链接器是如何工作的?链接器通过比较函数声明(在头中声明)和函数定义(在共享库中)的签名来匹配它们。如果链接器找不到完全匹配的函数定义,则可能会出现链接器错误。

即使声明和定义似乎匹配,仍然可能出现链接器错误吗?对它们在源代码中看起来可能相同,但这实际上取决于编译器所看到的内容。基本上,你可能会遇到这样的情况:

// header1.h
typedef int Number;
void foo(Number);

// header2.h
typedef float Number;
void foo(Number); // this only looks the same lexically

注意,即使两个函数声明在源代码中看起来相同,但根据编译器的不同,它们确实不同。

你可能会问,一个人在这样的情况下是如何结束的?当然包括路径!如果在编译共享库时,include路径指向header1.h,而您最终在自己的程序中使用了header2.h,那么您将无法理解发生了什么(双关语)。

下面将解释这在现实世界中如何发生的一个示例。

通过示例进一步阐述

我有两个项目:graphics.lib和main.exe。两个项目都依赖common_math.h。假设库导出以下函数:

// graphics.lib    
#include "common_math.h" 
   
void draw(vec3 p) { ... } // vec3 comes from common_math.h

然后,您继续将库包含在您自己的项目中。

// main.exe
#include "other/common_math.h"
#include "graphics.h"

int main() {
    draw(...);
}

繁荣你得到了一个链接器错误,你不知道它为什么会失败。原因是公共库使用相同includecommon_math.h的不同版本(我在本例中通过包含不同的路径来说明这一点,但可能并不总是那么明显。可能编译器设置中的包含路径不同)。

注意,在这个例子中,链接器会告诉你它找不到draw(),而实际上你知道它显然是由库导出的。你可以花几个小时挠头,想知道出了什么问题。问题是,链接器看到的签名不同,因为参数类型略有不同。在本例中,就编译器而言,vec3在两个项目中都是不同的类型。这可能是因为它们来自两个稍微不同的包含文件(可能包含文件来自库的两个不同版本)。

调试链接器

如果您正在使用Visual Studio,DUMPBIN是您的朋友。我相信其他编译器也有类似的工具。

过程如下:

注意链接器错误中给出的奇怪的损坏名称。(例如。draw@graphics@XYZ)。将库中导出的符号转储到文本文件中。搜索导出的感兴趣符号,并注意损坏的名称不同。请注意,为什么被弄乱的名字最终会不同。您将能够看到参数类型不同,即使它们在源代码中看起来相同。它们不同的原因。在上面给出的示例中,它们是不同的,因为包含文件不同。

[1] 我所说的项目是指一组链接在一起以生成库或可执行文件的源文件。

编辑1:改写第一节,使其更容易理解。请在下面评论,让我知道是否需要修复其他问题。谢谢

其他回答

未能链接到适当的库/对象文件或编译实现文件

通常,每个翻译单元都会生成一个包含该翻译单元中定义的符号定义的对象文件。要使用这些符号,必须链接这些对象文件。

在gcc下,您可以指定要在命令行中链接在一起的所有对象文件,或者一起编译实现文件。

g++ -o test objectFile1.o objectFile2.o -lLibraryName

-我。。。必须位于任何.o/.c/.cpp文件的右侧。

这里的libraryName只是库的裸名,没有特定于平台的添加。例如,在Linux上,库文件通常被称为libfoo.So,但您只能编写-lfo。在Windows上,相同的文件可能被称为foo.lib,但您将使用相同的参数。您可能需要添加目录,在该目录中可以使用-Lûdirectory›找到这些文件。确保不要在-l或-l后面写空格。

对于Xcode:添加用户标题搜索路径->添加库搜索路径->将实际的库引用拖放到项目文件夹中。

在MSVS下,添加到项目中的文件会自动将其对象文件链接在一起,并生成一个lib文件(常见用法)。要在单独的项目中使用符号,您需要需要在项目设置中包含lib文件。这是在项目财产的链接器部分的Input->Additional Dependencies中完成的。(指向lib文件的路径应为在Linker->General->Additional Library Directories中添加)当使用随lib文件提供的第三方库时,失败通常会导致错误。

还可能发生忘记将文件添加到编译中的情况,在这种情况下,不会生成对象文件。在gcc中,您可以将文件添加到命令行。在MSVS中,将文件添加到项目将使其自动编译(尽管文件可以手动从构建中单独排除)。

在Windows编程中,未链接必要库的标志是未解析符号的名称以__imp_开头。在文档中查找函数的名称,它应该指出您需要使用哪个库。例如,MSDN将信息放在名为“库”的部分中每个函数底部的框中。

班级成员:

纯虚拟析构函数需要实现。

声明析构函数pure仍然需要定义它(与常规函数不同):

struct X
{
    virtual ~X() = 0;
};
struct Y : X
{
    ~Y() {}
};
int main()
{
    Y y;
}
//X::~X(){} //uncomment this line for successful definition

这是因为在隐式销毁对象时调用基类析构函数,因此需要定义。

虚拟方法必须实现或定义为纯方法。

这类似于没有定义的非虚拟方法,增加了如下推理:纯声明会生成一个虚拟vtable,您可能会在不使用函数的情况下得到链接器错误:

struct X
{
    virtual void foo();
};
struct Y : X
{
   void foo() {}
};
int main()
{
   Y y; //linker error although there was no call to X::foo
}

要使其工作,请将X::foo()声明为纯:

struct X
{
    virtual void foo() = 0;
};

非虚拟类成员

即使未明确使用,也需要定义某些成员:

struct A
{ 
    ~A();
};

以下内容将产生错误:

A a;      //destructor undefined

实现可以在类定义本身中内联:

struct A
{ 
    ~A() {}
};

或外部:

A::~A() {}

如果实现在类定义之外,但在头中,则必须将方法标记为内联,以防止多重定义。

如果使用,则需要定义所有使用的成员方法。

一个常见的错误是忘记限定名称:

struct A
{
   void foo();
};

void foo() {}

int main()
{
   A a;
   a.foo();
}

定义应为

void A::foo() {}

静态数据成员必须在类外部的单个转换单元中定义:

struct X
{
    static int x;
};
int main()
{
    int x = X::x;
}
//int X::x; //uncomment this line to define X::x

可以为类定义中的整型或枚举类型的静态常量数据成员提供初始值设定项;然而,odr使用这个成员仍然需要如上所述的命名空间范围定义。C++11允许在类内初始化所有静态常量数据成员。

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

如果库相互依赖,则库的链接顺序也很重要。通常,如果库A依赖于库B,那么在链接器标志中,libA必须出现在libB之前。

例如:

// B.h
#ifndef B_H
#define B_H

struct B {
    B(int);
    int x;
};

#endif

// B.cpp
#include "B.h"
B::B(int xx) : x(xx) {}

// A.h
#include "B.h"

struct A {
    A(int x);
    B b;
};

// A.cpp
#include "A.h"

A::A(int x) : b(x) {}

// main.cpp
#include "A.h"

int main() {
    A a(5);
    return 0;
};

创建库:

$ g++ -c A.cpp
$ g++ -c B.cpp
$ ar rvs libA.a A.o 
ar: creating libA.a
a - A.o
$ ar rvs libB.a B.o 
ar: creating libB.a
a - B.o

编译:

$ g++ main.cpp -L. -lB -lA
./libA.a(A.o): In function `A::A(int)':
A.cpp:(.text+0x1c): undefined reference to `B::B(int)'
collect2: error: ld returned 1 exit status
$ g++ main.cpp -L. -lA -lB
$ ./a.out

再说一遍,顺序很重要!

当我们在程序中引用了对象名(类、函数、变量等),并且链接器试图在所有链接的对象文件和库中搜索它时无法找到它的定义时,就会出现“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) 未编译从属文件

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

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

如果所有其他操作都失败,请重新编译。

最近,我只需重新编译有问题的文件,就可以消除Visual Studio 2012中未解决的外部错误。当我重新构建时,错误消失了。

当两个(或多个)库具有循环依赖关系时,通常会发生这种情况。库A尝试使用B.lib中的符号,库B尝试使用A.lib中的字符。两者都不存在。当您尝试编译A时,链接步骤将失败,因为它找不到B.lib。将生成A.lib,但不会生成dll。然后编译B,这将成功并生成B.lib。重新编译A现在可以工作了,因为现在找到了B.lib。